diff options
Diffstat (limited to 'src/SMAPI/Framework')
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; |