summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/Command.cs6
-rw-r--r--src/SMAPI/Framework/CommandManager.cs37
-rw-r--r--src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs20
-rw-r--r--src/SMAPI/Framework/Commands/HelpCommand.cs8
-rw-r--r--src/SMAPI/Framework/Commands/IInternalCommand.cs2
-rw-r--r--src/SMAPI/Framework/Commands/ReloadI18nCommand.cs2
-rw-r--r--src/SMAPI/Framework/Content/AssetData.cs7
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForDictionary.cs4
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs4
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForMap.cs75
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForObject.cs25
-rw-r--r--src/SMAPI/Framework/Content/AssetEditOperation.cs6
-rw-r--r--src/SMAPI/Framework/Content/AssetInfo.cs16
-rw-r--r--src/SMAPI/Framework/Content/AssetInterceptorChange.cs7
-rw-r--r--src/SMAPI/Framework/Content/AssetLoadOperation.cs6
-rw-r--r--src/SMAPI/Framework/Content/AssetName.cs21
-rw-r--r--src/SMAPI/Framework/Content/AssetOperationGroup.cs2
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs17
-rw-r--r--src/SMAPI/Framework/Content/TilesheetReference.cs2
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs38
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs28
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs40
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs13
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs21
-rw-r--r--src/SMAPI/Framework/ContentPack.cs9
-rw-r--r--src/SMAPI/Framework/CursorPosition.cs4
-rw-r--r--src/SMAPI/Framework/DeprecationManager.cs85
-rw-r--r--src/SMAPI/Framework/DeprecationWarning.cs17
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs2
-rw-r--r--src/SMAPI/Framework/Events/IManagedEvent.cs2
-rw-r--r--src/SMAPI/Framework/Events/ManagedEvent.cs8
-rw-r--r--src/SMAPI/Framework/Events/ManagedEventHandler.cs4
-rw-r--r--src/SMAPI/Framework/Events/ModContentEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModDisplayEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModEventsBase.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModGameLoopEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModInputEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModMultiplayerEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModPlayerEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModSpecialisedEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModWorldEvents.cs2
-rw-r--r--src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs2
-rw-r--r--src/SMAPI/Framework/Exceptions/SContentLoadException.cs4
-rw-r--r--src/SMAPI/Framework/GameVersion.cs10
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs26
-rw-r--r--src/SMAPI/Framework/Input/GamePadStateBuilder.cs9
-rw-r--r--src/SMAPI/Framework/Input/IInputStateBuilder.cs2
-rw-r--r--src/SMAPI/Framework/Input/KeyboardStateBuilder.cs4
-rw-r--r--src/SMAPI/Framework/Input/MouseStateBuilder.cs2
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs6
-rw-r--r--src/SMAPI/Framework/InternalExtensions.cs7
-rw-r--r--src/SMAPI/Framework/Logging/InterceptingTextWriter.cs24
-rw-r--r--src/SMAPI/Framework/Logging/LogFileManager.cs4
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs34
-rw-r--r--src/SMAPI/Framework/ModHelpers/BaseHelper.cs17
-rw-r--r--src/SMAPI/Framework/ModHelpers/CommandHelper.cs10
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs46
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs8
-rw-r--r--src/SMAPI/Framework/ModHelpers/DataHelper.cs34
-rw-r--r--src/SMAPI/Framework/ModHelpers/GameContentHelper.cs30
-rw-r--r--src/SMAPI/Framework/ModHelpers/InputHelper.cs8
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModContentHelper.cs28
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModHelper.cs12
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs19
-rw-r--r--src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs14
-rw-r--r--src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs32
-rw-r--r--src/SMAPI/Framework/ModHelpers/TranslationHelper.cs12
-rw-r--r--src/SMAPI/Framework/ModLinked.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs8
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs2
-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/Finders/EventFinder.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs15
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs13
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs8
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs25
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs10
-rw-r--r--src/SMAPI/Framework/ModLoading/IInstructionHandler.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/InvalidModStateException.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs38
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs91
-rw-r--r--src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs9
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs5
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/ArchitectureAssemblyRewriter.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs20
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HarmonyRewriter.cs16
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs19
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs15
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs13
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs14
-rw-r--r--src/SMAPI/Framework/ModRegistry.cs34
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs78
-rw-r--r--src/SMAPI/Framework/Monitor.cs2
-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/Networking/SGalaxyNetClient.cs2
-rw-r--r--src/SMAPI/Framework/Networking/SGalaxyNetServer.cs2
-rw-r--r--src/SMAPI/Framework/Networking/SLidgrenClient.cs2
-rw-r--r--src/SMAPI/Framework/Networking/SLidgrenServer.cs2
-rw-r--r--src/SMAPI/Framework/Reflection/CacheEntry.cs12
-rw-r--r--src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs2
-rw-r--r--src/SMAPI/Framework/Reflection/ReflectedField.cs12
-rw-r--r--src/SMAPI/Framework/Reflection/ReflectedMethod.cs18
-rw-r--r--src/SMAPI/Framework/Reflection/ReflectedProperty.cs10
-rw-r--r--src/SMAPI/Framework/Reflection/Reflector.cs184
-rw-r--r--src/SMAPI/Framework/Rendering/SDisplayDevice.cs8
-rw-r--r--src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs4
-rw-r--r--src/SMAPI/Framework/RequestExitDelegate.cs9
-rw-r--r--src/SMAPI/Framework/SChatBox.cs4
-rw-r--r--src/SMAPI/Framework/SCore.cs210
-rw-r--r--src/SMAPI/Framework/SGame.cs31
-rw-r--r--src/SMAPI/Framework/SGameRunner.cs4
-rw-r--r--src/SMAPI/Framework/SModHooks.cs4
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs89
-rw-r--r--src/SMAPI/Framework/Serialization/KeybindConverter.cs4
-rw-r--r--src/SMAPI/Framework/Singleton.cs2
-rw-r--r--src/SMAPI/Framework/SnapshotDiff.cs6
-rw-r--r--src/SMAPI/Framework/SnapshotItemListDiff.cs5
-rw-r--r--src/SMAPI/Framework/SnapshotListDiff.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/ChestTracker.cs5
-rw-r--r--src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs5
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ImmutableCollectionWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs3
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs8
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs15
-rw-r--r--src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/IValueWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/IWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/LocationTracker.cs14
-rw-r--r--src/SMAPI/Framework/StateTracking/PlayerTracker.cs16
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs11
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs6
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs23
-rw-r--r--src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs122
-rw-r--r--src/SMAPI/Framework/Translator.cs22
-rw-r--r--src/SMAPI/Framework/Utilities/ContextHash.cs2
-rw-r--r--src/SMAPI/Framework/Utilities/TickCacheDictionary.cs5
-rw-r--r--src/SMAPI/Framework/WatcherCore.cs4
168 files changed, 1262 insertions, 1293 deletions
diff --git a/src/SMAPI/Framework/Command.cs b/src/SMAPI/Framework/Command.cs
index 776ba238..dca1dd09 100644
--- a/src/SMAPI/Framework/Command.cs
+++ b/src/SMAPI/Framework/Command.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Framework
@@ -11,7 +9,7 @@ namespace StardewModdingAPI.Framework
** Accessor
*********/
/// <summary>The mod that registered the command (or <c>null</c> if registered by SMAPI).</summary>
- public IModMetadata Mod { get; }
+ public IModMetadata? Mod { get; }
/// <summary>The command name, which the user must type to trigger it.</summary>
public string Name { get; }
@@ -31,7 +29,7 @@ namespace StardewModdingAPI.Framework
/// <param name="name">The command name, which the user must type to trigger it.</param>
/// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param>
/// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param>
- public Command(IModMetadata mod, string name, string documentation, Action<string, string[]> callback)
+ public Command(IModMetadata? mod, string name, string documentation, Action<string, string[]> callback)
{
this.Mod = mod;
this.Name = name;
diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs
index df798b0c..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;
@@ -36,20 +35,19 @@ namespace StardewModdingAPI.Framework
/// <param name="name">The command name, which the user must type to trigger it.</param>
/// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param>
/// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param>
- /// <param name="allowNullCallback">Whether to allow a null <paramref name="callback"/> argument; this should only used for backwards compatibility.</param>
/// <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, bool allowNullCallback = false)
+ 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))
throw new ArgumentNullException(nameof(name), "Can't register a command with no name.");
if (name.Any(char.IsWhiteSpace))
throw new FormatException($"Can't register the '{name}' command because the name can't contain whitespace.");
- if (callback == null && !allowNullCallback)
+ if (callback == null)
throw new ArgumentNullException(nameof(callback), $"Can't register the '{name}' command because without a callback.");
// ensure uniqueness
@@ -73,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;
}
@@ -95,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))
@@ -109,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
@@ -117,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;
@@ -141,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;
@@ -192,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;
@@ -221,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/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs
index fcfa928e..6dc6f131 100644
--- a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs
+++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -73,9 +71,9 @@ namespace StardewModdingAPI.Framework.Commands
private IEnumerable<SearchResult> FilterPatches(string[] searchTerms)
{
bool hasSearch = searchTerms.Any();
- bool IsMatch(string target) => !hasSearch || searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1);
+ bool IsMatch(string? target) => !hasSearch || searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1);
- foreach (var patch in this.GetAllPatches())
+ foreach (SearchResult patch in this.GetAllPatches())
{
// matches entire patch
if (IsMatch(patch.MethodDescription))
@@ -85,10 +83,10 @@ namespace StardewModdingAPI.Framework.Commands
}
// matches individual patchers
- foreach (var pair in patch.PatchTypesByOwner.ToArray())
+ foreach ((string patcherId, ISet<PatchType> patchTypes) in patch.PatchTypesByOwner.ToArray())
{
- if (!IsMatch(pair.Key) && !pair.Value.Any(type => IsMatch(type.ToString())))
- patch.PatchTypesByOwner.Remove(pair.Key);
+ if (!IsMatch(patcherId) && !patchTypes.Any(type => IsMatch(type.ToString())))
+ patch.PatchTypesByOwner.Remove(patcherId);
}
if (patch.PatchTypesByOwner.Any())
@@ -114,13 +112,13 @@ namespace StardewModdingAPI.Framework.Commands
// get patch types by owner
var typesByOwner = new Dictionary<string, ISet<PatchType>>();
- foreach (var group in patchGroups)
+ foreach ((PatchType type, IReadOnlyCollection<Patch> patches) in patchGroups)
{
- foreach (var patch in group.Value)
+ foreach (Patch patch in patches)
{
- if (!typesByOwner.TryGetValue(patch.owner, out ISet<PatchType> patchTypes))
+ if (!typesByOwner.TryGetValue(patch.owner, out ISet<PatchType>? patchTypes))
typesByOwner[patch.owner] = patchTypes = new HashSet<PatchType>();
- patchTypes.Add(group.Key);
+ patchTypes.Add(type);
}
}
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/Commands/IInternalCommand.cs b/src/SMAPI/Framework/Commands/IInternalCommand.cs
index 32e3e9f1..abf105b6 100644
--- a/src/SMAPI/Framework/Commands/IInternalCommand.cs
+++ b/src/SMAPI/Framework/Commands/IInternalCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Framework.Commands
{
/// <summary>A core SMAPI console command.</summary>
diff --git a/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs b/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs
index 2043b35e..12328bb6 100644
--- a/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs
+++ b/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Framework.Commands
diff --git a/src/SMAPI/Framework/Content/AssetData.cs b/src/SMAPI/Framework/Content/AssetData.cs
index be4a7ce6..0367e999 100644
--- a/src/SMAPI/Framework/Content/AssetData.cs
+++ b/src/SMAPI/Framework/Content/AssetData.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Framework.Content
@@ -7,12 +5,13 @@ namespace StardewModdingAPI.Framework.Content
/// <summary>Base implementation for a content helper which encapsulates access and changes to content being read from a data file.</summary>
/// <typeparam name="TValue">The interface value type.</typeparam>
internal class AssetData<TValue> : AssetInfo, IAssetData<TValue>
+ where TValue : notnull
{
/*********
** Fields
*********/
/// <summary>A callback to invoke when the data is replaced (if any).</summary>
- private readonly Action<TValue> OnDataReplaced;
+ private readonly Action<TValue>? OnDataReplaced;
/*********
@@ -31,7 +30,7 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="data">The content data being read.</param>
/// <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>
- public AssetData(string locale, IAssetName assetName, TValue data, Func<string, string> getNormalizedPath, Action<TValue> onDataReplaced)
+ public AssetData(string? locale, IAssetName assetName, TValue data, Func<string, string> getNormalizedPath, Action<TValue>? onDataReplaced)
: base(locale, assetName, data.GetType(), getNormalizedPath)
{
this.Data = data;
diff --git a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs
index 06dbe259..d9bfa7bf 100644
--- a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
@@ -17,7 +15,7 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="data">The content data being read.</param>
/// <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>
- public AssetDataForDictionary(string locale, IAssetName assetName, IDictionary<TKey, TValue> data, Func<string, string> getNormalizedPath, Action<IDictionary<TKey, TValue>> onDataReplaced)
+ public AssetDataForDictionary(string? locale, IAssetName assetName, IDictionary<TKey, TValue> data, Func<string, string> getNormalizedPath, Action<IDictionary<TKey, TValue>> onDataReplaced)
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
}
}
diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs
index 8e59cd27..97729c95 100644
--- a/src/SMAPI/Framework/Content/AssetDataForImage.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
@@ -27,7 +25,7 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="data">The content data being read.</param>
/// <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>
- public AssetDataForImage(string locale, IAssetName assetName, Texture2D data, Func<string, string> getNormalizedPath, Action<Texture2D> onDataReplaced)
+ public AssetDataForImage(string? locale, IAssetName assetName, Texture2D data, Func<string, string> getNormalizedPath, Action<Texture2D> onDataReplaced)
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
/// <inheritdoc />
diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs
index 0425e195..133dcc6c 100644
--- a/src/SMAPI/Framework/Content/AssetDataForMap.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs
@@ -1,14 +1,15 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
+using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using xTile;
+using xTile.Dimensions;
using xTile.Layers;
using xTile.Tiles;
+using Rectangle = Microsoft.Xna.Framework.Rectangle;
namespace StardewModdingAPI.Framework.Content
{
@@ -16,6 +17,13 @@ namespace StardewModdingAPI.Framework.Content
internal class AssetDataForMap : AssetData<Map>, IAssetDataForMap
{
/*********
+ ** Fields
+ *********/
+ /// <summary>Simplifies access to private code.</summary>
+ private readonly Reflector Reflection;
+
+
+ /*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
@@ -24,8 +32,12 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="data">The content data being read.</param>
/// <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>
- public AssetDataForMap(string locale, IAssetName assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced)
- : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
+ /// <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)
+ : base(locale, assetName, data, getNormalizedPath, onDataReplaced)
+ {
+ this.Reflection = reflection;
+ }
/// <inheritdoc />
/// <remarks>Derived from <see cref="GameLocation.ApplyMapOverride(Map,string,Rectangle?,Rectangle?)"/> with a few changes:
@@ -112,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);
@@ -123,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)
@@ -137,6 +150,42 @@ namespace StardewModdingAPI.Framework.Content
}
}
+ /// <inheritdoc />
+ public bool ExtendMap(Map map, int minWidth, int minHeight)
+ {
+ bool resized = false;
+
+ // resize layers
+ foreach (Layer layer in map.Layers)
+ {
+ // check if resize needed
+ if (layer.LayerWidth >= minWidth && layer.LayerHeight >= minHeight)
+ continue;
+ resized = true;
+
+ // build new tile matrix
+ int width = Math.Max(minWidth, layer.LayerWidth);
+ int height = Math.Max(minHeight, layer.LayerHeight);
+ Tile[,] tiles = new Tile[width, height];
+ for (int x = 0; x < layer.LayerWidth; x++)
+ {
+ for (int y = 0; y < layer.LayerHeight; y++)
+ tiles[x, y] = layer.Tiles[x, y];
+ }
+
+ // update fields
+ this.Reflection.GetField<Tile[,]>(layer, "m_tiles").SetValue(tiles);
+ this.Reflection.GetField<TileArray>(layer, "m_tileArray").SetValue(new TileArray(layer, tiles));
+ this.Reflection.GetField<Size>(layer, "m_layerSize").SetValue(new Size(width, height));
+ }
+
+ // resize map
+ if (resized)
+ this.Reflection.GetMethod(map, "UpdateDisplaySize").Invoke();
+
+ return resized;
+ }
+
/*********
** Private methods
@@ -145,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)
{
@@ -170,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/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs
index 4a6df64b..e508ca30 100644
--- a/src/SMAPI/Framework/Content/AssetDataForObject.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs
@@ -1,8 +1,7 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Graphics;
+using StardewModdingAPI.Framework.Reflection;
using xTile;
namespace StardewModdingAPI.Framework.Content
@@ -11,6 +10,13 @@ namespace StardewModdingAPI.Framework.Content
internal class AssetDataForObject : AssetData<object>, IAssetData
{
/*********
+ ** Fields
+ *********/
+ /// <summary>Simplifies access to private code.</summary>
+ private readonly Reflector Reflection;
+
+
+ /*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
@@ -18,15 +24,20 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="assetName">The asset name being read.</param>
/// <param name="data">The content data being read.</param>
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
- public AssetDataForObject(string locale, IAssetName assetName, object data, Func<string, string> getNormalizedPath)
- : base(locale, assetName, data, getNormalizedPath, onDataReplaced: null) { }
+ /// <param name="reflection">Simplifies access to private code.</param>
+ public AssetDataForObject(string? locale, IAssetName assetName, object data, Func<string, string> getNormalizedPath, Reflector reflection)
+ : base(locale, assetName, data, getNormalizedPath, onDataReplaced: null)
+ {
+ this.Reflection = reflection;
+ }
/// <summary>Construct an instance.</summary>
/// <param name="info">The asset metadata.</param>
/// <param name="data">The content data being read.</param>
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
- public AssetDataForObject(IAssetInfo info, object data, Func<string, string> getNormalizedPath)
- : this(info.Locale, info.Name, data, getNormalizedPath) { }
+ /// <param name="reflection">Simplifies access to private code.</param>
+ public AssetDataForObject(IAssetInfo info, object data, Func<string, string> getNormalizedPath, Reflector reflection)
+ : this(info.Locale, info.Name, data, getNormalizedPath, reflection) { }
/// <inheritdoc />
public IAssetDataForDictionary<TKey, TValue> AsDictionary<TKey, TValue>()
@@ -43,7 +54,7 @@ namespace StardewModdingAPI.Framework.Content
/// <inheritdoc />
public IAssetDataForMap AsMap()
{
- return new AssetDataForMap(this.Locale, this.Name, this.GetData<Map>(), this.GetNormalizedPath, this.ReplaceWith);
+ return new AssetDataForMap(this.Locale, this.Name, this.GetData<Map>(), this.GetNormalizedPath, this.ReplaceWith, this.Reflection);
}
/// <inheritdoc />
diff --git a/src/SMAPI/Framework/Content/AssetEditOperation.cs b/src/SMAPI/Framework/Content/AssetEditOperation.cs
index 1b7d0c93..464948b0 100644
--- a/src/SMAPI/Framework/Content/AssetEditOperation.cs
+++ b/src/SMAPI/Framework/Content/AssetEditOperation.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
@@ -18,7 +16,7 @@ namespace StardewModdingAPI.Framework.Content
public AssetEditPriority Priority { get; }
/// <summary>The content pack on whose behalf the edit is being applied, if any.</summary>
- public IModMetadata OnBehalfOf { get; }
+ public IModMetadata? OnBehalfOf { get; }
/// <summary>Apply the edit to an asset.</summary>
public Action<IAssetData> ApplyEdit { get; }
@@ -32,7 +30,7 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="priority">If there are multiple edits that apply to the same asset, the priority with which this one should be applied.</param>
/// <param name="onBehalfOf">The content pack on whose behalf the edit is being applied, if any.</param>
/// <param name="applyEdit">Apply the edit to an asset.</param>
- public AssetEditOperation(IModMetadata mod, AssetEditPriority priority, IModMetadata onBehalfOf, Action<IAssetData> applyEdit)
+ public AssetEditOperation(IModMetadata mod, AssetEditPriority priority, IModMetadata? onBehalfOf, Action<IAssetData> applyEdit)
{
this.Mod = mod;
this.Priority = priority;
diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs
index 51dcc61f..16b71487 100644
--- a/src/SMAPI/Framework/Content/AssetInfo.cs
+++ b/src/SMAPI/Framework/Content/AssetInfo.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Graphics;
@@ -19,7 +17,7 @@ namespace StardewModdingAPI.Framework.Content
** Accessors
*********/
/// <inheritdoc />
- public string Locale { get; }
+ public string? Locale { get; }
/// <inheritdoc />
public IAssetName Name { get; }
@@ -28,13 +26,13 @@ namespace StardewModdingAPI.Framework.Content
public IAssetName NameWithoutLocale { get; }
/// <inheritdoc />
- [Obsolete($"Use {nameof(Name)} or {nameof(NameWithoutLocale)} instead. This property will be removed in SMAPI 4.0.0.")]
+ [Obsolete($"Use {nameof(AssetInfo.Name)} or {nameof(AssetInfo.NameWithoutLocale)} instead. This property will be removed in SMAPI 4.0.0.")]
public string AssetName
{
get
{
SCore.DeprecationManager.Warn(
- source: SCore.DeprecationManager.GetSourceNameFromStack(),
+ source: SCore.DeprecationManager.GetModFromStack(),
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetName)}",
version: "3.14.0",
severity: DeprecationLevel.Notice
@@ -56,7 +54,7 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="assetName">The asset name being read.</param>
/// <param name="type">The content type being read.</param>
/// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
- public AssetInfo(string locale, IAssetName assetName, Type type, Func<string, string> getNormalizedPath)
+ public AssetInfo(string? locale, IAssetName assetName, Type type, Func<string, string> getNormalizedPath)
{
this.Locale = locale;
this.Name = assetName;
@@ -66,11 +64,11 @@ namespace StardewModdingAPI.Framework.Content
}
/// <inheritdoc />
- [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead. This method will be removed in SMAPI 4.0.0.")]
+ [Obsolete($"Use {nameof(Name)}.{nameof(IAssetName.IsEquivalentTo)} or {nameof(AssetInfo.NameWithoutLocale)}.{nameof(IAssetName.IsEquivalentTo)} instead. This method will be removed in SMAPI 4.0.0.")]
public bool AssetNameEquals(string path)
{
SCore.DeprecationManager.Warn(
- source: SCore.DeprecationManager.GetSourceNameFromStack(),
+ source: SCore.DeprecationManager.GetModFromStack(),
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetNameEquals)}",
version: "3.14.0",
severity: DeprecationLevel.Notice
@@ -106,7 +104,7 @@ namespace StardewModdingAPI.Framework.Content
return "string";
// default
- return type.FullName;
+ return type.FullName!;
}
}
}
diff --git a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
index 7f53db9b..fc8199e8 100644
--- a/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
+++ b/src/SMAPI/Framework/Content/AssetInterceptorChange.cs
@@ -1,9 +1,8 @@
-#nullable disable
-
using System;
using System.Reflection;
using StardewModdingAPI.Internal;
+#pragma warning disable CS0618 // obsolete asset interceptors deliberately supported here
namespace StardewModdingAPI.Framework.Content
{
/// <summary>A wrapper for <see cref="IAssetEditor"/> and <see cref="IAssetLoader"/> for internal cache invalidation.</summary>
@@ -46,11 +45,11 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="asset">Basic metadata about the asset being loaded.</param>
public bool CanIntercept(IAssetInfo asset)
{
- MethodInfo canIntercept = this.GetType().GetMethod(nameof(this.CanInterceptImpl), BindingFlags.Instance | BindingFlags.NonPublic);
+ MethodInfo? canIntercept = this.GetType().GetMethod(nameof(this.CanInterceptImpl), BindingFlags.Instance | BindingFlags.NonPublic);
if (canIntercept == null)
throw new InvalidOperationException($"SMAPI couldn't access the {nameof(AssetInterceptorChange)}.{nameof(this.CanInterceptImpl)} implementation.");
- return (bool)canIntercept.MakeGenericMethod(asset.DataType).Invoke(this, new object[] { asset });
+ return (bool)canIntercept.MakeGenericMethod(asset.DataType).Invoke(this, new object[] { asset })!;
}
diff --git a/src/SMAPI/Framework/Content/AssetLoadOperation.cs b/src/SMAPI/Framework/Content/AssetLoadOperation.cs
index 73e60e24..b6cdec27 100644
--- a/src/SMAPI/Framework/Content/AssetLoadOperation.cs
+++ b/src/SMAPI/Framework/Content/AssetLoadOperation.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
@@ -15,7 +13,7 @@ namespace StardewModdingAPI.Framework.Content
public IModMetadata Mod { get; }
/// <summary>The content pack on whose behalf the asset is being loaded, if any.</summary>
- public IModMetadata OnBehalfOf { get; }
+ public IModMetadata? OnBehalfOf { get; }
/// <summary>If there are multiple loads that apply to the same asset, the priority with which this one should be applied.</summary>
public AssetLoadPriority Priority { get; }
@@ -32,7 +30,7 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="priority">If there are multiple loads that apply to the same asset, the priority with which this one should be applied.</param>
/// <param name="onBehalfOf">The content pack on whose behalf the asset is being loaded, if any.</param>
/// <param name="getData">Load the initial value for an asset.</param>
- public AssetLoadOperation(IModMetadata mod, AssetLoadPriority priority, IModMetadata onBehalfOf, Func<IAssetInfo, object> getData)
+ public AssetLoadOperation(IModMetadata mod, AssetLoadPriority priority, IModMetadata? onBehalfOf, Func<IAssetInfo, object> getData)
{
this.Mod = mod;
this.Priority = priority;
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/AssetOperationGroup.cs b/src/SMAPI/Framework/Content/AssetOperationGroup.cs
index e3c3f92c..a2fcb722 100644
--- a/src/SMAPI/Framework/Content/AssetOperationGroup.cs
+++ b/src/SMAPI/Framework/Content/AssetOperationGroup.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Framework.Content
{
/// <summary>A set of operations to apply to an asset for a given <see cref="IAssetEditor"/> or <see cref="IAssetLoader"/> implementation.</summary>
diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs
index 4e620d28..cbb6c1e9 100644
--- a/src/SMAPI/Framework/Content/ContentCache.cs
+++ b/src/SMAPI/Framework/Content/ContentCache.cs
@@ -1,12 +1,9 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
-using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Utilities;
-using StardewValley;
namespace StardewModdingAPI.Framework.Content
{
@@ -42,11 +39,10 @@ namespace StardewModdingAPI.Framework.Content
** Constructor
****/
/// <summary>Construct an instance.</summary>
- /// <param name="contentManager">The underlying content manager whose cache to manage.</param>
- /// <param name="reflection">Simplifies access to private game code.</param>
- public ContentCache(LocalizedContentManager contentManager, Reflector reflection)
+ /// <param name="loadedAssets">The asset cache for the underlying content manager.</param>
+ public ContentCache(Dictionary<string, object> loadedAssets)
{
- this.Cache = reflection.GetField<Dictionary<string, object>>(contentManager, "loadedAssets").GetValue();
+ this.Cache = loadedAssets;
}
/****
@@ -66,7 +62,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 +90,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/Content/TilesheetReference.cs b/src/SMAPI/Framework/Content/TilesheetReference.cs
index cdc4bc62..0339b802 100644
--- a/src/SMAPI/Framework/Content/TilesheetReference.cs
+++ b/src/SMAPI/Framework/Content/TilesheetReference.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using xTile.Dimensions;
namespace StardewModdingAPI.Framework.Content
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 81820b05..92452224 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -71,7 +69,7 @@ namespace StardewModdingAPI.Framework
private readonly ReaderWriterLockSlim ContentManagerLock = new();
/// <summary>A cache of ordered tilesheet IDs used by vanilla maps.</summary>
- private readonly Dictionary<string, TilesheetReference[]> VanillaTilesheets = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary<string, TilesheetReference[]?> VanillaTilesheets = new(StringComparer.OrdinalIgnoreCase);
/// <summary>An unmodified content manager which doesn't intercept assets, used to compare asset data.</summary>
private readonly LocalizedContentManager VanillaContentManager;
@@ -230,7 +228,7 @@ namespace StardewModdingAPI.Framework
public void OnAdditionalLanguagesInitialized()
{
// update locale cache for custom languages, and load it now (since languages added later won't work)
- var customLanguages = this.MainContentManager.Load<List<ModLanguage>>("Data/AdditionalLanguages");
+ var customLanguages = this.MainContentManager.Load<List<ModLanguage?>>("Data/AdditionalLanguages");
this.LocaleCodes = new Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>>(() => this.GetLocaleCodes(customLanguages));
_ = this.LocaleCodes.Value;
}
@@ -303,7 +301,7 @@ namespace StardewModdingAPI.Framework
/// <param name="contentManagerID">The unique name for the content manager which should load this asset.</param>
/// <param name="relativePath">The asset name within the mod folder.</param>
/// <returns>Returns whether the asset was parsed successfully.</returns>
- public bool TryParseManagedAssetKey(string key, out string contentManagerID, out IAssetName relativePath)
+ public bool TryParseManagedAssetKey(string key, [NotNullWhen(true)] out string? contentManagerID, [NotNullWhen(true)] out IAssetName? relativePath)
{
contentManagerID = null;
relativePath = null;
@@ -333,9 +331,10 @@ namespace StardewModdingAPI.Framework
/// <param name="contentManagerID">The unique name for the content manager which should load this asset.</param>
/// <param name="assetName">The asset name within the mod folder.</param>
public bool DoesManagedAssetExist<T>(string contentManagerID, IAssetName assetName)
+ where T : notnull
{
// get content manager
- IContentManager contentManager = this.ContentManagerLock.InReadLock(() =>
+ IContentManager? contentManager = this.ContentManagerLock.InReadLock(() =>
this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID)
);
if (contentManager == null)
@@ -350,9 +349,10 @@ namespace StardewModdingAPI.Framework
/// <param name="contentManagerID">The unique name for the content manager which should load this asset.</param>
/// <param name="relativePath">The asset name within the mod folder.</param>
public T LoadManagedAsset<T>(string contentManagerID, IAssetName relativePath)
+ where T : notnull
{
// get content manager
- IContentManager contentManager = this.ContentManagerLock.InReadLock(() =>
+ IContentManager? contentManager = this.ContentManagerLock.InReadLock(() =>
this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID)
);
if (contentManager == null)
@@ -461,6 +461,7 @@ namespace StardewModdingAPI.Framework
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The asset info to load or edit.</param>
public IEnumerable<AssetOperationGroup> GetAssetOperations<T>(IAssetInfo info)
+ where T : notnull
{
return this.AssetOperationsByKey.GetOrSet(
info.Name,
@@ -491,7 +492,7 @@ namespace StardewModdingAPI.Framework
{
rootPath = PathUtilities.NormalizePath(rootPath);
- if (!this.CaseInsensitivePathCaches.TryGetValue(rootPath, out CaseInsensitivePathCache cache))
+ if (!this.CaseInsensitivePathCaches.TryGetValue(rootPath, out CaseInsensitivePathCache? cache))
this.CaseInsensitivePathCaches[rootPath] = cache = new CaseInsensitivePathCache(rootPath);
return cache;
@@ -501,9 +502,9 @@ namespace StardewModdingAPI.Framework
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
public TilesheetReference[] GetVanillaTilesheetIds(string assetName)
{
- if (!this.VanillaTilesheets.TryGetValue(assetName, out TilesheetReference[] tilesheets))
+ if (!this.VanillaTilesheets.TryGetValue(assetName, out TilesheetReference[]? tilesheets))
{
- tilesheets = this.TryLoadVanillaAsset(assetName, out Map map)
+ tilesheets = this.TryLoadVanillaAsset(assetName, out Map? map)
? map.TileSheets.Select((sheet, index) => new TilesheetReference(index, sheet.Id, sheet.ImageSource, sheet.SheetSize, sheet.TileSize)).ToArray()
: null;
@@ -516,7 +517,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Get the locale code which corresponds to a language enum (e.g. <c>fr-FR</c> given <see cref="LocalizedContentManager.LanguageCode.fr"/>).</summary>
/// <param name="language">The language enum to search.</param>
- public string GetLocaleCode(LocalizedContentManager.LanguageCode language)
+ public string? GetLocaleCode(LocalizedContentManager.LanguageCode language)
{
if (language == LocalizedContentManager.LanguageCode.mod && LocalizedContentManager.CurrentModLanguage == null)
return null;
@@ -535,7 +536,7 @@ namespace StardewModdingAPI.Framework
foreach (IContentManager contentManager in this.ContentManagers)
contentManager.Dispose();
this.ContentManagers.Clear();
- this.MainContentManager = null;
+ this.MainContentManager = null!; // instance no longer usable
this.ContentManagerLock.Dispose();
}
@@ -560,7 +561,8 @@ namespace StardewModdingAPI.Framework
/// <typeparam name="T">The type of asset to load.</typeparam>
/// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
/// <param name="asset">The loaded asset data.</param>
- private bool TryLoadVanillaAsset<T>(string assetName, out T asset)
+ private bool TryLoadVanillaAsset<T>(string assetName, [NotNullWhen(true)] out T? asset)
+ where T : notnull
{
try
{
@@ -576,12 +578,12 @@ namespace StardewModdingAPI.Framework
/// <summary>Get the language enums (like <see cref="LocalizedContentManager.LanguageCode.ja"/>) indexed by locale code (like <c>ja-JP</c>).</summary>
/// <param name="customLanguages">The custom languages to add to the lookup.</param>
- private Dictionary<string, LocalizedContentManager.LanguageCode> GetLocaleCodes(IEnumerable<ModLanguage> customLanguages)
+ private Dictionary<string, LocalizedContentManager.LanguageCode> GetLocaleCodes(IEnumerable<ModLanguage?> customLanguages)
{
var map = new Dictionary<string, LocalizedContentManager.LanguageCode>(StringComparer.OrdinalIgnoreCase);
// custom languages
- foreach (ModLanguage language in customLanguages)
+ foreach (ModLanguage? language in customLanguages)
{
if (!string.IsNullOrWhiteSpace(language?.LanguageCode))
map[language.LanguageCode] = LocalizedContentManager.LanguageCode.mod;
@@ -590,7 +592,7 @@ namespace StardewModdingAPI.Framework
// vanilla languages (override custom language if they conflict)
foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode)))
{
- string locale = this.GetLocaleCode(code);
+ string? locale = this.GetLocaleCode(code);
if (locale != null)
map[locale] = code;
}
@@ -602,6 +604,7 @@ namespace StardewModdingAPI.Framework
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The asset info to load or edit.</param>
private IEnumerable<AssetOperationGroup> GetAssetOperationsWithoutCache<T>(IAssetInfo info)
+ where T : notnull
{
IAssetInfo legacyInfo = this.GetLegacyAssetInfo(info);
@@ -727,7 +730,8 @@ namespace StardewModdingAPI.Framework
locale: null,
assetName: legacyName,
data: asset.Data,
- getNormalizedPath: this.MainContentManager.AssertAndNormalizeAssetName
+ getNormalizedPath: this.MainContentManager.AssertAndNormalizeAssetName,
+ reflection: this.Reflection
);
}
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index 4594d235..b2e3ec0f 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;
@@ -32,6 +30,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Encapsulates monitoring and logging.</summary>
protected readonly IMonitor Monitor;
+ /// <summary>Simplifies access to private code.</summary>
+ protected readonly Reflector Reflection;
+
/// <summary>Whether to enable more aggressive memory optimizations.</summary>
protected readonly bool AggressiveMemoryOptimizations;
@@ -88,18 +89,22 @@ namespace StardewModdingAPI.Framework.ContentManagers
// init
this.Name = name;
this.Coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator));
- this.Cache = new ContentCache(this, reflection);
+ // ReSharper disable once VirtualMemberCallInConstructor -- LoadedAssets isn't overridden by SMAPI or Stardew Valley
+ this.Cache = new ContentCache(this.LoadedAssets);
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
+ this.Reflection = reflection;
this.OnDisposing = onDisposing;
this.IsNamespaced = isNamespaced;
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);
}
@@ -127,6 +132,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)
@@ -168,11 +174,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.
@@ -249,7 +256,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
{
@@ -281,7 +288,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();
@@ -297,7 +305,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);
}
@@ -319,6 +328,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/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index f4e1bda4..6469fea4 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -70,7 +69,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
return true;
// managed asset
- if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string contentManagerID, out IAssetName relativePath))
+ if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath))
return this.Coordinator.DoesManagedAssetExist<T>(contentManagerID, relativePath);
// custom asset from a loader
@@ -78,7 +77,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName);
AssetLoadOperation[] loaders = this.GetLoaders<object>(info).ToArray();
- if (!this.AssertMaxOneRequiredLoader(info, loaders, out string error))
+ if (!this.AssertMaxOneRequiredLoader(info, loaders, out string? error))
{
this.Monitor.Log(error, LogLevel.Warn);
return false;
@@ -102,7 +101,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
return this.RawLoad<T>(assetName, useCache: true);
// get managed asset
- if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string contentManagerID, out IAssetName relativePath))
+ if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath))
{
T managedAsset = this.Coordinator.LoadManagedAsset<T>(contentManagerID, relativePath);
this.TrackAsset(assetName, managedAsset, useCache);
@@ -124,7 +123,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
IAssetInfo info = new AssetInfo(assetName.LocaleCode, assetName, typeof(T), this.AssertAndNormalizeAssetName);
IAssetData asset =
this.ApplyLoader<T>(info)
- ?? new AssetDataForObject(info, this.RawLoad<T>(assetName, useCache), this.AssertAndNormalizeAssetName);
+ ?? new AssetDataForObject(info, this.RawLoad<T>(assetName, useCache), this.AssertAndNormalizeAssetName, this.Reflection);
asset = this.ApplyEditors<T>(info, asset);
return (T)asset.Data;
});
@@ -151,14 +150,15 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Load the initial asset from the registered loaders.</summary>
/// <param name="info">The basic asset metadata.</param>
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
- private IAssetData ApplyLoader<T>(IAssetInfo info)
+ private IAssetData? ApplyLoader<T>(IAssetInfo info)
+ where T : notnull
{
// find matching loader
- AssetLoadOperation loader;
+ AssetLoadOperation? loader;
{
AssetLoadOperation[] loaders = this.GetLoaders<T>(info).OrderByDescending(p => p.Priority).ToArray();
- if (!this.AssertMaxOneRequiredLoader(info, loaders, out string error))
+ if (!this.AssertMaxOneRequiredLoader(info, loaders, out string? error))
{
this.Monitor.Log(error, LogLevel.Warn);
return null;
@@ -187,7 +187,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// return matched asset
return this.TryFixAndValidateLoadedAsset(info, data, loader)
- ? new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName)
+ ? new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName, this.Reflection)
: null;
}
@@ -196,20 +196,21 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="info">The basic asset metadata.</param>
/// <param name="asset">The loaded asset.</param>
private IAssetData ApplyEditors<T>(IAssetInfo info, IAssetData asset)
+ where T : notnull
{
- IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName);
+ IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName, this.Reflection);
// special case: if the asset was loaded with a more general type like 'object', call editors with the actual type instead.
{
Type actualType = asset.Data.GetType();
- Type actualOpenType = actualType.IsGenericType ? actualType.GetGenericTypeDefinition() : null;
+ Type? actualOpenType = actualType.IsGenericType ? actualType.GetGenericTypeDefinition() : null;
if (typeof(T) != actualType && (actualOpenType == typeof(Dictionary<,>) || actualOpenType == typeof(List<>) || actualType == typeof(Texture2D) || actualType == typeof(Map)))
{
return (IAssetData)this.GetType()
.GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance)!
.MakeGenericMethod(actualType)
- .Invoke(this, new object[] { info, asset });
+ .Invoke(this, new object[] { info, asset })!;
}
}
@@ -232,6 +233,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
// validate edit
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- it's only guaranteed non-null after this method
if (asset.Data == null)
{
mod.LogAsMod($"Mod incorrectly set asset '{info.Name}'{this.GetOnBehalfOfLabel(editor.OnBehalfOf)} to a null value; ignoring override.", LogLevel.Warn);
@@ -252,6 +254,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The basic asset metadata.</param>
private IEnumerable<AssetLoadOperation> GetLoaders<T>(IAssetInfo info)
+ where T : notnull
{
return this.Coordinator
.GetAssetOperations<T>(info)
@@ -262,6 +265,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The basic asset metadata.</param>
private IEnumerable<AssetEditOperation> GetEditors<T>(IAssetInfo info)
+ where T : notnull
{
return this.Coordinator
.GetAssetOperations<T>(info)
@@ -273,7 +277,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="loaders">The asset loaders to apply.</param>
/// <param name="error">The error message to show to the user, if the method returns false.</param>
/// <returns>Returns true if only one loader will apply, else false.</returns>
- private bool AssertMaxOneRequiredLoader(IAssetInfo info, AssetLoadOperation[] loaders, out string error)
+ private bool AssertMaxOneRequiredLoader(IAssetInfo info, AssetLoadOperation[] loaders, [NotNullWhen(false)] out string? error)
{
AssetLoadOperation[] required = loaders.Where(p => p.Priority == AssetLoadPriority.Exclusive).ToArray();
if (required.Length <= 1)
@@ -299,7 +303,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="onBehalfOf">The content pack on whose behalf the action is being performed.</param>
/// <param name="parenthetical">whether to format the label as a parenthetical shown after the mod name like <c> (for the 'X' content pack)</c>, instead of a standalone label like <c>the 'X' content pack</c>.</param>
/// <returns>Returns the on-behalf-of label if applicable, else <c>null</c>.</returns>
- private string GetOnBehalfOfLabel(IModMetadata onBehalfOf, bool parenthetical = true)
+ [return: NotNullIfNotNull("onBehalfOf")]
+ private string? GetOnBehalfOfLabel(IModMetadata? onBehalfOf, bool parenthetical = true)
{
if (onBehalfOf == null)
return null;
@@ -315,7 +320,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="data">The loaded asset data.</param>
/// <param name="loader">The loader which loaded the asset.</param>
/// <returns>Returns whether the asset passed validation checks (after any fixes were applied).</returns>
- private bool TryFixAndValidateLoadedAsset<T>(IAssetInfo info, T data, AssetLoadOperation loader)
+ private bool TryFixAndValidateLoadedAsset<T>(IAssetInfo info, [NotNullWhen(true)] T? data, AssetLoadOperation loader)
+ where T : notnull
{
IModMetadata mod = loader.Mod;
@@ -335,7 +341,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// add missing tilesheet
if (loadedMap.GetTileSheet(vanillaSheet.Id) == null)
{
- mod.Monitor.LogOnce("SMAPI fixed maps loaded by this mod to prevent errors. See the log file for details.", LogLevel.Warn);
+ mod.Monitor!.LogOnce("SMAPI fixed maps loaded by this mod to prevent errors. See the log file for details.", LogLevel.Warn);
this.Monitor.Log($"Fixed broken map replacement: {mod.DisplayName} loaded '{info.Name}' without a required tilesheet (id: {vanillaSheet.Id}, source: {vanillaSheet.ImageSource}).");
loadedMap.AddTileSheet(new TileSheet(vanillaSheet.Id, loadedMap, vanillaSheet.ImageSource, vanillaSheet.SheetSize, vanillaSheet.TileSize));
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs
index 46d5d24e..1b0e1016 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Globalization;
using Microsoft.Xna.Framework.Graphics;
@@ -39,7 +37,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Get whether a texture was loaded by this content manager.</summary>
/// <param name="texture">The texture to check.</param>
- public bool IsResponsibleFor(Texture2D texture)
+ public bool IsResponsibleFor(Texture2D? texture)
{
return
texture?.Tag is string tag
diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
index c8b2ae64..ac67cad5 100644
--- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Content;
@@ -33,25 +31,28 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Get whether an asset exists and can be loaded.</summary>
/// <typeparam name="T">The expected asset type.</typeparam>
/// <param name="assetName">The normalized asset name.</param>
- bool DoesAssetExist<T>(IAssetName assetName);
+ bool DoesAssetExist<T>(IAssetName assetName)
+ where T: notnull;
/// <summary>Load an asset through the content pipeline, using a localized variant of the <paramref name="assetName"/> if available.</summary>
/// <typeparam name="T">The type of asset to load.</typeparam>
/// <param name="assetName">The asset name relative to the loader root directory.</param>
/// <param name="language">The language for which to load the asset.</param>
/// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param>
- T LoadLocalized<T>(IAssetName assetName, LocalizedContentManager.LanguageCode language, bool useCache);
+ T LoadLocalized<T>(IAssetName assetName, LocalizedContentManager.LanguageCode language, bool useCache)
+ where T : notnull;
/// <summary>Load an asset through the content pipeline, using the exact asset name without checking for localized variants.</summary>
/// <typeparam name="T">The type of asset to load.</typeparam>
/// <param name="assetName">The asset name relative to the loader root directory.</param>
/// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param>
- T LoadExact<T>(IAssetName assetName, bool useCache);
+ T LoadExact<T>(IAssetName assetName, bool useCache)
+ where T : notnull;
/// <summary>Assert that the given key has a valid format and return a normalized form consistent with the underlying cache.</summary>
/// <param name="assetName">The asset key to check.</param>
/// <exception cref="SContentLoadException">The asset key is empty or contains invalid characters.</exception>
- string AssertAndNormalizeAssetName(string assetName);
+ string AssertAndNormalizeAssetName(string? assetName);
/// <summary>Get the current content locale.</summary>
string GetLocale();
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/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs
index 2d33a22e..2cfd5cce 100644
--- a/src/SMAPI/Framework/ContentPack.cs
+++ b/src/SMAPI/Framework/ContentPack.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.IO;
using StardewModdingAPI.Framework.ModHelpers;
@@ -69,12 +67,12 @@ namespace StardewModdingAPI.Framework
}
/// <inheritdoc />
- public TModel ReadJsonFile<TModel>(string path) where TModel : class
+ public TModel? ReadJsonFile<TModel>(string path) where TModel : class
{
path = PathUtilities.NormalizePath(path);
FileInfo file = this.GetFile(path);
- return file.Exists && this.JsonHelper.ReadJsonFileIfExists(file.FullName, out TModel model)
+ return file.Exists && this.JsonHelper.ReadJsonFileIfExists(file.FullName, out TModel? model)
? model
: null;
}
@@ -93,6 +91,7 @@ namespace StardewModdingAPI.Framework
/// <inheritdoc />
[Obsolete]
public T LoadAsset<T>(string key)
+ where T : notnull
{
return this.ModContent.Load<T>(key);
}
@@ -101,7 +100,7 @@ namespace StardewModdingAPI.Framework
[Obsolete]
public string GetActualAssetKey(string key)
{
- return this.ModContent.GetInternalAssetName(key)?.Name;
+ return this.ModContent.GetInternalAssetName(key).Name;
}
diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs
index 8f36a554..24084830 100644
--- a/src/SMAPI/Framework/CursorPosition.cs
+++ b/src/SMAPI/Framework/CursorPosition.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Microsoft.Xna.Framework;
using StardewValley;
@@ -41,7 +39,7 @@ namespace StardewModdingAPI.Framework
}
/// <inheritdoc />
- public bool Equals(ICursorPosition other)
+ public bool Equals(ICursorPosition? other)
{
return other != null && this.AbsolutePixels == other.AbsolutePixels;
}
diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs
index fe1b623f..37a5c8ef 100644
--- a/src/SMAPI/Framework/DeprecationManager.cs
+++ b/src/SMAPI/Framework/DeprecationManager.cs
@@ -1,8 +1,8 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using System.Text;
namespace StardewModdingAPI.Framework
{
@@ -37,32 +37,34 @@ namespace StardewModdingAPI.Framework
this.ModRegistry = modRegistry;
}
- /// <summary>Get the source name for a mod from its unique ID.</summary>
- public string GetSourceNameFromStack()
+ /// <summary>Get a mod for the closest assembly registered as a source of deprecation warnings.</summary>
+ /// <returns>Returns the source name, or <c>null</c> if no registered assemblies were found.</returns>
+ public IModMetadata? GetModFromStack()
{
- return this.ModRegistry.GetFromStack()?.DisplayName;
+ return this.ModRegistry.GetFromStack();
}
- /// <summary>Get the source name for a mod from its unique ID.</summary>
+ /// <summary>Get a mod from its unique ID.</summary>
/// <param name="modId">The mod's unique ID.</param>
- public string GetSourceName(string modId)
+ public IModMetadata? GetMod(string modId)
{
- return this.ModRegistry.Get(modId)?.DisplayName;
+ return this.ModRegistry.Get(modId);
}
/// <summary>Log a deprecation warning.</summary>
- /// <param name="source">The friendly mod name which used the deprecated code.</param>
+ /// <param name="source">The mod which used the deprecated code, if known.</param>
/// <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(IModMetadata? source, string nounPhrase, string version, DeprecationLevel severity)
{
// ignore if already warned
- if (!this.MarkWarned(source ?? this.GetSourceNameFromStack() ?? "<unknown>", nounPhrase, version))
+ if (!this.MarkWarned(source, nounPhrase, version))
return;
// queue warning
- this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, Environment.StackTrace));
+ var stack = new StackTrace(skipFrames: 1); // skip this method
+ this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, stack));
}
/// <summary>A placeholder method used to track deprecated code for which a separate warning will be shown.</summary>
@@ -99,17 +101,12 @@ namespace StardewModdingAPI.Framework
}
// log message
- if (warning.ModName != null)
- this.Monitor.Log(message, level);
+ if (level == LogLevel.Trace)
+ this.Monitor.Log($"{message}\n{this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod)}", 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(this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod), LogLevel.Debug);
}
}
@@ -121,20 +118,54 @@ namespace StardewModdingAPI.Framework
** Private methods
*********/
/// <summary>Mark a deprecation warning as already logged.</summary>
- /// <param name="source">The friendly name of the assembly which used the deprecated code.</param>
+ /// <param name="source">The mod which used the deprecated code.</param>
/// <param name="nounPhrase">A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method").</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
/// <returns>Returns whether the deprecation was successfully marked as warned. Returns <c>false</c> if it was already marked.</returns>
- private bool MarkWarned(string source, string nounPhrase, string version)
+ private bool MarkWarned(IModMetadata? source, string nounPhrase, string version)
{
- if (string.IsNullOrWhiteSpace(source))
- throw new InvalidOperationException("The deprecation source cannot be empty.");
-
- string key = $"{source}::{nounPhrase}::{version}";
+ string key = $"{source?.DisplayName ?? "<unknown>"}::{nounPhrase}::{version}";
if (this.LoggedDeprecations.Contains(key))
return false;
this.LoggedDeprecations.Add(key);
return true;
}
+
+ /// <summary>Get the simplest stack trace which shows where in the mod the deprecated code was called from.</summary>
+ /// <param name="stack">The stack trace.</param>
+ /// <param name="mod">The mod for which to show a stack trace.</param>
+ private string GetSimplifiedStackTrace(StackTrace stack, IModMetadata? mod)
+ {
+ // unknown mod, show entire stack trace
+ if (mod == null)
+ return stack.ToString();
+
+ // get frame info
+ var frames = stack
+ .GetFrames()
+ .Select(frame => (Frame: frame, Mod: this.ModRegistry.GetFrom(frame)))
+ .ToArray();
+ var modIds = new HashSet<string>(
+ from frame in frames
+ let id = frame.Mod?.Manifest.UniqueID
+ where id != null
+ select id
+ );
+
+ // can't filter to the target mod
+ if (modIds.Count != 1 || !modIds.Contains(mod.Manifest.UniqueID))
+ return stack.ToString();
+
+ // get stack frames for the target mod, plus one for context
+ var framesStartingAtMod = frames.SkipWhile(p => p.Mod == null).ToArray();
+ var displayFrames = framesStartingAtMod.TakeWhile(p => p.Mod != null).ToArray();
+ displayFrames = displayFrames.Concat(framesStartingAtMod.Skip(displayFrames.Length).Take(1)).ToArray();
+
+ // build stack trace
+ StringBuilder str = new();
+ foreach (var frame in displayFrames)
+ str.Append(new StackTrace(frame.Frame));
+ return str.ToString().TrimEnd();
+ }
}
}
diff --git a/src/SMAPI/Framework/DeprecationWarning.cs b/src/SMAPI/Framework/DeprecationWarning.cs
index f155358b..1e83f679 100644
--- a/src/SMAPI/Framework/DeprecationWarning.cs
+++ b/src/SMAPI/Framework/DeprecationWarning.cs
@@ -1,4 +1,4 @@
-#nullable disable
+using System.Diagnostics;
namespace StardewModdingAPI.Framework
{
@@ -8,8 +8,11 @@ namespace StardewModdingAPI.Framework
/*********
** Accessors
*********/
- /// <summary>The affected mod's display name.</summary>
- public string ModName { get; }
+ /// <summary>The affected mod.</summary>
+ public IModMetadata? Mod { get; }
+
+ /// <summary>Get the display name for the affected mod.</summary>
+ public string ModName => this.Mod?.DisplayName ?? "<unknown mod>";
/// <summary>A noun phrase describing what is deprecated.</summary>
public string NounPhrase { get; }
@@ -21,21 +24,21 @@ namespace StardewModdingAPI.Framework
public DeprecationLevel Level { get; }
/// <summary>The stack trace when the deprecation warning was raised.</summary>
- public string StackTrace { get; }
+ public StackTrace StackTrace { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="modName">The affected mod's display name.</param>
+ /// <param name="mod">The affected mod.</param>
/// <param name="nounPhrase">A noun phrase describing what is deprecated.</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
/// <param name="level">The deprecation level for the affected code.</param>
/// <param name="stackTrace">The stack trace when the deprecation warning was raised.</param>
- public DeprecationWarning(string modName, string nounPhrase, string version, DeprecationLevel level, string stackTrace)
+ public DeprecationWarning(IModMetadata? mod, string nounPhrase, string version, DeprecationLevel level, StackTrace stackTrace)
{
- this.ModName = modName;
+ this.Mod = mod;
this.NounPhrase = nounPhrase;
this.Version = version;
this.Level = level;
diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs
index c977e73d..41540047 100644
--- a/src/SMAPI/Framework/Events/EventManager.cs
+++ b/src/SMAPI/Framework/Events/EventManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using StardewModdingAPI.Events;
namespace StardewModdingAPI.Framework.Events
diff --git a/src/SMAPI/Framework/Events/IManagedEvent.cs b/src/SMAPI/Framework/Events/IManagedEvent.cs
index 57277576..e4e3ca08 100644
--- a/src/SMAPI/Framework/Events/IManagedEvent.cs
+++ b/src/SMAPI/Framework/Events/IManagedEvent.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Framework.Events
{
/// <summary>Metadata for an event raised by SMAPI.</summary>
diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs
index 8fa31165..4b8a770d 100644
--- a/src/SMAPI/Framework/Events/ManagedEvent.cs
+++ b/src/SMAPI/Framework/Events/ManagedEvent.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -23,7 +21,7 @@ namespace StardewModdingAPI.Framework.Events
private readonly List<ManagedEventHandler<TEventArgs>> Handlers = new();
/// <summary>A cached snapshot of <see cref="Handlers"/>, or <c>null</c> to rebuild it next raise.</summary>
- private ManagedEventHandler<TEventArgs>[] CachedHandlers = Array.Empty<ManagedEventHandler<TEventArgs>>();
+ private ManagedEventHandler<TEventArgs>[]? CachedHandlers = Array.Empty<ManagedEventHandler<TEventArgs>>();
/// <summary>The total number of event handlers registered for this events, regardless of whether they're still registered.</summary>
private int RegistrationIndex;
@@ -100,7 +98,7 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>Raise the event and notify all handlers.</summary>
/// <param name="args">The event arguments to pass.</param>
/// <param name="match">A lambda which returns true if the event should be raised for the given mod.</param>
- public void Raise(TEventArgs args, Func<IModMetadata, bool> match = null)
+ public void Raise(TEventArgs args, Func<IModMetadata, bool>? match = null)
{
this.Raise((_, invoke) => invoke(args), match);
}
@@ -108,7 +106,7 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>Raise the event and notify all handlers.</summary>
/// <param name="invoke">Invoke an event handler. This receives the mod which registered the handler, and should invoke the callback with the event arguments to pass it.</param>
/// <param name="match">A lambda which returns true if the event should be raised for the given mod.</param>
- public void Raise(Action<IModMetadata, Action<TEventArgs>> invoke, Func<IModMetadata, bool> match = null)
+ public void Raise(Action<IModMetadata, Action<TEventArgs>> invoke, Func<IModMetadata, bool>? match = null)
{
// skip if no handlers
if (this.Handlers.Count == 0)
diff --git a/src/SMAPI/Framework/Events/ManagedEventHandler.cs b/src/SMAPI/Framework/Events/ManagedEventHandler.cs
index f31bc04d..d32acdb9 100644
--- a/src/SMAPI/Framework/Events/ManagedEventHandler.cs
+++ b/src/SMAPI/Framework/Events/ManagedEventHandler.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
@@ -42,7 +40,7 @@ namespace StardewModdingAPI.Framework.Events
}
/// <inheritdoc />
- public int CompareTo(object obj)
+ public int CompareTo(object? obj)
{
if (obj is not ManagedEventHandler<TEventArgs> other)
throw new ArgumentException("Can't compare to an unrelated object type.");
diff --git a/src/SMAPI/Framework/Events/ModContentEvents.cs b/src/SMAPI/Framework/Events/ModContentEvents.cs
index f198b793..beb96031 100644
--- a/src/SMAPI/Framework/Events/ModContentEvents.cs
+++ b/src/SMAPI/Framework/Events/ModContentEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/Framework/Events/ModDisplayEvents.cs b/src/SMAPI/Framework/Events/ModDisplayEvents.cs
index b2110cce..48f55324 100644
--- a/src/SMAPI/Framework/Events/ModDisplayEvents.cs
+++ b/src/SMAPI/Framework/Events/ModDisplayEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs
index e8f8885d..1fb3482c 100644
--- a/src/SMAPI/Framework/Events/ModEvents.cs
+++ b/src/SMAPI/Framework/Events/ModEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using StardewModdingAPI.Events;
namespace StardewModdingAPI.Framework.Events
diff --git a/src/SMAPI/Framework/Events/ModEventsBase.cs b/src/SMAPI/Framework/Events/ModEventsBase.cs
index 295caa0d..77708fc1 100644
--- a/src/SMAPI/Framework/Events/ModEventsBase.cs
+++ b/src/SMAPI/Framework/Events/ModEventsBase.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Framework.Events
{
/// <summary>An internal base class for event API classes.</summary>
diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs
index 51803daf..5f0db369 100644
--- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs
+++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs
index 6af79c59..40edf806 100644
--- a/src/SMAPI/Framework/Events/ModInputEvents.cs
+++ b/src/SMAPI/Framework/Events/ModInputEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs
index 7d3ce510..b90f64fa 100644
--- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs
+++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/Framework/Events/ModPlayerEvents.cs b/src/SMAPI/Framework/Events/ModPlayerEvents.cs
index dac8f05b..b2d89e9a 100644
--- a/src/SMAPI/Framework/Events/ModPlayerEvents.cs
+++ b/src/SMAPI/Framework/Events/ModPlayerEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs
index 4b438034..7980208b 100644
--- a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs
+++ b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs
index 614945c7..a7b7d799 100644
--- a/src/SMAPI/Framework/Events/ModWorldEvents.cs
+++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs b/src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs
index 4e03a687..ec9279f1 100644
--- a/src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs
+++ b/src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Framework.Exceptions
diff --git a/src/SMAPI/Framework/Exceptions/SContentLoadException.cs b/src/SMAPI/Framework/Exceptions/SContentLoadException.cs
index c21a6b0e..be1fe748 100644
--- a/src/SMAPI/Framework/Exceptions/SContentLoadException.cs
+++ b/src/SMAPI/Framework/Exceptions/SContentLoadException.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Content;
@@ -14,7 +12,7 @@ namespace StardewModdingAPI.Framework.Exceptions
/// <summary>Construct an instance.</summary>
/// <param name="message">The error message.</param>
/// <param name="ex">The underlying exception, if any.</param>
- public SContentLoadException(string message, Exception ex = null)
+ public SContentLoadException(string message, Exception? ex = null)
: base(message, ex) { }
}
}
diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs
index aa91d8f3..542c1345 100644
--- a/src/SMAPI/Framework/GameVersion.cs
+++ b/src/SMAPI/Framework/GameVersion.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
@@ -55,7 +53,7 @@ namespace StardewModdingAPI.Framework
private static string GetSemanticVersionString(string gameVersion)
{
// mapped version
- return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion)
+ return GameVersion.VersionMap.TryGetValue(gameVersion, out string? semanticVersion)
? semanticVersion
: gameVersion;
}
@@ -64,10 +62,10 @@ namespace StardewModdingAPI.Framework
/// <param name="semanticVersion">The semantic version string.</param>
private static string GetGameVersionString(string semanticVersion)
{
- foreach (var mapping in GameVersion.VersionMap)
+ foreach ((string gameVersion, string equivalentSemanticVersion) in GameVersion.VersionMap)
{
- if (mapping.Value.Equals(semanticVersion, StringComparison.OrdinalIgnoreCase))
- return mapping.Key;
+ if (equivalentSemanticVersion.Equals(semanticVersion, StringComparison.OrdinalIgnoreCase))
+ return gameVersion;
}
return semanticVersion;
diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs
index 800b198a..7cee20b9 100644
--- a/src/SMAPI/Framework/IModMetadata.cs
+++ b/src/SMAPI/Framework/IModMetadata.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using StardewModdingAPI.Framework.ModHelpers;
@@ -29,7 +27,7 @@ namespace StardewModdingAPI.Framework
string RelativeDirectoryPath { get; }
/// <summary>Metadata about the mod from SMAPI's internal data (if any).</summary>
- ModDataRecordVersionedFields DataRecord { get; }
+ ModDataRecordVersionedFields? DataRecord { get; }
/// <summary>The metadata resolution status.</summary>
ModMetadataStatus Status { get; }
@@ -41,31 +39,31 @@ namespace StardewModdingAPI.Framework
ModWarning Warnings { get; }
/// <summary>The reason the metadata is invalid, if any.</summary>
- string Error { get; }
+ string? Error { get; }
/// <summary>A detailed technical message for <see cref="Error"/>, if any.</summary>
- public string ErrorDetails { get; }
+ string? ErrorDetails { get; }
/// <summary>Whether the mod folder should be ignored. This is <c>true</c> if it was found within a folder whose name starts with a dot.</summary>
bool IsIgnored { get; }
/// <summary>The mod instance (if loaded and <see cref="IModInfo.IsContentPack"/> is false).</summary>
- IMod Mod { get; }
+ IMod? Mod { get; }
/// <summary>The content pack instance (if loaded and <see cref="IModInfo.IsContentPack"/> is true).</summary>
- IContentPack ContentPack { get; }
+ IContentPack? ContentPack { get; }
/// <summary>The translations for this mod (if loaded).</summary>
- TranslationHelper Translations { get; }
+ TranslationHelper? Translations { get; }
/// <summary>Writes messages to the console and log file as this mod.</summary>
- IMonitor Monitor { get; }
+ IMonitor? Monitor { get; }
/// <summary>The mod-provided API (if any).</summary>
- object Api { get; }
+ object? Api { get; }
/// <summary>The update-check metadata for this mod (if any).</summary>
- ModEntryModel UpdateCheckData { get; }
+ ModEntryModel? UpdateCheckData { get; }
/// <summary>The fake content packs created by this mod, if any.</summary>
ISet<WeakReference<ContentPack>> FakeContentPacks { get; }
@@ -84,7 +82,7 @@ namespace StardewModdingAPI.Framework
/// <param name="error">The reason the metadata is invalid, if any.</param>
/// <param name="errorDetails">A detailed technical message, if any.</param>
/// <returns>Return the instance for chaining.</returns>
- IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string error, string errorDetails = null);
+ IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string? error, string? errorDetails = null);
/// <summary>Set a warning flag for the mod.</summary>
/// <param name="warning">The warning to set.</param>
@@ -103,7 +101,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Set the mod-provided API instance.</summary>
/// <param name="api">The mod-provided API.</param>
- IModMetadata SetApi(object api);
+ IModMetadata SetApi(object? api);
/// <summary>Set the update-check metadata for this mod.</summary>
/// <param name="data">The update-check metadata.</param>
@@ -117,7 +115,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether the mod has the given ID.</summary>
/// <param name="id">The mod ID to check.</param>
- bool HasID(string id);
+ bool HasID(string? id);
/// <summary>Get the defined update keys.</summary>
/// <param name="validOnly">Only return valid update keys.</param>
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/IInputStateBuilder.cs b/src/SMAPI/Framework/Input/IInputStateBuilder.cs
index 3fb62686..28d62439 100644
--- a/src/SMAPI/Framework/Input/IInputStateBuilder.cs
+++ b/src/SMAPI/Framework/Input/IInputStateBuilder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
namespace StardewModdingAPI.Framework.Input
diff --git a/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs b/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs
index 81ca0ebb..f66fbd07 100644
--- a/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs
+++ b/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework.Input;
@@ -29,7 +27,7 @@ namespace StardewModdingAPI.Framework.Input
this.State = state;
this.PressedButtons.Clear();
- foreach (var button in state.GetPressedKeys())
+ foreach (Keys button in state.GetPressedKeys())
this.PressedButtons.Add(button);
}
diff --git a/src/SMAPI/Framework/Input/MouseStateBuilder.cs b/src/SMAPI/Framework/Input/MouseStateBuilder.cs
index 85b38d32..c2a0891b 100644
--- a/src/SMAPI/Framework/Input/MouseStateBuilder.cs
+++ b/src/SMAPI/Framework/Input/MouseStateBuilder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using Microsoft.Xna.Framework.Input;
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..580651f3 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);
}
/****
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/LogFileManager.cs b/src/SMAPI/Framework/Logging/LogFileManager.cs
index 0b6f9ad2..b396091a 100644
--- a/src/SMAPI/Framework/Logging/LogFileManager.cs
+++ b/src/SMAPI/Framework/Logging/LogFileManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.IO;
@@ -32,7 +30,7 @@ namespace StardewModdingAPI.Framework.Logging
this.Path = path;
// create log directory if needed
- string logDir = System.IO.Path.GetDirectoryName(path);
+ string? logDir = System.IO.Path.GetDirectoryName(path);
if (logDir == null)
throw new ArgumentException($"The log path '{path}' is not valid.");
Directory.CreateDirectory(logDir);
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index dab7f554..b94807b5 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,23 +93,26 @@ 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 construction logic
+ // init log file
+ this.LogFile = new LogFileManager(logPath);
+
+ // init monitor
this.GetMonitorImpl = name => new Monitor(name, LogManager.IgnoreChar, this.LogFile, colorConfig, isVerbose, getScreenIdForLog)
{
WriteToConsole = writeToConsole,
ShowTraceInConsole = isDeveloperMode,
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/BaseHelper.cs b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs
index 1cd1a6b3..12390976 100644
--- a/src/SMAPI/Framework/ModHelpers/BaseHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs
@@ -1,25 +1,30 @@
-#nullable disable
-
namespace StardewModdingAPI.Framework.ModHelpers
{
/// <summary>The common base class for mod helpers.</summary>
internal abstract class BaseHelper : IModLinked
{
/*********
+ ** Fields
+ *********/
+ /// <summary>The mod using this instance.</summary>
+ protected readonly IModMetadata Mod;
+
+
+ /*********
** Accessors
*********/
/// <inheritdoc />
- public string ModID { get; }
+ public string ModID => this.Mod.Manifest.UniqueID;
/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="modID">The unique ID of the relevant mod.</param>
- protected BaseHelper(string modID)
+ /// <param name="mod">The mod using this instance.</param>
+ protected BaseHelper(IModMetadata mod)
{
- this.ModID = modID;
+ this.Mod = mod;
}
}
}
diff --git a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs
index c2b5092e..e430fb1c 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
@@ -10,9 +8,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
/*********
** Fields
*********/
- /// <summary>The mod using this instance.</summary>
- private readonly IModMetadata Mod;
-
/// <summary>Manages console commands.</summary>
private readonly CommandManager CommandManager;
@@ -24,9 +19,8 @@ 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)
{
- this.Mod = mod;
this.CommandManager = commandManager;
}
@@ -42,7 +36,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
public bool Trigger(string name, string[] arguments)
{
SCore.DeprecationManager.Warn(
- source: SCore.DeprecationManager.GetSourceName(this.ModID),
+ source: SCore.DeprecationManager.GetMod(this.ModID),
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}",
version: "3.8.1",
severity: DeprecationLevel.Notice
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index e72e397e..534ac138 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -10,6 +8,7 @@ using System.Linq;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Exceptions;
+using StardewModdingAPI.Framework.Reflection;
using StardewValley;
namespace StardewModdingAPI.Framework.ModHelpers
@@ -30,12 +29,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>A content manager for this mod which manages files from the mod's folder.</summary>
private readonly ModContentManager ModContentManager;
- /// <summary>The friendly mod name for use in errors.</summary>
- private readonly string ModName;
-
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
+ /// <summary>Simplifies access to private code.</summary>
+ private readonly Reflector Reflection;
+
/*********
** Accessors
@@ -58,7 +57,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
get
{
SCore.DeprecationManager.Warn(
- source: this.ModName,
+ source: this.Mod,
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetLoaders)}",
version: "3.14.0",
severity: DeprecationLevel.Notice
@@ -74,7 +73,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
get
{
SCore.DeprecationManager.Warn(
- source: this.ModName,
+ source: this.Mod,
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetEditors)}",
version: "3.14.0",
severity: DeprecationLevel.Notice
@@ -91,23 +90,24 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Construct an instance.</summary>
/// <param name="contentCore">SMAPI's core content logic.</param>
/// <param name="modFolderPath">The absolute path to the mod folder.</param>
- /// <param name="modID">The unique ID of the relevant mod.</param>
- /// <param name="modName">The friendly mod name for use in errors.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor)
- : base(modID)
+ /// <param name="reflection">Simplifies access to private code.</param>
+ public ContentHelper(ContentCoordinator contentCore, string modFolderPath, IModMetadata mod, IMonitor monitor, Reflector reflection)
+ : base(mod)
{
- string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID);
+ string managedAssetPrefix = contentCore.GetManagedAssetPrefix(mod.Manifest.UniqueID);
this.ContentCore = contentCore;
this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content");
- this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, this.GameContentManager);
- this.ModName = modName;
+ this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, this.Mod.DisplayName, modFolderPath, this.GameContentManager);
this.Monitor = monitor;
+ this.Reflection = reflection;
}
/// <inheritdoc />
public T Load<T>(string key, ContentSource source = ContentSource.ModFolder)
+ where T : notnull
{
IAssetName assetName = this.ContentCore.ParseAssetName(key, allowLocales: source == ContentSource.GameContent);
@@ -123,18 +123,18 @@ namespace StardewModdingAPI.Framework.ModHelpers
return this.ModContentManager.LoadExact<T>(assetName, useCache: false);
default:
- throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: unknown content source '{source}'.");
+ throw new SContentLoadException($"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}: unknown content source '{source}'.");
}
}
catch (Exception ex) when (ex is not SContentLoadException)
{
- throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}.", ex);
+ throw new SContentLoadException($"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}.", ex);
}
}
/// <inheritdoc />
[Pure]
- public string NormalizeAssetName(string assetName)
+ public string NormalizeAssetName(string? assetName)
{
return this.ModContentManager.AssertAndNormalizeAssetName(assetName);
}
@@ -165,6 +165,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <inheritdoc />
public bool InvalidateCache<T>()
+ where T : notnull
{
this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.");
return this.ContentCore.InvalidateCache((_, _, type) => typeof(T).IsAssignableFrom(type)).Any();
@@ -178,14 +179,21 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
/// <inheritdoc />
- public IAssetData GetPatchHelper<T>(T data, string assetName = null)
+ public IAssetData GetPatchHelper<T>(T data, string? assetName = null)
+ where T : notnull
{
if (data == null)
throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value.");
assetName ??= $"temp/{Guid.NewGuid():N}";
- return new AssetDataForObject(this.CurrentLocale, this.ContentCore.ParseAssetName(assetName, allowLocales: true/* no way to know if it's a game or mod asset here*/), data, this.NormalizeAssetName);
+ return new AssetDataForObject(
+ locale: this.CurrentLocale,
+ assetName: this.ContentCore.ParseAssetName(assetName, allowLocales: true/* no way to know if it's a game or mod asset here*/),
+ data: data,
+ getNormalizedPath: this.NormalizeAssetName,
+ reflection: this.Reflection
+ );
}
diff --git a/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs
index 336214e2..9f4a7ceb 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -24,11 +22,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="contentPacks">The content packs loaded for this mod.</param>
/// <param name="createContentPack">Create a temporary content pack.</param>
- public ContentPackHelper(string modID, Lazy<IContentPack[]> contentPacks, Func<string, IManifest, IContentPack> createContentPack)
- : base(modID)
+ public ContentPackHelper(IModMetadata mod, Lazy<IContentPack[]> contentPacks, Func<string, IManifest, IContentPack> createContentPack)
+ : base(mod)
{
this.ContentPacks = contentPacks;
this.CreateContentPack = createContentPack;
diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs
index 86a34ee8..2eaa940a 100644
--- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -28,11 +26,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="modFolderPath">The absolute path to the mod folder.</param>
/// <param name="jsonHelper">The absolute path to the mod folder.</param>
- public DataHelper(string modID, string modFolderPath, JsonHelper jsonHelper)
- : base(modID)
+ public DataHelper(IModMetadata mod, string modFolderPath, JsonHelper jsonHelper)
+ : base(mod)
{
this.ModFolderPath = modFolderPath;
this.JsonHelper = jsonHelper;
@@ -42,19 +40,21 @@ namespace StardewModdingAPI.Framework.ModHelpers
** JSON file
****/
/// <inheritdoc />
- public TModel ReadJsonFile<TModel>(string path) where TModel : class
+ public TModel? ReadJsonFile<TModel>(string path)
+ where TModel : class
{
if (!PathUtilities.IsSafeRelativePath(path))
throw new InvalidOperationException($"You must call {nameof(IModHelper.Data)}.{nameof(this.ReadJsonFile)} with a relative path.");
path = Path.Combine(this.ModFolderPath, PathUtilities.NormalizePath(path));
- return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data)
+ return this.JsonHelper.ReadJsonFileIfExists(path, out TModel? data)
? data
: null;
}
/// <inheritdoc />
- public void WriteJsonFile<TModel>(string path, TModel data) where TModel : class
+ public void WriteJsonFile<TModel>(string path, TModel? data)
+ where TModel : class
{
if (!PathUtilities.IsSafeRelativePath(path))
throw new InvalidOperationException($"You must call {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteJsonFile)} with a relative path (without directory climbing).");
@@ -71,7 +71,8 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Save file
****/
/// <inheritdoc />
- public TModel ReadSaveData<TModel>(string key) where TModel : class
+ public TModel? ReadSaveData<TModel>(string key)
+ where TModel : class
{
if (Context.LoadStage == LoadStage.None)
throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} when a save file isn't loaded.");
@@ -82,14 +83,15 @@ namespace StardewModdingAPI.Framework.ModHelpers
string internalKey = this.GetSaveFileKey(key);
foreach (IDictionary<string, string> dataField in this.GetDataFields(Context.LoadStage))
{
- if (dataField.TryGetValue(internalKey, out string value))
+ if (dataField.TryGetValue(internalKey, out string? value))
return this.JsonHelper.Deserialize<TModel>(value);
}
return null;
}
/// <inheritdoc />
- public void WriteSaveData<TModel>(string key, TModel model) where TModel : class
+ public void WriteSaveData<TModel>(string key, TModel? model)
+ where TModel : class
{
if (Context.LoadStage == LoadStage.None)
throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when a save file isn't loaded.");
@@ -97,7 +99,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when connected to a remote host. (Save files are stored on the main player's computer.)");
string internalKey = this.GetSaveFileKey(key);
- string data = model != null
+ string? data = model != null
? this.JsonHelper.Serialize(model, Formatting.None)
: null;
@@ -114,16 +116,18 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Global app data
****/
/// <inheritdoc />
- public TModel ReadGlobalData<TModel>(string key) where TModel : class
+ public TModel? ReadGlobalData<TModel>(string key)
+ where TModel : class
{
string path = this.GetGlobalDataPath(key);
- return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data)
+ return this.JsonHelper.ReadJsonFileIfExists(path, out TModel? data)
? data
: null;
}
/// <inheritdoc />
- public void WriteGlobalData<TModel>(string key, TModel data) where TModel : class
+ public void WriteGlobalData<TModel>(string key, TModel? data)
+ where TModel : class
{
string path = this.GetGlobalDataPath(key);
if (data != null)
diff --git a/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs b/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs
index 956bac7f..232e9287 100644
--- a/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs
@@ -1,10 +1,9 @@
-#nullable disable
-
using System;
using System.Linq;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Exceptions;
+using StardewModdingAPI.Framework.Reflection;
using StardewValley;
namespace StardewModdingAPI.Framework.ModHelpers
@@ -27,6 +26,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
+ /// <summary>Simplifies access to private code.</summary>
+ private readonly Reflector Reflection;
+
/*********
** Accessors
@@ -43,18 +45,20 @@ namespace StardewModdingAPI.Framework.ModHelpers
*********/
/// <summary>Construct an instance.</summary>
/// <param name="contentCore">SMAPI's core content logic.</param>
- /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="modName">The friendly mod name for use in errors.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- public GameContentHelper(ContentCoordinator contentCore, string modID, string modName, IMonitor monitor)
- : base(modID)
+ /// <param name="reflection">Simplifies access to private code.</param>
+ public GameContentHelper(ContentCoordinator contentCore, IModMetadata mod, string modName, IMonitor monitor, Reflector reflection)
+ : base(mod)
{
- string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID);
+ string managedAssetPrefix = contentCore.GetManagedAssetPrefix(mod.Manifest.UniqueID);
this.ContentCore = contentCore;
this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content");
this.ModName = modName;
this.Monitor = monitor;
+ this.Reflection = reflection;
}
/// <inheritdoc />
@@ -65,6 +69,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <inheritdoc />
public T Load<T>(string key)
+ where T : notnull
{
IAssetName assetName = this.ContentCore.ParseAssetName(key, allowLocales: true);
return this.Load<T>(assetName);
@@ -72,6 +77,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <inheritdoc />
public T Load<T>(IAssetName assetName)
+ where T : notnull
{
try
{
@@ -99,6 +105,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <inheritdoc />
public bool InvalidateCache<T>()
+ where T : notnull
{
this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.");
return this.ContentCore.InvalidateCache((_, _, type) => typeof(T).IsAssignableFrom(type)).Any();
@@ -112,14 +119,21 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
/// <inheritdoc />
- public IAssetData GetPatchHelper<T>(T data, string assetName = null)
+ public IAssetData GetPatchHelper<T>(T data, string? assetName = null)
+ where T : notnull
{
if (data == null)
throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value.");
assetName ??= $"temp/{Guid.NewGuid():N}";
- return new AssetDataForObject(this.CurrentLocale, this.ContentCore.ParseAssetName(assetName, allowLocales: true), data, key => this.ParseAssetName(key).Name);
+ return new AssetDataForObject(
+ locale: this.CurrentLocale,
+ assetName: this.ContentCore.ParseAssetName(assetName, allowLocales: true),
+ data: data,
+ getNormalizedPath: key => this.ParseAssetName(key).Name,
+ reflection: this.Reflection
+ );
}
/// <summary>Get the underlying game content manager.</summary>
diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs
index 29f80d87..6c158258 100644
--- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Utilities;
@@ -20,10 +18,10 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="currentInputState">Manages the game's input state for the current player instance. That may not be the main player in split-screen mode.</param>
- public InputHelper(string modID, Func<SInputState> currentInputState)
- : base(modID)
+ public InputHelper(IModMetadata mod, Func<SInputState> currentInputState)
+ : base(mod)
{
this.CurrentInputState = currentInputState;
}
diff --git a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs
index 90064354..4a058a48 100644
--- a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs
@@ -1,10 +1,9 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Content;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Exceptions;
+using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Utilities;
namespace StardewModdingAPI.Framework.ModHelpers
@@ -27,6 +26,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>A case-insensitive lookup of relative paths within the <see cref="ContentManager.RootDirectory"/>.</summary>
private readonly CaseInsensitivePathCache RelativePathCache;
+ /// <summary>Simplifies access to private code.</summary>
+ private readonly Reflector Reflection;
+
/*********
** Public methods
@@ -34,23 +36,26 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Construct an instance.</summary>
/// <param name="contentCore">SMAPI's core content logic.</param>
/// <param name="modFolderPath">The absolute path to the mod folder.</param>
- /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="modName">The friendly mod name for use in errors.</param>
/// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param>
/// <param name="relativePathCache">A case-insensitive lookup of relative paths within the <paramref name="relativePathCache"/>.</param>
- public ModContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IContentManager gameContentManager, CaseInsensitivePathCache relativePathCache)
- : base(modID)
+ /// <param name="reflection">Simplifies access to private code.</param>
+ public ModContentHelper(ContentCoordinator contentCore, string modFolderPath, IModMetadata mod, string modName, IContentManager gameContentManager, CaseInsensitivePathCache relativePathCache, Reflector reflection)
+ : base(mod)
{
- string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID);
+ string managedAssetPrefix = contentCore.GetManagedAssetPrefix(mod.Manifest.UniqueID);
this.ContentCore = contentCore;
this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, gameContentManager);
this.ModName = modName;
this.RelativePathCache = relativePathCache;
+ this.Reflection = reflection;
}
/// <inheritdoc />
public T Load<T>(string relativePath)
+ where T : notnull
{
relativePath = this.RelativePathCache.GetAssetName(relativePath);
@@ -74,7 +79,8 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
/// <inheritdoc />
- public IAssetData GetPatchHelper<T>(T data, string relativePath = null)
+ public IAssetData GetPatchHelper<T>(T data, string? relativePath = null)
+ where T : notnull
{
if (data == null)
throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value.");
@@ -83,7 +89,13 @@ namespace StardewModdingAPI.Framework.ModHelpers
? this.RelativePathCache.GetAssetName(relativePath)
: $"temp/{Guid.NewGuid():N}";
- return new AssetDataForObject(this.ContentCore.GetLocale(), this.ContentCore.ParseAssetName(relativePath, allowLocales: false), data, key => this.ContentCore.ParseAssetName(key, allowLocales: false).Name);
+ return new AssetDataForObject(
+ locale: this.ContentCore.GetLocale(),
+ assetName: this.ContentCore.ParseAssetName(relativePath, allowLocales: false),
+ data: data,
+ getNormalizedPath: key => this.ContentCore.ParseAssetName(key, allowLocales: false).Name,
+ reflection: this.Reflection
+ );
}
}
}
diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
index 3cfe52bf..5b450c36 100644
--- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.IO;
using StardewModdingAPI.Events;
@@ -34,7 +32,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
get
{
SCore.DeprecationManager.Warn(
- source: SCore.DeprecationManager.GetSourceName(this.ModID),
+ source: SCore.DeprecationManager.GetMod(this.ModID),
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.Content)}",
version: "3.14.0",
severity: DeprecationLevel.Notice
@@ -79,7 +77,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="modID">The mod's unique ID.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="modDirectory">The full path to the mod's folder.</param>
/// <param name="currentInputState">Manages the game's input state for the current player instance. That may not be the main player in split-screen mode.</param>
/// <param name="events">Manages access to events raised by SMAPI.</param>
@@ -96,13 +94,13 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <exception cref="ArgumentNullException">An argument is null or empty.</exception>
/// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception>
public ModHelper(
- string modID, string modDirectory, Func<SInputState> currentInputState, IModEvents events,
+ IModMetadata mod, string modDirectory, Func<SInputState> currentInputState, IModEvents events,
#pragma warning disable CS0612 // deprecated code
ContentHelper contentHelper,
#pragma warning restore CS0612
IGameContentHelper gameContentHelper, IModContentHelper modContentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper
)
- : base(modID)
+ : base(mod)
{
// validate directory
if (string.IsNullOrWhiteSpace(modDirectory))
@@ -119,7 +117,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.ModContent = modContentHelper ?? throw new ArgumentNullException(nameof(modContentHelper));
this.ContentPacks = contentPackHelper ?? throw new ArgumentNullException(nameof(contentPackHelper));
this.Data = dataHelper ?? throw new ArgumentNullException(nameof(dataHelper));
- this.Input = new InputHelper(modID, currentInputState);
+ this.Input = new InputHelper(mod, currentInputState);
this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry));
this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper));
this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper));
diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
index e277e6fa..39cef758 100644
--- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using StardewModdingAPI.Framework.Reflection;
@@ -28,12 +26,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="registry">The underlying mod registry.</param>
/// <param name="proxyFactory">Generates proxy classes to access mod APIs through an arbitrary interface.</param>
/// <param name="monitor">Encapsulates monitoring and logging for the mod.</param>
- public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor)
- : base(modID)
+ public ModRegistryHelper(IModMetadata mod, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor)
+ : base(mod)
{
this.Registry = registry;
this.ProxyFactory = proxyFactory;
@@ -47,7 +45,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
/// <inheritdoc />
- public IModInfo Get(string uniqueID)
+ public IModInfo? Get(string uniqueID)
{
return this.Registry.Get(uniqueID);
}
@@ -59,7 +57,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
/// <inheritdoc />
- public object GetApi(string uniqueID)
+ public object? GetApi(string uniqueID)
{
// validate ready
if (!this.Registry.AreAllModsInitialized)
@@ -69,17 +67,18 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
// get raw API
- IModMetadata mod = this.Registry.Get(uniqueID);
+ IModMetadata? mod = this.Registry.Get(uniqueID);
if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID))
this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}.");
return mod?.Api;
}
/// <inheritdoc />
- public TInterface GetApi<TInterface>(string uniqueID) where TInterface : class
+ public TInterface? GetApi<TInterface>(string uniqueID)
+ where TInterface : class
{
// get raw API
- object api = this.GetApi(uniqueID);
+ object? api = this.GetApi(uniqueID);
if (api == null)
return null;
diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs
index 96b074e2..6900a1d2 100644
--- a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using StardewModdingAPI.Framework.Networking;
using StardewValley;
@@ -20,10 +18,10 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="multiplayer">SMAPI's core multiplayer utility.</param>
- public MultiplayerHelper(string modID, SMultiplayer multiplayer)
- : base(modID)
+ public MultiplayerHelper(IModMetadata mod, SMultiplayer multiplayer)
+ : base(mod)
{
this.Multiplayer = multiplayer;
}
@@ -41,9 +39,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
/// <inheritdoc />
- public IMultiplayerPeer GetConnectedPlayer(long id)
+ public IMultiplayerPeer? GetConnectedPlayer(long id)
{
- return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer peer)
+ return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer? peer)
? peer
: null;
}
@@ -55,7 +53,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
/// <inheritdoc />
- public void SendMessage<TMessage>(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null)
+ public void SendMessage<TMessage>(TMessage message, string messageType, string[]? modIDs = null, long[]? playerIDs = null)
{
this.Multiplayer.BroadcastModMessage(
message: message,
diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs
index 24cbd01c..a559906b 100644
--- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Reflection;
using StardewModdingAPI.Framework.Reflection;
@@ -24,11 +22,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="modName">The mod name for error messages.</param>
/// <param name="reflector">The underlying reflection helper.</param>
- public ReflectionHelper(string modID, string modName, Reflector reflector)
- : base(modID)
+ public ReflectionHelper(IModMetadata mod, string modName, Reflector reflector)
+ : base(mod)
{
this.ModName = modName;
this.Reflector = reflector;
@@ -39,7 +37,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
return this.AssertAccessAllowed(
this.Reflector.GetField<TValue>(obj, name, required)
- );
+ )!;
}
/// <inheritdoc />
@@ -47,7 +45,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
return this.AssertAccessAllowed(
this.Reflector.GetField<TValue>(type, name, required)
- );
+ )!;
}
/// <inheritdoc />
@@ -55,7 +53,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
return this.AssertAccessAllowed(
this.Reflector.GetProperty<TValue>(obj, name, required)
- );
+ )!;
}
/// <inheritdoc />
@@ -63,7 +61,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
return this.AssertAccessAllowed(
this.Reflector.GetProperty<TValue>(type, name, required)
- );
+ )!;
}
/// <inheritdoc />
@@ -71,7 +69,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
return this.AssertAccessAllowed(
this.Reflector.GetMethod(obj, name, required)
- );
+ )!;
}
/// <inheritdoc />
@@ -79,7 +77,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
return this.AssertAccessAllowed(
this.Reflector.GetMethod(type, name, required)
- );
+ )!;
}
@@ -90,7 +88,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <typeparam name="T">The field value type.</typeparam>
/// <param name="field">The field being accessed.</param>
/// <returns>Returns the same field instance for convenience.</returns>
- private IReflectedField<T> AssertAccessAllowed<T>(IReflectedField<T> field)
+ private IReflectedField<T>? AssertAccessAllowed<T>(IReflectedField<T>? field)
{
this.AssertAccessAllowed(field?.FieldInfo);
return field;
@@ -100,7 +98,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <typeparam name="T">The property value type.</typeparam>
/// <param name="property">The property being accessed.</param>
/// <returns>Returns the same property instance for convenience.</returns>
- private IReflectedProperty<T> AssertAccessAllowed<T>(IReflectedProperty<T> property)
+ private IReflectedProperty<T>? AssertAccessAllowed<T>(IReflectedProperty<T>? property)
{
this.AssertAccessAllowed(property?.PropertyInfo.GetMethod?.GetBaseDefinition());
this.AssertAccessAllowed(property?.PropertyInfo.SetMethod?.GetBaseDefinition());
@@ -110,7 +108,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Assert that mods can use the reflection helper to access the given member.</summary>
/// <param name="method">The method being accessed.</param>
/// <returns>Returns the same method instance for convenience.</returns>
- private IReflectedMethod AssertAccessAllowed(IReflectedMethod method)
+ private IReflectedMethod? AssertAccessAllowed(IReflectedMethod? method)
{
this.AssertAccessAllowed(method?.MethodInfo.GetBaseDefinition());
return method;
@@ -118,18 +116,18 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Assert that mods can use the reflection helper to access the given member.</summary>
/// <param name="member">The member being accessed.</param>
- private void AssertAccessAllowed(MemberInfo member)
+ private void AssertAccessAllowed(MemberInfo? member)
{
if (member == null)
return;
// get type which defines the member
- Type declaringType = member.DeclaringType;
+ Type? declaringType = member.DeclaringType;
if (declaringType == null)
throw new InvalidOperationException($"Can't validate access to {member.MemberType} {member.Name} because it has no declaring type."); // should never happen
// validate access
- string rootNamespace = typeof(Program).Namespace;
+ string? rootNamespace = typeof(Program).Namespace;
if (declaringType.Namespace == rootNamespace || declaringType.Namespace?.StartsWith(rootNamespace + ".") == true)
throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning. (Detected access to {declaringType.FullName}.{member.Name}.)");
}
diff --git a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs
index 37345a76..ae49d651 100644
--- a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using StardewValley;
@@ -29,11 +27,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="modID">The unique ID of the relevant mod.</param>
+ /// <param name="mod">The mod using this instance.</param>
/// <param name="locale">The initial locale.</param>
/// <param name="languageCode">The game's current language code.</param>
- public TranslationHelper(string modID, string locale, LocalizedContentManager.LanguageCode languageCode)
- : base(modID)
+ public TranslationHelper(IModMetadata mod, string locale, LocalizedContentManager.LanguageCode languageCode)
+ : base(mod)
{
this.Translator = new Translator();
this.Translator.SetLocale(locale, languageCode);
@@ -52,7 +50,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
/// <inheritdoc />
- public Translation Get(string key, object tokens)
+ public Translation Get(string key, object? tokens)
{
return this.Translator.Get(key, tokens);
}
@@ -71,7 +69,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
return this;
}
- /// <summary>Set the current locale and precache translations.</summary>
+ /// <summary>Set the current locale and pre-cache translations.</summary>
/// <param name="locale">The current locale.</param>
/// <param name="localeEnum">The game's current language code.</param>
internal void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum)
diff --git a/src/SMAPI/Framework/ModLinked.cs b/src/SMAPI/Framework/ModLinked.cs
index 5a3e38ca..8cfe6f5f 100644
--- a/src/SMAPI/Framework/ModLinked.cs
+++ b/src/SMAPI/Framework/ModLinked.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Framework
{
/// <summary>A generic tuple which links something to a mod.</summary>
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
index 1d4ddf72..b3378ad1 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using Mono.Cecil;
@@ -38,6 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Resolve an assembly reference.</summary>
/// <param name="name">The assembly name.</param>
+ /// <exception cref="AssemblyResolutionException">The assembly can't be resolved.</exception>
public override AssemblyDefinition Resolve(AssemblyNameReference name)
{
return this.ResolveName(name.Name) ?? base.Resolve(name);
@@ -46,6 +45,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Resolve an assembly reference.</summary>
/// <param name="name">The assembly name.</param>
/// <param name="parameters">The assembly reader parameters.</param>
+ /// <exception cref="AssemblyResolutionException">The assembly can't be resolved.</exception>
public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
{
return this.ResolveName(name.Name) ?? base.Resolve(name, parameters);
@@ -57,9 +57,9 @@ namespace StardewModdingAPI.Framework.ModLoading
*********/
/// <summary>Resolve a known assembly definition based on its short or full name.</summary>
/// <param name="name">The assembly's short or full name.</param>
- private AssemblyDefinition ResolveName(string name)
+ private AssemblyDefinition? ResolveName(string name)
{
- return this.Lookup.TryGetValue(name, out AssemblyDefinition match)
+ return this.Lookup.TryGetValue(name, out AssemblyDefinition? match)
? match
: null;
}
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs
index d2d5d83b..11be19fc 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Framework.ModLoading
{
/// <summary>Indicates the result of an assembly load.</summary>
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/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs
index 7c94beb7..f5d449c5 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
@@ -57,7 +55,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
{
if (this.MethodNames.Any())
{
- MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction);
if (methodRef != null && methodRef.DeclaringType.FullName == this.FullTypeName && this.MethodNames.Contains(methodRef.Name))
{
string eventName = methodRef.Name.Split(new[] { '_' }, 2)[1];
diff --git a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs
index 96b4098a..7fe4abec 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
@@ -51,7 +49,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
{
if (this.FieldNames.Any())
{
- FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+ FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction);
if (fieldRef != null && fieldRef.DeclaringType.FullName == this.FullTypeName && this.FieldNames.Contains(fieldRef.Name))
{
this.FieldNames.Remove(fieldRef.Name);
diff --git a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs
index 7d3c1fd7..e8fdc8c7 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Mono.Cecil;
using Mono.Cecil.Cil;
using StardewModdingAPI.Framework.ModLoading.Framework;
@@ -54,7 +52,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
/// <param name="instruction">The IL instruction.</param>
protected bool IsMatch(Instruction instruction)
{
- MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction);
return
methodRef != null
&& methodRef.DeclaringType.FullName == this.FullTypeName
diff --git a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs
index b2f2e193..2af76f55 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Mono.Cecil;
using Mono.Cecil.Cil;
using StardewModdingAPI.Framework.ModLoading.Framework;
@@ -54,7 +52,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
/// <param name="instruction">The IL instruction.</param>
protected bool IsMatch(Instruction instruction)
{
- MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction);
return
methodRef != null
&& methodRef.DeclaringType.FullName == this.FullTypeName
diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
index 81f90498..f34542c3 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -34,11 +33,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
{
// field reference
- FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+ FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction);
if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType))
{
// get target field
- FieldDefinition targetField = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name);
+ FieldDefinition? targetField = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name);
if (targetField == null)
return false;
@@ -51,16 +50,16 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
}
// method reference
- MethodReference methodReference = RewriteHelper.AsMethodReference(instruction);
+ MethodReference? methodReference = RewriteHelper.AsMethodReference(instruction);
if (methodReference != null && !this.IsUnsupported(methodReference) && this.ShouldValidate(methodReference.DeclaringType))
{
// get potential targets
- MethodDefinition[] candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray();
+ MethodDefinition[]? candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray();
if (candidateMethods == null || !candidateMethods.Any())
return false;
// compare return types
- MethodDefinition methodDef = methodReference.Resolve();
+ MethodDefinition? methodDef = methodReference.Resolve();
if (methodDef == null)
return false; // validated by ReferenceToMissingMemberFinder
@@ -80,7 +79,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
*********/
/// <summary>Whether references to the given type should be validated.</summary>
/// <param name="type">The type reference.</param>
- private bool ShouldValidate(TypeReference type)
+ private bool ShouldValidate([NotNullWhen(true)] TypeReference? type)
{
return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name);
}
diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs
index 001d1986..fae7fb12 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using Mono.Cecil;
using Mono.Cecil.Cil;
using StardewModdingAPI.Framework.ModLoading.Framework;
@@ -33,10 +32,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
{
// field reference
- FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+ FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction);
if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType))
{
- FieldDefinition target = fieldRef.Resolve();
+ FieldDefinition? target = fieldRef.Resolve();
if (target == null || target.HasConstant)
{
this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)");
@@ -45,10 +44,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
}
// method reference
- MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction);
if (methodRef != null && this.ShouldValidate(methodRef.DeclaringType) && !this.IsUnsupported(methodRef))
{
- MethodDefinition target = methodRef.Resolve();
+ MethodDefinition? target = methodRef.Resolve();
if (target == null)
{
string phrase;
@@ -73,7 +72,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
*********/
/// <summary>Whether references to the given type should be validated.</summary>
/// <param name="type">The type reference.</param>
- private bool ShouldValidate(TypeReference type)
+ private bool ShouldValidate([NotNullWhen(true)] TypeReference? type)
{
return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name);
}
diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs
index 4c589ed8..17acbf9a 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Mono.Cecil;
using StardewModdingAPI.Framework.ModLoading.Framework;
@@ -19,7 +17,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
private readonly InstructionHandleResult Result;
/// <summary>Get whether a matched type should be ignored.</summary>
- private readonly Func<TypeReference, bool> ShouldIgnore;
+ private readonly Func<TypeReference, bool>? ShouldIgnore;
/*********
@@ -29,7 +27,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
/// <param name="assemblyName">The full assembly name to which to find references.</param>
/// <param name="result">The result to return for matching instructions.</param>
/// <param name="shouldIgnore">Get whether a matched type should be ignored.</param>
- public TypeAssemblyFinder(string assemblyName, InstructionHandleResult result, Func<TypeReference, bool> shouldIgnore = null)
+ public TypeAssemblyFinder(string assemblyName, InstructionHandleResult result, Func<TypeReference, bool>? shouldIgnore = null)
: base(defaultPhrase: $"{assemblyName} assembly")
{
this.AssemblyName = assemblyName;
diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs
index 04a5b970..77762f41 100644
--- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs
+++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using Mono.Cecil;
@@ -20,7 +18,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
private readonly InstructionHandleResult Result;
/// <summary>Get whether a matched type should be ignored.</summary>
- private readonly Func<TypeReference, bool> ShouldIgnore;
+ private readonly Func<TypeReference, bool>? ShouldIgnore;
/*********
@@ -30,7 +28,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
/// <param name="fullTypeNames">The full type names to match.</param>
/// <param name="result">The result to return for matching instructions.</param>
/// <param name="shouldIgnore">Get whether a matched type should be ignored.</param>
- public TypeFinder(string[] fullTypeNames, InstructionHandleResult result, Func<TypeReference, bool> shouldIgnore = null)
+ public TypeFinder(string[] fullTypeNames, InstructionHandleResult result, Func<TypeReference, bool>? shouldIgnore = null)
: base(defaultPhrase: $"{string.Join(", ", fullTypeNames)} type{(fullTypeNames.Length != 1 ? "s" : "")}") // default phrase should never be used
{
this.FullTypeNames = new HashSet<string>(fullTypeNames);
@@ -42,7 +40,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
/// <param name="fullTypeName">The full type name to match.</param>
/// <param name="result">The result to return for matching instructions.</param>
/// <param name="shouldIgnore">Get whether a matched type should be ignored.</param>
- public TypeFinder(string fullTypeName, InstructionHandleResult result, Func<TypeReference, bool> shouldIgnore = null)
+ public TypeFinder(string fullTypeName, InstructionHandleResult result, Func<TypeReference, bool>? shouldIgnore = null)
: this(new[] { fullTypeName }, result, shouldIgnore) { }
/// <inheritdoc />
diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs
index bea786cd..865bf076 100644
--- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs
+++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using Mono.Cecil;
@@ -59,7 +57,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
/// <param name="flag">The result flag to set.</param>
/// <param name="resultMessage">The result message to add.</param>
/// <returns>Returns true for convenience.</returns>
- protected bool MarkFlag(InstructionHandleResult flag, string resultMessage = null)
+ protected bool MarkFlag(InstructionHandleResult flag, string? resultMessage = null)
{
this.Flags.Add(flag);
if (resultMessage != null)
diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs
index 09ff78f7..55369602 100644
--- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -10,6 +9,7 @@ using Mono.Collections.Generic;
namespace StardewModdingAPI.Framework.ModLoading.Framework
{
/// <summary>Handles recursively rewriting loaded assembly code.</summary>
+ [SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = "Rewrite callbacks are invoked immediately.")]
internal class RecursiveRewriter
{
/*********
@@ -77,7 +77,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
{
changed |= this.RewriteModuleImpl(this.Module);
- foreach (var type in types)
+ foreach (TypeDefinition type in types)
changed |= this.RewriteTypeDefinition(type);
}
catch (Exception ex)
@@ -129,9 +129,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
ILProcessor cil = method.Body.GetILProcessor();
Collection<Instruction> instructions = cil.Body.Instructions;
bool addedInstructions = false;
+ // ReSharper disable once ForCanBeConvertedToForeach -- deliberate to allow changing the collection
for (int i = 0; i < instructions.Count; i++)
{
- var instruction = instructions[i];
+ Instruction instruction = instructions[i];
if (instruction.OpCode.Code == Code.Nop)
continue;
@@ -174,7 +175,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
bool rewritten = false;
// field reference
- FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+ FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction);
if (fieldRef != null)
{
rewritten |= this.RewriteTypeReference(fieldRef.DeclaringType, newType => fieldRef.DeclaringType = newType);
@@ -182,7 +183,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
}
// method reference
- MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction);
if (methodRef != null)
this.RewriteMethodReference(methodRef);
@@ -212,7 +213,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
});
rewritten |= this.RewriteTypeReference(methodRef.ReturnType, newType => methodRef.ReturnType = newType);
- foreach (var parameter in methodRef.Parameters)
+ foreach (ParameterDefinition parameter in methodRef.Parameters)
rewritten |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType);
if (methodRef is GenericInstanceMethod genericRef)
@@ -264,7 +265,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
bool curChanged = false;
// attribute type
- TypeReference newAttrType = null;
+ TypeReference? newAttrType = null;
rewritten |= this.RewriteTypeReference(attribute.AttributeType, newType =>
{
newAttrType = newType;
@@ -289,9 +290,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
if (curChanged)
{
// get constructor
- MethodDefinition constructor = (newAttrType ?? attribute.AttributeType)
+ MethodDefinition? constructor = (newAttrType ?? attribute.AttributeType)
.Resolve()
- .Methods
+ ?.Methods
.Where(method => method.IsConstructor)
.FirstOrDefault(ctor => RewriteHelper.HasMatchingSignature(ctor, attribute.Constructor));
if (constructor == null)
@@ -301,9 +302,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
var newAttr = new CustomAttribute(this.Module.ImportReference(constructor));
for (int i = 0; i < argTypes.Length; i++)
newAttr.ConstructorArguments.Add(new CustomAttributeArgument(argTypes[i], attribute.ConstructorArguments[i].Value));
- foreach (var prop in attribute.Properties)
+ foreach (CustomAttributeNamedArgument prop in attribute.Properties)
newAttr.Properties.Add(new CustomAttributeNamedArgument(prop.Name, prop.Argument));
- foreach (var field in attribute.Fields)
+ foreach (CustomAttributeNamedArgument field in attribute.Fields)
newAttr.Fields.Add(new CustomAttributeNamedArgument(field.Name, field.Argument));
// swap attribute
diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
index 8f47fbdd..15f71251 100644
--- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
+++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Linq;
using System.Reflection;
@@ -23,7 +21,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
*********/
/// <summary>Get the field reference from an instruction if it matches.</summary>
/// <param name="instruction">The IL instruction.</param>
- public static FieldReference AsFieldReference(Instruction instruction)
+ public static FieldReference? AsFieldReference(Instruction instruction)
{
return instruction.OpCode == OpCodes.Ldfld || instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Stfld || instruction.OpCode == OpCodes.Stsfld
? (FieldReference)instruction.Operand
@@ -32,7 +30,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
/// <summary>Get the method reference from an instruction if it matches.</summary>
/// <param name="instruction">The IL instruction.</param>
- public static MethodReference AsMethodReference(Instruction instruction)
+ public static MethodReference? AsMethodReference(Instruction instruction)
{
return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt || instruction.OpCode == OpCodes.Newobj
? (MethodReference)instruction.Operand
@@ -42,7 +40,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
/// <summary>Get the CIL instruction to load a value onto the stack.</summary>
/// <param name="rawValue">The constant value to inject.</param>
/// <returns>Returns the instruction, or <c>null</c> if the value type isn't supported.</returns>
- public static Instruction GetLoadValueInstruction(object rawValue)
+ public static Instruction? GetLoadValueInstruction(object? rawValue)
{
return rawValue switch
{
@@ -151,7 +149,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
/// <param name="typeA">The type ID to compare.</param>
/// <param name="typeB">The other type ID to compare.</param>
/// <returns>true if the type IDs look like the same type, false if not.</returns>
- public static bool LooksLikeSameType(TypeReference typeA, TypeReference typeB)
+ public static bool LooksLikeSameType(TypeReference? typeA, TypeReference? typeB)
{
return RewriteHelper.TypeDefinitionComparer.Equals(typeA, typeB);
}
diff --git a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs
index 126504e3..d41732f8 100644
--- a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs
+++ b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using Mono.Cecil;
diff --git a/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs b/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs
index 29406f2a..1f9add30 100644
--- a/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs
+++ b/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Framework.ModLoading
diff --git a/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs b/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs
index 9dca9bc4..b53a9886 100644
--- a/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs
+++ b/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Framework.ModLoading
@@ -10,7 +8,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Construct an instance.</summary>
/// <param name="message">The error message.</param>
/// <param name="ex">The underlying exception, if any.</param>
- public InvalidModStateException(string message, Exception ex = null)
+ public InvalidModStateException(string message, Exception? ex = null)
: base(message, ex) { }
}
}
diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
index 0e698bfd..fe54634b 100644
--- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/SMAPI/Framework/ModLoading/ModMetadata.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.Framework.ModHelpers;
@@ -44,7 +43,7 @@ namespace StardewModdingAPI.Framework.ModLoading
public IManifest Manifest { get; }
/// <inheritdoc />
- public ModDataRecordVersionedFields DataRecord { get; }
+ public ModDataRecordVersionedFields? DataRecord { get; }
/// <inheritdoc />
public ModMetadataStatus Status { get; private set; }
@@ -56,33 +55,35 @@ namespace StardewModdingAPI.Framework.ModLoading
public ModWarning Warnings => this.ActualWarnings & ~(this.DataRecord?.DataRecord.SuppressWarnings ?? ModWarning.None);
/// <inheritdoc />
- public string Error { get; private set; }
+ public string? Error { get; private set; }
/// <inheritdoc />
- public string ErrorDetails { get; private set; }
+ public string? ErrorDetails { get; private set; }
/// <inheritdoc />
public bool IsIgnored { get; }
/// <inheritdoc />
- public IMod Mod { get; private set; }
+ public IMod? Mod { get; private set; }
/// <inheritdoc />
- public IContentPack ContentPack { get; private set; }
+ public IContentPack? ContentPack { get; private set; }
/// <inheritdoc />
- public TranslationHelper Translations { get; private set; }
+ public TranslationHelper? Translations { get; private set; }
/// <inheritdoc />
- public IMonitor Monitor { get; private set; }
+ public IMonitor? Monitor { get; private set; }
/// <inheritdoc />
- public object Api { get; private set; }
+ public object? Api { get; private set; }
/// <inheritdoc />
- public ModEntryModel UpdateCheckData { get; private set; }
+ public ModEntryModel? UpdateCheckData { get; private set; }
/// <inheritdoc />
+ [MemberNotNullWhen(true, nameof(ModMetadata.ContentPack))]
+ [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The manifest may be null for broken mods while loading.")]
public bool IsContentPack => this.Manifest?.ContentPackFor != null;
/// <summary>The fake content packs created by this mod, if any.</summary>
@@ -99,13 +100,13 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="manifest">The mod manifest.</param>
/// <param name="dataRecord">Metadata about the mod from SMAPI's internal data (if any).</param>
/// <param name="isIgnored">Whether the mod folder should be ignored. This should be <c>true</c> if it was found within a folder whose name starts with a dot.</param>
- public ModMetadata(string displayName, string directoryPath, string rootPath, IManifest manifest, ModDataRecordVersionedFields dataRecord, bool isIgnored)
+ public ModMetadata(string displayName, string directoryPath, string rootPath, IManifest? manifest, ModDataRecordVersionedFields? dataRecord, bool isIgnored)
{
this.DisplayName = displayName;
this.DirectoryPath = directoryPath;
this.RootPath = rootPath;
this.RelativeDirectoryPath = PathUtilities.GetRelativePath(this.RootPath, this.DirectoryPath);
- this.Manifest = manifest;
+ this.Manifest = manifest!; // manifest may be null in low-level SMAPI code, but won't be null once it's received by mods via IModInfo
this.DataRecord = dataRecord;
this.IsIgnored = isIgnored;
@@ -121,7 +122,7 @@ namespace StardewModdingAPI.Framework.ModLoading
}
/// <inheritdoc />
- public IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string error, string errorDetails = null)
+ public IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string? error, string? errorDetails = null)
{
this.Status = status;
this.FailReason = reason;
@@ -162,7 +163,7 @@ namespace StardewModdingAPI.Framework.ModLoading
}
/// <inheritdoc />
- public IModMetadata SetApi(object api)
+ public IModMetadata SetApi(object? api)
{
this.Api = api;
return this;
@@ -176,6 +177,7 @@ namespace StardewModdingAPI.Framework.ModLoading
}
/// <inheritdoc />
+ [MemberNotNullWhen(true, nameof(IModInfo.Manifest))]
public bool HasManifest()
{
return this.Manifest != null;
@@ -190,7 +192,7 @@ namespace StardewModdingAPI.Framework.ModLoading
}
/// <inheritdoc />
- public bool HasID(string id)
+ public bool HasID(string? id)
{
return
this.HasID()
@@ -245,7 +247,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <inheritdoc />
public string GetRelativePathWithRoot()
{
- string rootFolderName = Path.GetFileName(this.RootPath) ?? "";
+ string rootFolderName = Path.GetFileName(this.RootPath);
return Path.Combine(rootFolderName, this.RelativeDirectoryPath);
}
@@ -254,7 +256,7 @@ namespace StardewModdingAPI.Framework.ModLoading
{
foreach (var reference in this.FakeContentPacks.ToArray())
{
- if (!reference.TryGetTarget(out ContentPack pack))
+ if (!reference.TryGetTarget(out ContentPack? pack))
{
this.FakeContentPacks.Remove(reference);
continue;
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 2842c11a..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:
@@ -394,35 +396,16 @@ namespace StardewModdingAPI.Framework.ModLoading
}
}
- /// <summary>Get all mod folders in a root folder, passing through empty folders as needed.</summary>
- /// <param name="rootPath">The root folder path to search.</param>
- private IEnumerable<DirectoryInfo> GetModFolders(string rootPath)
- {
- foreach (string modRootPath in Directory.GetDirectories(rootPath))
- {
- DirectoryInfo directory = new(modRootPath);
-
- // if a folder only contains another folder, check the inner folder instead
- while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
- directory = directory.GetDirectories().First();
-
- yield return directory;
- }
- }
-
/// <summary>Get the dependencies declared in a manifest.</summary>
/// <param name="manifest">The mod manifest.</param>
/// <param name="loadedMods">The loaded mods.</param>
private IEnumerable<ModDependency> GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods)
{
- 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)
@@ -431,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;
@@ -448,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")
+ ".";
@@ -475,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; }
/*********
@@ -492,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/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs
index 0898f095..d4366294 100644
--- a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs
+++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs
index c05005b8..afe38bfd 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -19,7 +17,7 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
/*********
** Public methods
*********/
- public static ConstructorInfo DeclaredConstructor(Type type, Type[] parameters = null)
+ public static ConstructorInfo DeclaredConstructor(Type type, Type[]? parameters = null)
{
// Harmony 1.x matched both static and instance constructors
return
@@ -27,7 +25,7 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
?? AccessTools.DeclaredConstructor(type, parameters, searchForStatic: true);
}
- public static ConstructorInfo Constructor(Type type, Type[] parameters = null)
+ public static ConstructorInfo Constructor(Type type, Type[]? parameters = null)
{
// Harmony 1.x matched both static and instance constructors
return
diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs
index fea8c100..9c8ba2b0 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -30,7 +28,8 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
return new Harmony(id);
}
- public DynamicMethod Patch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null)
+ [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "If the user passes a null original method, we let it fail in the underlying Harmony instance instead of handling it here.")]
+ public DynamicMethod Patch(MethodBase original, HarmonyMethod? prefix = null, HarmonyMethod? postfix = null, HarmonyMethod? transpiler = null)
{
// In Harmony 1.x you could target a virtual method that's not implemented by the
// target type, but in Harmony 2.0 you need to target the concrete implementation.
@@ -60,7 +59,7 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
/// <param name="prefix">The prefix method, if any.</param>
/// <param name="postfix">The postfix method, if any.</param>
/// <param name="transpiler">The transpiler method, if any.</param>
- private string GetPatchTypesLabel(HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null)
+ private string GetPatchTypesLabel(HarmonyMethod? prefix = null, HarmonyMethod? postfix = null, HarmonyMethod? transpiler = null)
{
var patchTypes = new List<string>();
@@ -76,7 +75,7 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
/// <summary>Get a human-readable label for the method being patched.</summary>
/// <param name="method">The method being patched.</param>
- private string GetMethodLabel(MethodBase method)
+ private string GetMethodLabel(MethodBase? method)
{
return method != null
? $"method {method.DeclaringType?.FullName}.{method.Name}"
diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs
index 93124591..2b1ca54b 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
@@ -23,7 +21,7 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
this.ImportMethodImpl(method);
}
- public HarmonyMethodFacade(Type type, string name, Type[] parameters = null)
+ public HarmonyMethodFacade(Type type, string name, Type[]? parameters = null)
{
this.ImportMethodImpl(AccessTools.Method(type, name, parameters));
}
@@ -40,7 +38,7 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
// internal code still handles null fine. For backwards compatibility, this bypasses
// the new restriction when the mod hasn't been updated for Harmony 2.0 yet.
- MethodInfo importMethod = typeof(HarmonyMethod).GetMethod("ImportMethod", BindingFlags.Instance | BindingFlags.NonPublic);
+ MethodInfo? importMethod = typeof(HarmonyMethod).GetMethod("ImportMethod", BindingFlags.Instance | BindingFlags.NonPublic);
if (importMethod == null)
throw new InvalidOperationException("Can't find 'HarmonyMethod.ImportMethod' method");
importMethod.Invoke(this, new object[] { methodInfo });
diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs
index 20a30f8f..67569424 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
@@ -20,7 +18,8 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public SpriteBatchFacade(GraphicsDevice graphicsDevice) : base(graphicsDevice) { }
+ public SpriteBatchFacade(GraphicsDevice graphicsDevice)
+ : base(graphicsDevice) { }
/****
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/ArchitectureAssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/ArchitectureAssemblyRewriter.cs
index 4985d72a..cc830216 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/ArchitectureAssemblyRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/ArchitectureAssemblyRewriter.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Mono.Cecil;
using StardewModdingAPI.Framework.ModLoading.Framework;
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/ModLoading/Rewriters/HarmonyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HarmonyRewriter.cs
index 92397c58..aea490c8 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/HarmonyRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/HarmonyRewriter.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using HarmonyLib;
using Mono.Cecil;
@@ -59,7 +57,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
if (this.ShouldRewrite)
{
// rewrite Harmony 1.x methods to Harmony 2.0
- MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction);
if (this.TryRewriteMethodsToFacade(module, methodRef))
{
this.OnChanged();
@@ -67,7 +65,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
}
// rewrite renamed fields
- FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+ FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction);
if (fieldRef != null)
{
if (fieldRef.DeclaringType.FullName == "HarmonyLib.HarmonyMethod" && fieldRef.Name == "prioritiy")
@@ -95,13 +93,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/// <summary>Rewrite methods to use Harmony facades if needed.</summary>
/// <param name="module">The assembly module containing the method reference.</param>
/// <param name="methodRef">The method reference to map.</param>
- private bool TryRewriteMethodsToFacade(ModuleDefinition module, MethodReference methodRef)
+ private bool TryRewriteMethodsToFacade(ModuleDefinition module, MethodReference? methodRef)
{
if (!this.ReplacedTypes)
return false; // not Harmony (or already using Harmony 2.0)
// get facade type
- Type toType = methodRef?.DeclaringType.FullName switch
+ Type? toType = methodRef?.DeclaringType.FullName switch
{
"HarmonyLib.Harmony" => typeof(HarmonyInstanceFacade),
"HarmonyLib.AccessTools" => typeof(AccessToolsFacade),
@@ -112,9 +110,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
return false;
// map if there's a matching method
- if (RewriteHelper.HasMatchingSignature(toType, methodRef))
+ if (RewriteHelper.HasMatchingSignature(toType, methodRef!))
{
- methodRef.DeclaringType = module.ImportReference(toType);
+ methodRef!.DeclaringType = module.ImportReference(toType);
return true;
}
@@ -139,7 +137,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
{
string fullName = type.FullName.Replace("Harmony.", "HarmonyLib.");
string targetName = typeof(Harmony).AssemblyQualifiedName!.Replace(typeof(Harmony).FullName!, fullName);
- return Type.GetType(targetName, throwOnError: true);
+ return Type.GetType(targetName, throwOnError: true)!;
}
}
}
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs
index fc06e779..9c6a3980 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -33,17 +32,17 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
{
// get field ref
- FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
+ FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction);
if (fieldRef == null || !this.ShouldValidate(fieldRef.DeclaringType))
return false;
// skip if not broken
- FieldDefinition fieldDefinition = fieldRef.Resolve();
+ FieldDefinition? fieldDefinition = fieldRef.Resolve();
if (fieldDefinition?.HasConstant == false)
return false;
// rewrite if possible
- TypeDefinition declaringType = fieldRef.DeclaringType.Resolve();
+ TypeDefinition? declaringType = fieldRef.DeclaringType.Resolve();
bool isRead = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld;
return
this.TryRewriteToProperty(module, instruction, fieldRef, declaringType, isRead)
@@ -56,7 +55,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
*********/
/// <summary>Whether references to the given type should be validated.</summary>
/// <param name="type">The type reference.</param>
- private bool ShouldValidate(TypeReference type)
+ private bool ShouldValidate([NotNullWhen(true)] TypeReference? type)
{
return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name);
}
@@ -70,8 +69,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
private bool TryRewriteToProperty(ModuleDefinition module, Instruction instruction, FieldReference fieldRef, TypeDefinition declaringType, bool isRead)
{
// get equivalent property
- PropertyDefinition property = declaringType?.Properties.FirstOrDefault(p => p.Name == fieldRef.Name);
- MethodDefinition method = isRead ? property?.GetMethod : property?.SetMethod;
+ PropertyDefinition? property = declaringType?.Properties.FirstOrDefault(p => p.Name == fieldRef.Name);
+ MethodDefinition? method = isRead ? property?.GetMethod : property?.SetMethod;
if (method == null)
return false;
@@ -86,14 +85,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/// <summary>Try rewriting the field into a matching const field.</summary>
/// <param name="instruction">The CIL instruction to rewrite.</param>
/// <param name="field">The field definition.</param>
- private bool TryRewriteToConstField(Instruction instruction, FieldDefinition field)
+ private bool TryRewriteToConstField(Instruction instruction, FieldDefinition? field)
{
// must have been a static field read, and the new field must be const
if (instruction.OpCode != OpCodes.Ldsfld || field?.HasConstant != true)
return false;
// get opcode for value type
- Instruction loadInstruction = RewriteHelper.GetLoadValueInstruction(field.Constant);
+ Instruction? loadInstruction = RewriteHelper.GetLoadValueInstruction(field.Constant);
if (loadInstruction == null)
return false;
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs
index 4860072c..601ecbbc 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -33,7 +32,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
{
// get method ref
- MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction);
if (methodRef == null || !this.ShouldValidate(methodRef.DeclaringType))
return false;
@@ -42,13 +41,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
return false;
// get type
- var type = methodRef.DeclaringType.Resolve();
+ TypeDefinition? type = methodRef.DeclaringType.Resolve();
if (type == null)
return false;
// get method definition
- MethodDefinition method = null;
- foreach (var match in type.Methods.Where(p => p.Name == methodRef.Name))
+ MethodDefinition? method = null;
+ foreach (MethodDefinition match in type.Methods.Where(p => p.Name == methodRef.Name))
{
// reference matches initial parameters of definition
if (methodRef.Parameters.Count >= match.Parameters.Count || !this.InitialParametersMatch(methodRef, match))
@@ -72,7 +71,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
return false; // SMAPI needs to load the value onto the stack before the method call, but the optional parameter type wasn't recognized
// rewrite method reference
- foreach (Instruction loadInstruction in loadInstructions)
+ foreach (Instruction? loadInstruction in loadInstructions)
cil.InsertBefore(instruction, loadInstruction);
instruction.Operand = module.ImportReference(method);
@@ -86,7 +85,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
*********/
/// <summary>Whether references to the given type should be validated.</summary>
/// <param name="type">The type reference.</param>
- private bool ShouldValidate(TypeReference type)
+ private bool ShouldValidate([NotNullWhen(true)] TypeReference? type)
{
return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name);
}
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs
index 00daf337..2e2f6316 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -28,7 +27,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/// <param name="fromType">The type whose methods to remap.</param>
/// <param name="toType">The type with methods to map to.</param>
/// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
- public MethodParentRewriter(string fromType, Type toType, string nounPhrase = null)
+ public MethodParentRewriter(string fromType, Type toType, string? nounPhrase = null)
: base(nounPhrase ?? $"{fromType.Split('.').Last()} methods")
{
this.FromType = fromType;
@@ -39,14 +38,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/// <param name="fromType">The type whose methods to remap.</param>
/// <param name="toType">The type with methods to map to.</param>
/// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param>
- public MethodParentRewriter(Type fromType, Type toType, string nounPhrase = null)
- : this(fromType.FullName, toType, nounPhrase) { }
+ public MethodParentRewriter(Type fromType, Type toType, string? nounPhrase = null)
+ : this(fromType.FullName!, toType, nounPhrase) { }
/// <inheritdoc />
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
{
// get method ref
- MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
+ MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction);
if (!this.IsMatch(methodRef))
return false;
@@ -61,7 +60,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
*********/
/// <summary>Get whether a CIL instruction matches.</summary>
/// <param name="methodRef">The method reference.</param>
- private bool IsMatch(MethodReference methodRef)
+ private bool IsMatch([NotNullWhen(true)] MethodReference? methodRef)
{
return
methodRef != null
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs
index bdc4c4f3..a81cb5be 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Mono.Cecil;
using StardewModdingAPI.Framework.ModLoading.Framework;
@@ -19,7 +17,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
private readonly Type ToType;
/// <summary>Get whether a matched type should be ignored.</summary>
- private readonly Func<TypeReference, bool> ShouldIgnore;
+ private readonly Func<TypeReference, bool>? ShouldIgnore;
/*********
@@ -29,7 +27,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/// <param name="fromTypeFullName">The full type name to which to find references.</param>
/// <param name="toType">The new type to reference.</param>
/// <param name="shouldIgnore">Get whether a matched type should be ignored.</param>
- public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func<TypeReference, bool> shouldIgnore = null)
+ public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func<TypeReference, bool>? shouldIgnore = null)
: base($"{fromTypeFullName} type")
{
this.FromTypeName = fromTypeFullName;
diff --git a/src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs
index 55b7e0c8..2171895d 100644
--- a/src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs
+++ b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.IO;
using Mono.Cecil;
using Mono.Cecil.Cil;
diff --git a/src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs
index 4af7c1e7..0d3aff9f 100644
--- a/src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs
+++ b/src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -38,7 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Symbols
/// <param name="fileName">The assembly file name.</param>
public ISymbolReader GetSymbolReader(ModuleDefinition module, string fileName)
{
- return this.SymbolsByAssemblyPath.TryGetValue(module.Name, out Stream symbolData)
+ return this.SymbolsByAssemblyPath.TryGetValue(module.Name, out Stream? symbolData)
? new SymbolReader(module, symbolData)
: this.BaseProvider.GetSymbolReader(module, fileName);
}
@@ -48,7 +46,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Symbols
/// <param name="symbolStream">The loaded symbol file stream.</param>
public ISymbolReader GetSymbolReader(ModuleDefinition module, Stream symbolStream)
{
- return this.SymbolsByAssemblyPath.TryGetValue(module.Name, out Stream symbolData)
+ return this.SymbolsByAssemblyPath.TryGetValue(module.Name, out Stream? symbolData)
? new SymbolReader(module, symbolData)
: this.BaseProvider.GetSymbolReader(module, symbolStream);
}
diff --git a/src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs b/src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs
index c2ac4cd6..8f7e05d1 100644
--- a/src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs
+++ b/src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.IO;
using Mono.Cecil;
using Mono.Cecil.Cil;
diff --git a/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs
index 248c29fc..d81d763e 100644
--- a/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs
+++ b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -18,7 +16,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <c>TKey</c> in the above example). If all components are equal after substitution, and the
/// tokens can all be mapped to the same generic type, the types are considered equal.
/// </remarks>
- internal class TypeReferenceComparer : IEqualityComparer<TypeReference>
+ internal class TypeReferenceComparer : IEqualityComparer<TypeReference?>
{
/*********
** Public methods
@@ -26,7 +24,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Get whether the specified objects are equal.</summary>
/// <param name="a">The first object to compare.</param>
/// <param name="b">The second object to compare.</param>
- public bool Equals(TypeReference a, TypeReference b)
+ public bool Equals(TypeReference? a, TypeReference? b)
{
if (a == null || b == null)
return a == b;
@@ -54,7 +52,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="typeB">The second type to compare.</param>
private bool HeuristicallyEquals(TypeReference typeA, TypeReference typeB)
{
- bool HeuristicallyEquals(string typeNameA, string typeNameB, IDictionary<string, string> tokenMap)
+ bool HeuristicallyEqualsImpl(string typeNameA, string typeNameB, IDictionary<string, string> tokenMap)
{
// analyze type names
bool hasTokensA = typeNameA.Contains("!");
@@ -82,14 +80,14 @@ namespace StardewModdingAPI.Framework.ModLoading
for (int i = 0; i < symbolsA.Length; i++)
{
- if (!HeuristicallyEquals(symbolsA[i], symbolsB[i], tokenMap))
+ if (!HeuristicallyEqualsImpl(symbolsA[i], symbolsB[i], tokenMap))
return false;
}
return true;
}
- return HeuristicallyEquals(typeA.FullName, typeB.FullName, new Dictionary<string, string>());
+ return HeuristicallyEqualsImpl(typeA.FullName, typeB.FullName, new Dictionary<string, string>());
}
/// <summary>Map a generic type placeholder (like <c>!0</c>) to its actual type.</summary>
@@ -99,7 +97,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <returns>Returns the previously-mapped type if applicable, else the <paramref name="type"/>.</returns>
private string MapPlaceholder(string placeholder, string type, IDictionary<string, string> map)
{
- if (map.TryGetValue(placeholder, out string result))
+ if (map.TryGetValue(placeholder, out string? result))
return result;
map[placeholder] = type;
diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs
index cae38637..1ae5643f 100644
--- a/src/SMAPI/Framework/ModRegistry.cs
+++ b/src/SMAPI/Framework/ModRegistry.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -37,12 +35,12 @@ namespace StardewModdingAPI.Framework
this.Mods.Add(metadata);
}
- /// <summary>Track a mod's assembly for use via <see cref="GetFrom"/>.</summary>
+ /// <summary>Track a mod's assembly for use via <see cref="GetFrom(Type?)"/>.</summary>
/// <param name="metadata">The mod metadata.</param>
/// <param name="modAssembly">The mod assembly.</param>
public void TrackAssemblies(IModMetadata metadata, Assembly modAssembly)
{
- this.ModNamesByAssembly[modAssembly.FullName] = metadata;
+ this.ModNamesByAssembly[modAssembly.FullName!] = metadata;
}
/// <summary>Get metadata for all loaded mods.</summary>
@@ -61,8 +59,8 @@ namespace StardewModdingAPI.Framework
/// <summary>Get metadata for a loaded mod.</summary>
/// <param name="uniqueID">The mod's unique ID.</param>
- /// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns>
- public IModMetadata Get(string uniqueID)
+ /// <returns>Returns the mod's metadata, or <c>null</c> if not found.</returns>
+ public IModMetadata? Get(string uniqueID)
{
// normalize search ID
if (string.IsNullOrWhiteSpace(uniqueID))
@@ -75,15 +73,15 @@ namespace StardewModdingAPI.Framework
/// <summary>Get the mod metadata from one of its assemblies.</summary>
/// <param name="type">The type to check.</param>
- /// <returns>Returns the mod name, or <c>null</c> if the type isn't part of a known mod.</returns>
- public IModMetadata GetFrom(Type type)
+ /// <returns>Returns the mod's metadata, or <c>null</c> if the type isn't part of a known mod.</returns>
+ public IModMetadata? GetFrom(Type? type)
{
// null
if (type == null)
return null;
// known type
- string assemblyName = type.Assembly.FullName;
+ string assemblyName = type.Assembly.FullName!;
if (this.ModNamesByAssembly.ContainsKey(assemblyName))
return this.ModNamesByAssembly[assemblyName];
@@ -91,9 +89,18 @@ namespace StardewModdingAPI.Framework
return null;
}
- /// <summary>Get the friendly name for the closest assembly registered as a source of deprecation warnings.</summary>
- /// <returns>Returns the source name, or <c>null</c> if no registered assemblies were found.</returns>
- public IModMetadata GetFromStack()
+ /// <summary>Get the mod metadata from a stack frame, if any.</summary>
+ /// <param name="frame">The stack frame to check.</param>
+ /// <returns>Returns the mod's metadata, or <c>null</c> if the frame isn't part of a known mod.</returns>
+ public IModMetadata? GetFrom(StackFrame frame)
+ {
+ MethodBase? method = frame.GetMethod();
+ return this.GetFrom(method?.ReflectedType);
+ }
+
+ /// <summary>Get the mod metadata from the closest assembly registered as a source of deprecation warnings.</summary>
+ /// <returns>Returns the mod's metadata, or <c>null</c> if no registered assemblies were found.</returns>
+ public IModMetadata? GetFromStack()
{
// get stack frames
StackTrace stack = new();
@@ -102,8 +109,7 @@ namespace StardewModdingAPI.Framework
// search stack for a source assembly
foreach (StackFrame frame in frames)
{
- MethodBase method = frame.GetMethod();
- IModMetadata mod = this.GetFrom(method.ReflectedType);
+ IModMetadata? mod = this.GetFrom(frame);
if (mod != null)
return mod;
}
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/Monitor.cs b/src/SMAPI/Framework/Monitor.cs
index de145d1d..6b53daff 100644
--- a/src/SMAPI/Framework/Monitor.cs
+++ b/src/SMAPI/Framework/Monitor.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
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/Networking/SGalaxyNetClient.cs b/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs
index 8e19b4a7..01095c66 100644
--- a/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs
+++ b/src/SMAPI/Framework/Networking/SGalaxyNetClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Galaxy.Api;
using StardewValley.Network;
diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs
index 07a004a2..71e11576 100644
--- a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs
+++ b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
diff --git a/src/SMAPI/Framework/Networking/SLidgrenClient.cs b/src/SMAPI/Framework/Networking/SLidgrenClient.cs
index ecf18cbd..39876744 100644
--- a/src/SMAPI/Framework/Networking/SLidgrenClient.cs
+++ b/src/SMAPI/Framework/Networking/SLidgrenClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewValley.Network;
diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs
index c0b247c8..ff871e64 100644
--- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs
+++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
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/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs
index 4c49e219..40adde8e 100644
--- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs
+++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Reflection;
using System.Reflection.Emit;
using Nanoray.Pintail;
diff --git a/src/SMAPI/Framework/Reflection/ReflectedField.cs b/src/SMAPI/Framework/Reflection/ReflectedField.cs
index 921876b9..a97ca3f0 100644
--- a/src/SMAPI/Framework/Reflection/ReflectedField.cs
+++ b/src/SMAPI/Framework/Reflection/ReflectedField.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Reflection;
@@ -15,8 +13,8 @@ namespace StardewModdingAPI.Framework.Reflection
/// <summary>The type that has the field.</summary>
private readonly Type ParentType;
- /// <summary>The object that has the instance field (if applicable).</summary>
- private readonly object Parent;
+ /// <summary>The object that has the instance field, or <c>null</c> for a static field.</summary>
+ private readonly object? Parent;
/// <summary>The display name shown in error messages.</summary>
private string DisplayName => $"{this.ParentType.FullName}::{this.FieldInfo.Name}";
@@ -34,12 +32,12 @@ namespace StardewModdingAPI.Framework.Reflection
*********/
/// <summary>Construct an instance.</summary>
/// <param name="parentType">The type that has the field.</param>
- /// <param name="obj">The object that has the instance field (if applicable).</param>
+ /// <param name="obj">The object that has the instance field, or <c>null</c> for a static field.</param>
/// <param name="field">The reflection metadata.</param>
/// <param name="isStatic">Whether the field is static.</param>
/// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="field"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static field, or not null for a static field.</exception>
- public ReflectedField(Type parentType, object obj, FieldInfo field, bool isStatic)
+ public ReflectedField(Type parentType, object? obj, FieldInfo field, bool isStatic)
{
// validate
if (parentType == null)
@@ -62,7 +60,7 @@ namespace StardewModdingAPI.Framework.Reflection
{
try
{
- return (TValue)this.FieldInfo.GetValue(this.Parent);
+ return (TValue)this.FieldInfo.GetValue(this.Parent)!;
}
catch (InvalidCastException)
{
diff --git a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs
index 50f89b40..a607141e 100644
--- a/src/SMAPI/Framework/Reflection/ReflectedMethod.cs
+++ b/src/SMAPI/Framework/Reflection/ReflectedMethod.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Reflection;
@@ -14,8 +12,8 @@ namespace StardewModdingAPI.Framework.Reflection
/// <summary>The type that has the method.</summary>
private readonly Type ParentType;
- /// <summary>The object that has the instance method (if applicable).</summary>
- private readonly object Parent;
+ /// <summary>The object that has the instance method, or <c>null</c> for a static method.</summary>
+ private readonly object? Parent;
/// <summary>The display name shown in error messages.</summary>
private string DisplayName => $"{this.ParentType.FullName}::{this.MethodInfo.Name}";
@@ -33,12 +31,12 @@ namespace StardewModdingAPI.Framework.Reflection
*********/
/// <summary>Construct an instance.</summary>
/// <param name="parentType">The type that has the method.</param>
- /// <param name="obj">The object that has the instance method(if applicable).</param>
+ /// <param name="obj">The object that has the instance method, or <c>null</c> for a static method.</param>
/// <param name="method">The reflection metadata.</param>
/// <param name="isStatic">Whether the method is static.</param>
/// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="method"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static method, or not null for a static method.</exception>
- public ReflectedMethod(Type parentType, object obj, MethodInfo method, bool isStatic)
+ public ReflectedMethod(Type parentType, object? obj, MethodInfo method, bool isStatic)
{
// validate
if (parentType == null)
@@ -57,10 +55,10 @@ namespace StardewModdingAPI.Framework.Reflection
}
/// <inheritdoc />
- public TValue Invoke<TValue>(params object[] arguments)
+ public TValue Invoke<TValue>(params object?[] arguments)
{
// invoke method
- object result;
+ object? result;
try
{
result = this.MethodInfo.Invoke(this.Parent, arguments);
@@ -77,7 +75,7 @@ namespace StardewModdingAPI.Framework.Reflection
// cast return value
try
{
- return (TValue)result;
+ return (TValue)result!;
}
catch (InvalidCastException)
{
@@ -86,7 +84,7 @@ namespace StardewModdingAPI.Framework.Reflection
}
/// <inheritdoc />
- public void Invoke(params object[] arguments)
+ public void Invoke(params object?[] arguments)
{
// invoke method
try
diff --git a/src/SMAPI/Framework/Reflection/ReflectedProperty.cs b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs
index a6d8c75c..72e701d1 100644
--- a/src/SMAPI/Framework/Reflection/ReflectedProperty.cs
+++ b/src/SMAPI/Framework/Reflection/ReflectedProperty.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Reflection;
@@ -16,10 +14,10 @@ namespace StardewModdingAPI.Framework.Reflection
private readonly string DisplayName;
/// <summary>The underlying property getter.</summary>
- private readonly Func<TValue> GetMethod;
+ private readonly Func<TValue>? GetMethod;
/// <summary>The underlying property setter.</summary>
- private readonly Action<TValue> SetMethod;
+ private readonly Action<TValue>? SetMethod;
/*********
@@ -34,12 +32,12 @@ namespace StardewModdingAPI.Framework.Reflection
*********/
/// <summary>Construct an instance.</summary>
/// <param name="parentType">The type that has the property.</param>
- /// <param name="obj">The object that has the instance property (if applicable).</param>
+ /// <param name="obj">The object that has the instance property, or <c>null</c> for a static property.</param>
/// <param name="property">The reflection metadata.</param>
/// <param name="isStatic">Whether the property is static.</param>
/// <exception cref="ArgumentNullException">The <paramref name="parentType"/> or <paramref name="property"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="obj"/> is null for a non-static property, or not null for a static property.</exception>
- public ReflectedProperty(Type parentType, object obj, PropertyInfo property, bool isStatic)
+ public ReflectedProperty(Type parentType, object? obj, PropertyInfo property, bool isStatic)
{
// validate input
if (parentType == null)
diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs
index d5938c3f..79575c26 100644
--- a/src/SMAPI/Framework/Reflection/Reflector.cs
+++ b/src/SMAPI/Framework/Reflection/Reflector.cs
@@ -1,7 +1,4 @@
-#nullable disable
-
using System;
-using System.Linq;
using System.Reflection;
using System.Runtime.Caching;
@@ -15,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);
@@ -31,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
@@ -40,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!;
}
/****
@@ -67,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
@@ -75,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!;
}
/****
@@ -100,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
@@ -109,58 +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;
- }
-
- /****
- ** Methods by signature
- ****/
- /// <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="argumentTypes">The argument types of the method signature to find.</param>
- /// <param name="required">Whether to throw an exception if the field is not found.</param>
- public IReflectedMethod GetMethod(object obj, string name, Type[] argumentTypes, bool required = true)
- {
- // validate parent
- if (obj == null)
- throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object.");
-
- // get method from hierarchy
- ReflectedMethod method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, argumentTypes);
- if (required && method == null)
- throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method with that signature.");
- 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="argumentTypes">The argument types of the method signature to find.</param>
- /// <param name="required">Whether to throw an exception if the field is not found.</param>
- public IReflectedMethod GetMethod(Type type, string name, Type[] argumentTypes, bool required = true)
- {
- // get field from hierarchy
- ReflectedMethod method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, argumentTypes);
- if (required && method == null)
- throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method with that signature.");
- return method;
+ return method!;
}
@@ -170,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 = curType.GetField(name, bindingFlags);
+ if (fieldInfo != null)
+ {
+ type = curType;
+ return fieldInfo;
+ }
+ }
+
+ return null;
});
return field != null
@@ -192,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 = curType.GetProperty(name, bindingFlags);
+ if (propertyInfo != null)
+ {
+ type = curType;
+ return propertyInfo;
+ }
+ }
+
+ return null;
});
return property != null
@@ -213,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 = curType.GetMethod(name, bindingFlags);
+ if (methodInfo != null)
+ {
+ type = curType;
+ return methodInfo;
+ }
+ }
+
+ return null;
});
return method != null
@@ -232,32 +226,12 @@ namespace StardewModdingAPI.Framework.Reflection
: null;
}
- /// <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="name">The method name.</param>
- /// <param name="bindingFlags">The reflection binding which flags which indicates what type of method to find.</param>
- /// <param name="argumentTypes">The argument types of the method signature to find.</param>
- private ReflectedMethod GetMethodFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags, Type[] argumentTypes)
- {
- bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
- MethodInfo method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}({string.Join(",", argumentTypes.Select(p => p.FullName))})", () =>
- {
- MethodInfo methodInfo = null;
- for (; type != null && methodInfo == null; type = type.BaseType)
- methodInfo = type.GetMethod(name, bindingFlags, null, argumentTypes, null);
- return methodInfo;
- });
- return method != null
- ? new ReflectedMethod(type, obj, method, isStatic)
- : null;
- }
-
/// <summary>Get a method or field through the cache.</summary>
/// <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))
@@ -269,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/Rendering/SDisplayDevice.cs b/src/SMAPI/Framework/Rendering/SDisplayDevice.cs
index 8718bcb1..37996b0f 100644
--- a/src/SMAPI/Framework/Rendering/SDisplayDevice.cs
+++ b/src/SMAPI/Framework/Rendering/SDisplayDevice.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
@@ -27,7 +25,7 @@ namespace StardewModdingAPI.Framework.Rendering
/// <param name="tile">The tile to draw.</param>
/// <param name="location">The tile position to draw.</param>
/// <param name="layerDepth">The layer depth at which to draw.</param>
- public override void DrawTile(Tile tile, Location location, float layerDepth)
+ public override void DrawTile(Tile? tile, Location location, float layerDepth)
{
// identical to XnaDisplayDevice
if (tile == null)
@@ -58,7 +56,7 @@ namespace StardewModdingAPI.Framework.Rendering
/// <param name="tile">The tile being drawn.</param>
private SpriteEffects GetSpriteEffects(Tile tile)
{
- return tile.Properties.TryGetValue("@Flip", out PropertyValue propertyValue) && int.TryParse(propertyValue, out int value)
+ return tile.Properties.TryGetValue("@Flip", out PropertyValue? propertyValue) && int.TryParse(propertyValue, out int value)
? (SpriteEffects)value
: SpriteEffects.None;
}
@@ -67,7 +65,7 @@ namespace StardewModdingAPI.Framework.Rendering
/// <param name="tile">The tile being drawn.</param>
private float GetRotation(Tile tile)
{
- if (!tile.Properties.TryGetValue("@Rotation", out PropertyValue propertyValue) || !int.TryParse(propertyValue, out int value))
+ if (!tile.Properties.TryGetValue("@Rotation", out PropertyValue? propertyValue) || !int.TryParse(propertyValue, out int value))
return 0;
value %= 360;
diff --git a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs
index 21edaedd..94b13378 100644
--- a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs
+++ b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -91,7 +89,7 @@ namespace StardewModdingAPI.Framework.Rendering
/// <param name="tile">The tile to draw.</param>
/// <param name="location">The tile position to draw.</param>
/// <param name="layerDepth">The layer depth at which to draw.</param>
- public virtual void DrawTile(Tile tile, Location location, float layerDepth)
+ public virtual void DrawTile(Tile? tile, Location location, float layerDepth)
{
if (tile == null)
return;
diff --git a/src/SMAPI/Framework/RequestExitDelegate.cs b/src/SMAPI/Framework/RequestExitDelegate.cs
deleted file mode 100644
index 93ef1cf9..00000000
--- a/src/SMAPI/Framework/RequestExitDelegate.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-#nullable disable
-
-namespace StardewModdingAPI.Framework
-{
- /// <summary>A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
- /// <param name="module">The module which requested an immediate exit.</param>
- /// <param name="reason">The reason provided for the shutdown.</param>
- internal delegate void RequestExitDelegate(string module, string reason);
-}
diff --git a/src/SMAPI/Framework/SChatBox.cs b/src/SMAPI/Framework/SChatBox.cs
index d6286c12..7d6f2e5f 100644
--- a/src/SMAPI/Framework/SChatBox.cs
+++ b/src/SMAPI/Framework/SChatBox.cs
@@ -1,11 +1,9 @@
-#nullable disable
-
using StardewValley;
using StardewValley.Menus;
namespace StardewModdingAPI.Framework
{
- /// <summary>SMAPI's implementation of the chatbox which intercepts errors for logging.</summary>
+ /// <summary>SMAPI's implementation of the chat box which intercepts errors for logging.</summary>
internal class SChatBox : ChatBox
{
/*********
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 1a58d84b..990fe5ea 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,13 +1592,13 @@ 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)
{
SCore.DeprecationManager.Warn(
- source: metadata.DisplayName,
+ source: metadata,
nounPhrase: $"{nameof(IAssetEditor)}",
version: "3.14.0",
severity: DeprecationLevel.Notice
@@ -1617,7 +1610,7 @@ namespace StardewModdingAPI.Framework
if (metadata.Mod is IAssetLoader loader)
{
SCore.DeprecationManager.Warn(
- source: metadata.DisplayName,
+ source: metadata,
nounPhrase: $"{nameof(IAssetLoader)}",
version: "3.14.0",
severity: DeprecationLevel.Notice
@@ -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,12 +1747,11 @@ 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);
- IModContentHelper modContentHelper = new ModContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache);
- TranslationHelper translationHelper = new(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
+ GameContentHelper gameContentHelper = new(this.ContentCore, mod, mod.DisplayName, monitor, this.Reflection);
+ IModContentHelper modContentHelper = new ModContentHelper(this.ContentCore, mod.DirectoryPath, mod, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache, this.Reflection);
+ TranslationHelper translationHelper = new(mod, contentCore.GetLocale(), contentCore.Language);
IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, modContentHelper, translationHelper, jsonHelper, relativePathCache);
mod.SetMod(contentPack, monitor, translationHelper);
this.ModRegistry.Add(mod);
@@ -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,14 +1816,14 @@ 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();
}
// init mod helpers
IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName);
- TranslationHelper translationHelper = new(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
+ TranslationHelper translationHelper = new(mod, contentCore.GetLocale(), contentCore.Language);
IModHelper modHelper;
{
IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest)
@@ -1838,9 +1832,9 @@ namespace StardewModdingAPI.Framework
CaseInsensitivePathCache relativePathCache = this.ContentCore.GetCaseInsensitivePathCache(packDirPath);
- GameContentHelper gameContentHelper = new(contentCore, packManifest.UniqueID, packManifest.Name, packMonitor);
- IModContentHelper packContentHelper = new ModContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, gameContentHelper.GetUnderlyingContentManager(), relativePathCache);
- TranslationHelper packTranslationHelper = new(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language);
+ GameContentHelper gameContentHelper = new(contentCore, mod, packManifest.Name, packMonitor, this.Reflection);
+ IModContentHelper packContentHelper = new ModContentHelper(contentCore, packDirPath, mod, packManifest.Name, gameContentHelper.GetUnderlyingContentManager(), relativePathCache, this.Reflection);
+ TranslationHelper packTranslationHelper = new(mod, contentCore.GetLocale(), contentCore.Language);
ContentPack contentPack = new(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper, relativePathCache);
this.ReloadTranslationsForTemporaryContentPack(mod, contentPack);
@@ -1852,17 +1846,17 @@ namespace StardewModdingAPI.Framework
ICommandHelper commandHelper = new CommandHelper(mod, this.CommandManager);
CaseInsensitivePathCache relativePathCache = this.ContentCore.GetCaseInsensitivePathCache(mod.DirectoryPath);
#pragma warning disable CS0612 // deprecated code
- ContentHelper contentHelper = new(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor);
+ ContentHelper contentHelper = new(contentCore, mod.DirectoryPath, mod, monitor, this.Reflection);
#pragma warning restore CS0612
- GameContentHelper gameContentHelper = new(contentCore, manifest.UniqueID, mod.DisplayName, monitor);
- IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache);
- IContentPackHelper contentPackHelper = new ContentPackHelper(manifest.UniqueID, new Lazy<IContentPack[]>(GetContentPacks), CreateFakeContentPack);
- IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper);
- IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection);
- IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor);
- IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.Multiplayer);
-
- modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, contentHelper, gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper);
+ GameContentHelper gameContentHelper = new(contentCore, mod, mod.DisplayName, monitor, this.Reflection);
+ IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, mod, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache, this.Reflection);
+ IContentPackHelper contentPackHelper = new ContentPackHelper(mod, new Lazy<IContentPack[]>(GetContentPacks), CreateFakeContentPack);
+ IDataHelper dataHelper = new DataHelper(mod, mod.DirectoryPath, jsonHelper);
+ IReflectionHelper reflectionHelper = new ReflectionHelper(mod, mod.DisplayName, this.Reflection);
+ IModRegistry modRegistryHelper = new ModRegistryHelper(mod, this.ModRegistry, proxyFactory, monitor);
+ IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(mod, this.Multiplayer);
+
+ modHelper = new ModHelper(mod, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, contentHelper, gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper);
}
// init mod
@@ -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/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 7ca89eec..0a8a068f 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -1,10 +1,7 @@
-#nullable disable
-
using System;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Text;
-using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Events;
@@ -48,10 +45,10 @@ namespace StardewModdingAPI.Framework
private readonly Action<string> ExitGameImmediately;
/// <summary>The initial override for <see cref="Input"/>. This value is null after initialization.</summary>
- private SInputState InitialInput;
+ private SInputState? InitialInput;
/// <summary>The initial override for <see cref="Multiplayer"/>. This value is null after initialization.</summary>
- private SMultiplayer InitialMultiplayer;
+ private SMultiplayer? InitialMultiplayer;
/// <summary>Raised when the instance is updating its state (roughly 60 times per second).</summary>
private readonly Action<SGame, GameTime, Action> OnUpdating;
@@ -66,11 +63,8 @@ namespace StardewModdingAPI.Framework
/// <summary>Manages input visible to the game.</summary>
public SInputState Input => (SInputState)Game1.input;
- /// <summary>The game background task which initializes a new day.</summary>
- public Task NewDayTask => Game1._newDayTask;
-
/// <summary>Monitors the entire game state for changes.</summary>
- public WatcherCore Watchers { get; private set; }
+ public WatcherCore Watchers { get; private set; } = null!; // initialized on first update tick
/// <summary>A snapshot of the current <see cref="Watchers"/> state.</summary>
public WatcherSnapshot WatcherSnapshot { get; } = new();
@@ -94,7 +88,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Construct a content manager to read game content files.</summary>
/// <remarks>This must be static because the game accesses it before the <see cref="SGame"/> constructor is called.</remarks>
[NonInstancedStatic]
- public static Func<IServiceProvider, string, LocalizedContentManager> CreateContentManagerImpl;
+ public static Func<IServiceProvider, string, LocalizedContentManager>? CreateContentManagerImpl;
/*********
@@ -138,11 +132,10 @@ namespace StardewModdingAPI.Framework
/// <remarks>This is intended for use by <see cref="Keybind"/> and shouldn't be used directly in most cases.</remarks>
internal static SButtonState GetInputState(SButton button)
{
- SInputState input = Game1.input as SInputState;
- if (input == null)
+ if (Game1.input is not SInputState inputHandler)
throw new InvalidOperationException("SMAPI's input state is not in a ready state yet.");
- return input.GetState(button);
+ return inputHandler.GetState(button);
}
/// <inheritdoc />
@@ -172,13 +165,11 @@ namespace StardewModdingAPI.Framework
{
base.Initialize();
- // The game resets public static fields after the class is constructed (see
- // GameRunner.SetInstanceDefaults), so SMAPI needs to re-override them here.
+ // The game resets public static fields after the class is constructed (see GameRunner.SetInstanceDefaults), so SMAPI needs to re-override them here.
Game1.input = this.InitialInput;
Game1.multiplayer = this.InitialMultiplayer;
- // The Initial* fields should no longer be used after this point, since mods may
- // further override them after initialization.
+ // The Initial* fields should no longer be used after this point, since mods may further override them after initialization.
this.InitialInput = null;
this.InitialMultiplayer = null;
}
@@ -251,6 +242,7 @@ namespace StardewModdingAPI.Framework
Context.IsInDrawLoop = false;
}
+#nullable disable
/// <summary>Replicate the game's draw logic with some changes for SMAPI.</summary>
/// <param name="gameTime">A snapshot of the game timing state.</param>
/// <param name="target_screen">The render target, if any.</param>
@@ -258,6 +250,7 @@ namespace StardewModdingAPI.Framework
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")]
+ [SuppressMessage("ReSharper", "MergeIntoPattern", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "RedundantArgumentDefaultValue", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = "copied from game code as-is")]
@@ -265,8 +258,9 @@ namespace StardewModdingAPI.Framework
[SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "IdentifierTypo", Justification = "copied from game code as-is")]
- [SuppressMessage("SMAPI.CommonErrors", "AvoidNetField", Justification = "copied from game code as-is")]
+ [SuppressMessage("ReSharper", "MergeIntoPattern", Justification = "copied from game code as-is")]
[SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")]
+ [SuppressMessage("SMAPI.CommonErrors", "AvoidNetField", Justification = "copied from game code as-is")]
private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen)
{
var events = this.Events;
@@ -952,5 +946,6 @@ namespace StardewModdingAPI.Framework
this.drawOverlays(Game1.spriteBatch);
Game1.PopUIMode();
}
+#nullable enable
}
}
diff --git a/src/SMAPI/Framework/SGameRunner.cs b/src/SMAPI/Framework/SGameRunner.cs
index dae314af..213fe561 100644
--- a/src/SMAPI/Framework/SGameRunner.cs
+++ b/src/SMAPI/Framework/SGameRunner.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -150,7 +148,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Update metadata when a split screen is added or removed.</summary>
private void UpdateForSplitScreenChanges()
{
- HashSet<int> oldScreenIds = new HashSet<int>(Context.ActiveScreenIds);
+ HashSet<int> oldScreenIds = new(Context.ActiveScreenIds);
// track active screens
Context.ActiveScreenIds.Clear();
diff --git a/src/SMAPI/Framework/SModHooks.cs b/src/SMAPI/Framework/SModHooks.cs
index 7941e102..a7736c8b 100644
--- a/src/SMAPI/Framework/SModHooks.cs
+++ b/src/SMAPI/Framework/SModHooks.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Threading.Tasks;
using StardewValley;
@@ -35,7 +33,7 @@ namespace StardewModdingAPI.Framework
/// <param name="action">The vanilla <see cref="Game1.newDayAfterFade"/> logic.</param>
public override void OnGame1_NewDayAfterFade(Action action)
{
- this.BeforeNewDayAfterFade?.Invoke();
+ this.BeforeNewDayAfterFade();
action();
}
diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs
index de3c25a5..e41e7edc 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/Singleton.cs b/src/SMAPI/Framework/Singleton.cs
index da16c48e..1bf318c4 100644
--- a/src/SMAPI/Framework/Singleton.cs
+++ b/src/SMAPI/Framework/Singleton.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Framework
{
/// <summary>Provides singleton instances of a given type.</summary>
diff --git a/src/SMAPI/Framework/SnapshotDiff.cs b/src/SMAPI/Framework/SnapshotDiff.cs
index eb2aebe1..d659d2b4 100644
--- a/src/SMAPI/Framework/SnapshotDiff.cs
+++ b/src/SMAPI/Framework/SnapshotDiff.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using StardewModdingAPI.Framework.StateTracking;
namespace StardewModdingAPI.Framework
@@ -15,10 +13,10 @@ namespace StardewModdingAPI.Framework
public bool IsChanged { get; private set; }
/// <summary>The previous value.</summary>
- public T Old { get; private set; }
+ public T? Old { get; private set; }
/// <summary>The current value.</summary>
- public T New { get; private set; }
+ public T? New { get; private set; }
/*********
diff --git a/src/SMAPI/Framework/SnapshotItemListDiff.cs b/src/SMAPI/Framework/SnapshotItemListDiff.cs
index 97942783..76060db2 100644
--- a/src/SMAPI/Framework/SnapshotItemListDiff.cs
+++ b/src/SMAPI/Framework/SnapshotItemListDiff.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Events;
using StardewValley;
@@ -48,7 +47,7 @@ namespace StardewModdingAPI.Framework
/// <param name="stackSizes">The items with their previous stack sizes.</param>
/// <param name="changes">The inventory changes, or <c>null</c> if nothing changed.</param>
/// <returns>Returns whether anything changed.</returns>
- public static bool TryGetChanges(ISet<Item> added, ISet<Item> removed, IDictionary<Item, int> stackSizes, out SnapshotItemListDiff changes)
+ public static bool TryGetChanges(ISet<Item> added, ISet<Item> removed, IDictionary<Item, int> stackSizes, [NotNullWhen(true)] out SnapshotItemListDiff? changes)
{
KeyValuePair<Item, int>[] sizesChanged = stackSizes.Where(p => p.Key.Stack != p.Value).ToArray();
if (sizesChanged.Any() || added.Any() || removed.Any())
diff --git a/src/SMAPI/Framework/SnapshotListDiff.cs b/src/SMAPI/Framework/SnapshotListDiff.cs
index 1d585c15..90066af1 100644
--- a/src/SMAPI/Framework/SnapshotListDiff.cs
+++ b/src/SMAPI/Framework/SnapshotListDiff.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using StardewModdingAPI.Framework.StateTracking;
@@ -39,7 +37,7 @@ namespace StardewModdingAPI.Framework
/// <param name="isChanged">Whether the value changed since the last update.</param>
/// <param name="removed">The removed values.</param>
/// <param name="added">The added values.</param>
- public void Update(bool isChanged, IEnumerable<T> removed, IEnumerable<T> added)
+ public void Update(bool isChanged, IEnumerable<T>? removed, IEnumerable<T>? added)
{
this.IsChanged = isChanged;
diff --git a/src/SMAPI/Framework/StateTracking/ChestTracker.cs b/src/SMAPI/Framework/StateTracking/ChestTracker.cs
index 28335200..c33a7498 100644
--- a/src/SMAPI/Framework/StateTracking/ChestTracker.cs
+++ b/src/SMAPI/Framework/StateTracking/ChestTracker.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Framework.StateTracking.Comparers;
using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
@@ -86,7 +85,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)
{
return SnapshotItemListDiff.TryGetChanges(added: this.Added, removed: this.Removed, stackSizes: this.StackSizes, out changes);
}
diff --git a/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs
index 987e1820..9d8559b4 100644
--- a/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs
+++ b/src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@@ -17,7 +15,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Comparers
/// <returns>true if the specified objects are equal; otherwise, false.</returns>
/// <param name="x">The first object to compare.</param>
/// <param name="y">The second object to compare.</param>
- public bool Equals(T x, T y)
+ public bool Equals(T? x, T? y)
{
if (x == null)
return y == null;
diff --git a/src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs
index f6b04583..41b17e10 100644
--- a/src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs
+++ b/src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@@ -16,7 +14,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Comparers
/// <returns>true if the specified objects are equal; otherwise, false.</returns>
/// <param name="x">The first object to compare.</param>
/// <param name="y">The second object to compare.</param>
- public bool Equals(T x, T y)
+ public bool Equals(T? x, T? y)
{
if (x == null)
return y == null;
diff --git a/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs b/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs
index 8d3a7eb9..e6ece854 100644
--- a/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs
+++ b/src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@@ -16,7 +14,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Comparers
/// <returns>true if the specified objects are equal; otherwise, false.</returns>
/// <param name="x">The first object to compare.</param>
/// <param name="y">The second object to compare.</param>
- public bool Equals(T x, T y)
+ public bool Equals(T? x, T? y)
{
return object.ReferenceEquals(x, y);
}
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs
index 03bf84d9..60006c51 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs
index 52e1dbad..256370ce 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
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/FieldWatchers/ImmutableCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ImmutableCollectionWatcher.cs
index 94ce0c8e..84340fbf 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ImmutableCollectionWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ImmutableCollectionWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs
index e662c433..676c9fb4 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using Netcode;
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs
index 0d7f2ad2..f55e4cea 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using Netcode;
@@ -12,6 +10,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <typeparam name="TSerialDict">The serializable dictionary type that can store the keys and values.</typeparam>
/// <typeparam name="TSelf">The net field instance type.</typeparam>
internal class NetDictionaryWatcher<TKey, TValue, TField, TSerialDict, TSelf> : BaseDisposableWatcher, IDictionaryWatcher<TKey, TValue>
+ where TKey : notnull
where TField : class, INetObject<INetSerializable>, new()
where TSerialDict : IDictionary<TKey, TValue>, new()
where TSelf : NetDictionary<TKey, TValue, TField, TSerialDict, TSelf>
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs
index a97e754c..0b4d3030 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using Netcode;
using StardewModdingAPI.Framework.StateTracking.Comparers;
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs
index 26641750..48d5d681 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Netcode;
namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs
index 82e5387e..97aedca8 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
@@ -81,7 +79,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <summary>A callback invoked when an entry is added or removed from the collection.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
- private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
@@ -90,8 +88,8 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
}
else
{
- TValue[] added = e.NewItems?.Cast<TValue>().ToArray();
- TValue[] removed = e.OldItems?.Cast<TValue>().ToArray();
+ TValue[]? added = e.NewItems?.Cast<TValue>().ToArray();
+ TValue[]? removed = e.OldItems?.Cast<TValue>().ToArray();
if (removed != null)
{
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
index 0b99914c..c4a4d0b9 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -20,7 +18,8 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <summary>Get a watcher which compares values using their <see cref="object.Equals(object)"/> method. This method should only be used when <see cref="ForEquatable{T}"/> won't work, since this doesn't validate whether they're comparable.</summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="getValue">Get the current value.</param>
- public static IValueWatcher<T> ForGenericEquality<T>(Func<T> getValue) where T : struct
+ public static IValueWatcher<T> ForGenericEquality<T>(Func<T> getValue)
+ where T : struct
{
return new ComparableWatcher<T>(getValue, new GenericEqualsComparer<T>());
}
@@ -28,7 +27,8 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <summary>Get a watcher for an <see cref="IEquatable{T}"/> value.</summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="getValue">Get the current value.</param>
- public static IValueWatcher<T> ForEquatable<T>(Func<T> getValue) where T : IEquatable<T>
+ public static IValueWatcher<T> ForEquatable<T>(Func<T> getValue)
+ where T : IEquatable<T>
{
return new ComparableWatcher<T>(getValue, new EquatableComparer<T>());
}
@@ -79,7 +79,8 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <summary>Get a watcher for a net collection.</summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="collection">The net collection.</param>
- public static ICollectionWatcher<T> ForNetCollection<T>(NetCollection<T> collection) where T : class, INetObject<INetSerializable>
+ public static ICollectionWatcher<T> ForNetCollection<T>(NetCollection<T> collection)
+ where T : class, INetObject<INetSerializable>
{
return new NetCollectionWatcher<T>(collection);
}
@@ -87,7 +88,8 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <summary>Get a watcher for a net list.</summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="collection">The net list.</param>
- public static ICollectionWatcher<T> ForNetList<T>(NetList<T, NetRef<T>> collection) where T : class, INetObject<INetSerializable>
+ public static ICollectionWatcher<T> ForNetList<T>(NetList<T, NetRef<T>> collection)
+ where T : class, INetObject<INetSerializable>
{
return new NetListWatcher<T>(collection);
}
@@ -100,6 +102,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <typeparam name="TSelf">The net field instance type.</typeparam>
/// <param name="field">The net field.</param>
public static NetDictionaryWatcher<TKey, TValue, TField, TSerialDict, TSelf> ForNetDictionary<TKey, TValue, TField, TSerialDict, TSelf>(NetDictionary<TKey, TValue, TField, TSerialDict, TSelf> field)
+ where TKey : notnull
where TField : class, INetObject<INetSerializable>, new()
where TSerialDict : IDictionary<TKey, TValue>, new()
where TSelf : NetDictionary<TKey, TValue, TField, TSerialDict, TSelf>
diff --git a/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs
index 74c9313b..7a7759e3 100644
--- a/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
namespace StardewModdingAPI.Framework.StateTracking
diff --git a/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs b/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs
index 81fb7460..691ed377 100644
--- a/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
namespace StardewModdingAPI.Framework.StateTracking
diff --git a/src/SMAPI/Framework/StateTracking/IValueWatcher.cs b/src/SMAPI/Framework/StateTracking/IValueWatcher.cs
index 7d46053c..4afca972 100644
--- a/src/SMAPI/Framework/StateTracking/IValueWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/IValueWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Framework.StateTracking
{
/// <summary>A watcher which tracks changes to a value.</summary>
diff --git a/src/SMAPI/Framework/StateTracking/IWatcher.cs b/src/SMAPI/Framework/StateTracking/IWatcher.cs
index 3603b6f8..8c7fa51c 100644
--- a/src/SMAPI/Framework/StateTracking/IWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/IWatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Framework.StateTracking
diff --git a/src/SMAPI/Framework/StateTracking/LocationTracker.cs b/src/SMAPI/Framework/StateTracking/LocationTracker.cs
index 9c2ff7f0..ff72a19b 100644
--- a/src/SMAPI/Framework/StateTracking/LocationTracker.cs
+++ b/src/SMAPI/Framework/StateTracking/LocationTracker.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -132,20 +130,20 @@ namespace StardewModdingAPI.Framework.StateTracking
private void UpdateChestWatcherList(IEnumerable<KeyValuePair<Vector2, SObject>> added, IEnumerable<KeyValuePair<Vector2, SObject>> removed)
{
// remove unused watchers
- foreach (KeyValuePair<Vector2, SObject> pair in removed)
+ foreach ((Vector2 tile, SObject? obj) in removed)
{
- if (pair.Value is Chest && this.ChestWatchers.TryGetValue(pair.Key, out ChestTracker watcher))
+ if (obj is Chest && this.ChestWatchers.TryGetValue(tile, out ChestTracker? watcher))
{
watcher.Dispose();
- this.ChestWatchers.Remove(pair.Key);
+ this.ChestWatchers.Remove(tile);
}
}
// add new watchers
- foreach (KeyValuePair<Vector2, SObject> pair in added)
+ foreach ((Vector2 tile, SObject? obj) in added)
{
- if (pair.Value is Chest chest && !this.ChestWatchers.ContainsKey(pair.Key))
- this.ChestWatchers.Add(pair.Key, new ChestTracker(chest));
+ if (obj is Chest chest && !this.ChestWatchers.ContainsKey(tile))
+ this.ChestWatchers.Add(tile, new ChestTracker(chest));
}
}
}
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/LocationSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs
index 3d13f92b..0d0469d7 100644
--- a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs
+++ b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewValley;
@@ -70,7 +68,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots
this.ChestItems.Clear();
foreach (ChestTracker tracker in watcher.ChestWatchers.Values)
{
- if (tracker.TryGetInventoryChanges(out SnapshotItemListDiff changes))
+ if (tracker.TryGetInventoryChanges(out SnapshotItemListDiff? changes))
this.ChestItems[tracker.Chest] = changes;
}
}
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/StateTracking/Snapshots/WatcherSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs
index 1d43ef26..27a891de 100644
--- a/src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs
+++ b/src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Menus;
@@ -16,7 +14,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots
public SnapshotDiff<Point> WindowSize { get; } = new();
/// <summary>Tracks changes to the current player.</summary>
- public PlayerSnapshot CurrentPlayer { get; private set; }
+ public PlayerSnapshot? CurrentPlayer { get; private set; }
/// <summary>Tracks changes to the time of day (in 24-hour military format).</summary>
public SnapshotDiff<int> Time { get; } = new();
@@ -56,7 +54,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots
// update snapshots
this.WindowSize.Update(watchers.WindowSizeWatcher);
this.Locale.Update(watchers.LocaleWatcher);
- this.CurrentPlayer?.Update(watchers.CurrentPlayerTracker);
+ this.CurrentPlayer?.Update(watchers.CurrentPlayerTracker!);
this.Time.Update(watchers.TimeWatcher);
this.SaveID.Update(watchers.SaveIdWatcher);
this.Locations.Update(watchers.LocationsWatcher);
diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs
index 88aac0df..59f94942 100644
--- a/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs
+++ b/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using StardewModdingAPI.Framework.StateTracking.Comparers;
@@ -44,7 +42,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots
// update locations
foreach (LocationTracker locationWatcher in watcher.Locations)
{
- if (!this.LocationsDict.TryGetValue(locationWatcher.Location, out LocationSnapshot snapshot))
+ if (!this.LocationsDict.TryGetValue(locationWatcher.Location, out LocationSnapshot? snapshot))
this.LocationsDict[locationWatcher.Location] = snapshot = new LocationSnapshot(locationWatcher.Location);
snapshot.Update(locationWatcher);
diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
index ab02d7d5..817a6011 100644
--- a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
+++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
@@ -27,10 +25,10 @@ namespace StardewModdingAPI.Framework.StateTracking
private readonly ICollectionWatcher<GameLocation> VolcanoLocationListWatcher;
/// <summary>A lookup of the tracked locations.</summary>
- private IDictionary<GameLocation, LocationTracker> LocationDict { get; } = new Dictionary<GameLocation, LocationTracker>(new ObjectReferenceComparer<GameLocation>());
+ private Dictionary<GameLocation, LocationTracker> LocationDict { get; } = new(new ObjectReferenceComparer<GameLocation>());
/// <summary>A lookup of registered buildings and their indoor location.</summary>
- private readonly IDictionary<Building, GameLocation> BuildingIndoors = new Dictionary<Building, GameLocation>(new ObjectReferenceComparer<Building>());
+ private readonly Dictionary<Building, GameLocation?> BuildingIndoors = new(new ObjectReferenceComparer<Building>());
/*********
@@ -101,10 +99,9 @@ namespace StardewModdingAPI.Framework.StateTracking
}
// detect building interiors changed (e.g. construction completed)
- foreach (KeyValuePair<Building, GameLocation> pair in this.BuildingIndoors.Where(p => !object.Equals(p.Key.indoors.Value, p.Value)))
+ foreach ((Building building, GameLocation? oldIndoors) in this.BuildingIndoors.Where(p => !object.Equals(p.Key.indoors.Value, p.Value)))
{
- GameLocation oldIndoors = pair.Value;
- GameLocation newIndoors = pair.Key.indoors.Value;
+ GameLocation? newIndoors = building.indoors.Value;
if (oldIndoors != null)
this.Added.Add(oldIndoors);
@@ -189,19 +186,19 @@ namespace StardewModdingAPI.Framework.StateTracking
****/
/// <summary>Add the given building.</summary>
/// <param name="building">The building to add.</param>
- public void Add(Building building)
+ public void Add(Building? building)
{
if (building == null)
return;
- GameLocation indoors = building.indoors.Value;
+ GameLocation? indoors = building.indoors.Value;
this.BuildingIndoors[building] = indoors;
this.Add(indoors);
}
/// <summary>Add the given location.</summary>
/// <param name="location">The location to add.</param>
- public void Add(GameLocation location)
+ public void Add(GameLocation? location)
{
if (location == null)
return;
@@ -220,7 +217,7 @@ namespace StardewModdingAPI.Framework.StateTracking
/// <summary>Remove the given building.</summary>
/// <param name="building">The building to remove.</param>
- public void Remove(Building building)
+ public void Remove(Building? building)
{
if (building == null)
return;
@@ -231,12 +228,12 @@ namespace StardewModdingAPI.Framework.StateTracking
/// <summary>Remove the given location.</summary>
/// <param name="location">The location to remove.</param>
- public void Remove(GameLocation location)
+ public void Remove(GameLocation? location)
{
if (location == null)
return;
- if (this.LocationDict.TryGetValue(location, out LocationTracker watcher))
+ if (this.LocationDict.TryGetValue(location, out LocationTracker? watcher))
{
// track change
this.Removed.Add(location);
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;
+ }
}
}
}
diff --git a/src/SMAPI/Framework/Translator.cs b/src/SMAPI/Framework/Translator.cs
index 144b043c..b230a727 100644
--- a/src/SMAPI/Framework/Translator.cs
+++ b/src/SMAPI/Framework/Translator.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley;
@@ -23,7 +22,7 @@ namespace StardewModdingAPI.Framework
/*********
** Accessors
*********/
- /// <summary>The current locale.</summary>
+ /// <summary>The current locale code like <c>fr-FR</c>, or an empty string for English.</summary>
public string Locale { get; private set; }
/// <summary>The game's current language code.</summary>
@@ -39,9 +38,10 @@ namespace StardewModdingAPI.Framework
this.SetLocale(string.Empty, LocalizedContentManager.LanguageCode.en);
}
- /// <summary>Set the current locale and precache translations.</summary>
+ /// <summary>Set the current locale and pre-cache translations.</summary>
/// <param name="locale">The current locale.</param>
/// <param name="localeEnum">The game's current language code.</param>
+ [MemberNotNull(nameof(Translator.ForLocale), nameof(Translator.Locale))]
public void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum)
{
this.Locale = locale.ToLower().Trim();
@@ -50,7 +50,7 @@ namespace StardewModdingAPI.Framework
this.ForLocale = new Dictionary<string, Translation>(StringComparer.OrdinalIgnoreCase);
foreach (string key in this.GetAllKeysRaw())
{
- string text = this.GetRaw(key, locale, withFallback: true);
+ string? text = this.GetRaw(key, locale, withFallback: true);
this.ForLocale.Add(key, new Translation(this.Locale, key, text));
}
}
@@ -65,14 +65,14 @@ namespace StardewModdingAPI.Framework
/// <param name="key">The translation key.</param>
public Translation Get(string key)
{
- this.ForLocale.TryGetValue(key, out Translation translation);
+ this.ForLocale.TryGetValue(key, out Translation? translation);
return translation ?? new Translation(this.Locale, key, null);
}
/// <summary>Get a translation for the current locale.</summary>
/// <param name="key">The translation key.</param>
/// <param name="tokens">An object containing token key/value pairs. This can be an anonymous object (like <c>new { value = 42, name = "Cranberries" }</c>), a dictionary, or a class instance.</param>
- public Translation Get(string key, object tokens)
+ public Translation Get(string key, object? tokens)
{
return this.Get(key).Tokens(tokens);
}
@@ -87,7 +87,7 @@ namespace StardewModdingAPI.Framework
foreach (var localeSet in this.All)
{
string locale = localeSet.Key;
- string text = this.GetRaw(key, locale, withFallback);
+ string? text = this.GetRaw(key, locale, withFallback);
if (text != null)
translations[locale] = new Translation(locale, key, text);
@@ -128,13 +128,13 @@ namespace StardewModdingAPI.Framework
/// <param name="key">The translation key.</param>
/// <param name="locale">The locale to get.</param>
/// <param name="withFallback">Whether to add duplicate translations for locale fallback. For example, if a translation is defined in <c>default.json</c> but not <c>fr.json</c>, setting this to true will add a <c>fr</c> entry which duplicates the default text.</param>
- private string GetRaw(string key, string locale, bool withFallback)
+ private string? GetRaw(string key, string locale, bool withFallback)
{
foreach (string next in this.GetRelevantLocales(locale))
{
- string translation = null;
+ string? translation = null;
bool hasTranslation =
- this.All.TryGetValue(next, out IDictionary<string, string> translations)
+ this.All.TryGetValue(next, out IDictionary<string, string>? translations)
&& translations.TryGetValue(key, out translation);
if (hasTranslation)
diff --git a/src/SMAPI/Framework/Utilities/ContextHash.cs b/src/SMAPI/Framework/Utilities/ContextHash.cs
index 46b9099e..6c0fdc90 100644
--- a/src/SMAPI/Framework/Utilities/ContextHash.cs
+++ b/src/SMAPI/Framework/Utilities/ContextHash.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
diff --git a/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs b/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs
index 94ce0069..20d206e2 100644
--- a/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs
+++ b/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
@@ -9,6 +7,7 @@ namespace StardewModdingAPI.Framework.Utilities
/// <typeparam name="TKey">The dictionary key type.</typeparam>
/// <typeparam name="TValue">The dictionary value type.</typeparam>
internal class TickCacheDictionary<TKey, TValue>
+ where TKey : notnull
{
/*********
** Fields
@@ -36,7 +35,7 @@ namespace StardewModdingAPI.Framework.Utilities
}
// fetch value
- if (!this.Cache.TryGetValue(cacheKey, out TValue cached))
+ if (!this.Cache.TryGetValue(cacheKey, out TValue? cached))
this.Cache[cacheKey] = cached = get();
return cached;
}
diff --git a/src/SMAPI/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs
index bd8d3367..5e20ac7b 100644
--- a/src/SMAPI/Framework/WatcherCore.cs
+++ b/src/SMAPI/Framework/WatcherCore.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.Xna.Framework;
@@ -29,7 +27,7 @@ namespace StardewModdingAPI.Framework
public readonly IValueWatcher<Point> WindowSizeWatcher;
/// <summary>Tracks changes to the current player.</summary>
- public PlayerTracker CurrentPlayerTracker;
+ public PlayerTracker? CurrentPlayerTracker;
/// <summary>Tracks changes to the time of day (in 24-hour military format).</summary>
public readonly IValueWatcher<int> TimeWatcher;