summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets4
-rw-r--r--docs/release-notes.md9
-rw-r--r--docs/technical/mod-package.md5
-rw-r--r--src/SMAPI.Installer/Framework/InstallerContext.cs4
-rw-r--r--src/SMAPI.Installer/Framework/InstallerPaths.cs2
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs81
-rw-r--r--src/SMAPI.Installer/Program.cs8
-rw-r--r--src/SMAPI.Internal.Patching/BasePatcher.cs6
-rw-r--r--src/SMAPI.Internal.Patching/HarmonyPatcher.cs2
-rw-r--r--src/SMAPI.Internal.Patching/IPatcher.cs2
-rw-r--r--src/SMAPI.Internal.Patching/PatchHelper.cs8
-rw-r--r--src/SMAPI.Internal/ConsoleWriting/ColorSchemeConfig.cs22
-rw-r--r--src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs19
-rw-r--r--src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs2
-rw-r--r--src/SMAPI.Internal/ExceptionHelper.cs6
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticResult.cs5
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.Helper.cs29
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs31
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetCollection.cs6
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetFieldBase.cs9
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetInt.cs2
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetList.cs5
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetObjectList.cs2
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs6
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs3
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Object.cs2
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs2
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs2
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs8
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs2
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs2
-rw-r--r--src/SMAPI.ModBuildConfig/DeployModTask.cs2
-rw-r--r--src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs2
-rw-r--r--src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs2
-rw-r--r--src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj8
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs7
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/IConsoleCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs4
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs14
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs9
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs6
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs14
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs6
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs4
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs23
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs4
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs42
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/ModEntry.cs14
-rw-r--r--src/SMAPI.Mods.ErrorHandler/ModEntry.cs6
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs10
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs9
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs2
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs8
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs10
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs6
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs4
-rw-r--r--src/SMAPI.Mods.SaveBackup/ModEntry.cs11
-rw-r--r--src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs2
-rw-r--r--src/SMAPI.Tests.ModApiConsumer/Interfaces/ISimpleApi.cs2
-rw-r--r--src/SMAPI.Tests.ModApiProvider/Framework/BaseApi.cs4
-rw-r--r--src/SMAPI.Tests.ModApiProvider/Framework/SimpleApi.cs12
-rw-r--r--src/SMAPI.Tests.ModApiProvider/ProviderMod.cs2
-rw-r--r--src/SMAPI.Tests/Core/AssetNameTests.cs10
-rw-r--r--src/SMAPI.Tests/Core/InterfaceProxyTests.cs12
-rw-r--r--src/SMAPI.Tests/Core/ModResolverTests.cs31
-rw-r--r--src/SMAPI.Tests/Core/TranslationTests.cs83
-rw-r--r--src/SMAPI.Tests/Sample.cs2
-rw-r--r--src/SMAPI.Tests/Utilities/KeybindListTests.cs22
-rw-r--r--src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs183
-rw-r--r--src/SMAPI.Tests/Utilities/SDateTests.cs19
-rw-r--r--src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs24
-rw-r--r--src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs4
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs19
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs9
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs36
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs27
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs10
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs5
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs16
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs159
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs33
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs2
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs96
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs23
-rw-r--r--src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs2
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs4
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs10
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs32
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs12
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs28
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs14
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs4
-rw-r--r--src/SMAPI.Toolkit/Properties/AssemblyInfo.cs1
-rw-r--r--src/SMAPI.Toolkit/SemanticVersion.cs3
-rw-r--r--src/SMAPI.Toolkit/SemanticVersionComparer.cs2
-rw-r--r--src/SMAPI.Toolkit/Serialization/Models/Manifest.cs2
-rw-r--r--src/SMAPI.Web/BackgroundService.cs24
-rw-r--r--src/SMAPI.Web/Controllers/IndexController.cs12
-rw-r--r--src/SMAPI.Web/Controllers/JsonValidatorController.cs46
-rw-r--r--src/SMAPI.Web/Controllers/LogParserController.cs6
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs70
-rw-r--r--src/SMAPI.Web/Controllers/ModsController.cs7
-rw-r--r--src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs4
-rw-r--r--src/SMAPI.Web/Framework/Caching/Cached.cs11
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs5
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs5
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs9
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs11
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs11
-rw-r--r--src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs12
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs14
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs22
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs30
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModDownload.cs13
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModPage.cs23
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs26
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs30
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs26
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs36
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs26
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs6
-rw-r--r--src/SMAPI.Web/Framework/Clients/IModSiteClient.cs4
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs20
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs42
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs24
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs7
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs22
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/DisabledNexusClient.cs31
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs63
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs43
-rw-r--r--src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs2
-rw-r--r--src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs28
-rw-r--r--src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs14
-rw-r--r--src/SMAPI.Web/Framework/Compression/GzipHelper.cs9
-rw-r--r--src/SMAPI.Web/Framework/Compression/IGzipHelper.cs5
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs36
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs6
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs10
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs6
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs8
-rw-r--r--src/SMAPI.Web/Framework/Extensions.cs8
-rw-r--r--src/SMAPI.Web/Framework/IModDownload.cs9
-rw-r--r--src/SMAPI.Web/Framework/IModPage.cs18
-rw-r--r--src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs2
-rw-r--r--src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs2
-rw-r--r--src/SMAPI.Web/Framework/ModInfoModel.cs33
-rw-r--r--src/SMAPI.Web/Framework/ModSiteManager.cs58
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs10
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs6
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs6
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs6
-rw-r--r--src/SMAPI.Web/Framework/Storage/IStorageProvider.cs2
-rw-r--r--src/SMAPI.Web/Framework/Storage/StorageProvider.cs52
-rw-r--r--src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs41
-rw-r--r--src/SMAPI.Web/Framework/Storage/UploadResult.cs14
-rw-r--r--src/SMAPI.Web/Framework/VersionConstraint.cs6
-rw-r--r--src/SMAPI.Web/Program.cs2
-rw-r--r--src/SMAPI.Web/Startup.cs27
-rw-r--r--src/SMAPI.Web/ViewModels/IndexModel.cs13
-rw-r--r--src/SMAPI.Web/ViewModels/IndexVersionModel.cs15
-rw-r--r--src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs15
-rw-r--r--src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs29
-rw-r--r--src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs19
-rw-r--r--src/SMAPI.Web/ViewModels/LogParserModel.cs2
-rw-r--r--src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs27
-rw-r--r--src/SMAPI.Web/ViewModels/ModLinkModel.cs6
-rw-r--r--src/SMAPI.Web/ViewModels/ModListModel.cs19
-rw-r--r--src/SMAPI.Web/ViewModels/ModModel.cs65
-rw-r--r--src/SMAPI.Web/Views/Index/Index.cshtml6
-rw-r--r--src/SMAPI.Web/Views/Index/Privacy.cshtml4
-rw-r--r--src/SMAPI.Web/Views/JsonValidator/Index.cshtml12
-rw-r--r--src/SMAPI.Web/Views/LogParser/Index.cshtml2
-rw-r--r--src/SMAPI.Web/Views/Mods/Index.cshtml6
-rw-r--r--src/SMAPI.Web/Views/Shared/_Layout.cshtml4
-rw-r--r--src/SMAPI.Web/Views/_ViewStart.cshtml4
-rw-r--r--src/SMAPI.Web/appsettings.json3
-rw-r--r--src/SMAPI.sln.DotSettings16
-rw-r--r--src/SMAPI/Constants.cs28
-rw-r--r--src/SMAPI/Context.cs2
-rw-r--r--src/SMAPI/Events/AssetReadyEventArgs.cs2
-rw-r--r--src/SMAPI/Events/AssetRequestedEventArgs.cs14
-rw-r--r--src/SMAPI/Events/AssetsInvalidatedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/BuildingListChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/ButtonPressedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/ButtonReleasedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/ButtonsChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/ChangeType.cs15
-rw-r--r--src/SMAPI/Events/ChestInventoryChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/CursorMovedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/DebrisListChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/FurnitureListChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/IContentEvents.cs2
-rw-r--r--src/SMAPI/Events/IDisplayEvents.cs2
-rw-r--r--src/SMAPI/Events/IGameLoopEvents.cs2
-rw-r--r--src/SMAPI/Events/IInputEvents.cs2
-rw-r--r--src/SMAPI/Events/IModEvents.cs2
-rw-r--r--src/SMAPI/Events/IMultiplayerEvents.cs2
-rw-r--r--src/SMAPI/Events/IPlayerEvents.cs2
-rw-r--r--src/SMAPI/Events/ISpecialisedEvents.cs2
-rw-r--r--src/SMAPI/Events/IWorldEvents.cs2
-rw-r--r--src/SMAPI/Events/InventoryChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/ItemStackSizeChange.cs2
-rw-r--r--src/SMAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/LevelChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/LocaleChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/LocationListChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/MenuChangedEventArgs.cs16
-rw-r--r--src/SMAPI/Events/ModMessageReceivedEventArgs.cs6
-rw-r--r--src/SMAPI/Events/NpcListChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/ObjectListChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/PeerConnectedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/PeerContextReceivedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/PeerDisconnectedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/RenderedActiveMenuEventArgs.cs2
-rw-r--r--src/SMAPI/Events/RenderedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/RenderedHudEventArgs.cs2
-rw-r--r--src/SMAPI/Events/RenderedWorldEventArgs.cs2
-rw-r--r--src/SMAPI/Events/RenderingActiveMenuEventArgs.cs2
-rw-r--r--src/SMAPI/Events/RenderingEventArgs.cs2
-rw-r--r--src/SMAPI/Events/RenderingHudEventArgs.cs2
-rw-r--r--src/SMAPI/Events/RenderingWorldEventArgs.cs2
-rw-r--r--src/SMAPI/Events/TerrainFeatureListChangedEventArgs.cs2
-rw-r--r--src/SMAPI/Events/WarpedEventArgs.cs2
-rw-r--r--src/SMAPI/Framework/Command.cs6
-rw-r--r--src/SMAPI/Framework/CommandManager.cs37
-rw-r--r--src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs20
-rw-r--r--src/SMAPI/Framework/Commands/HelpCommand.cs8
-rw-r--r--src/SMAPI/Framework/Commands/IInternalCommand.cs2
-rw-r--r--src/SMAPI/Framework/Commands/ReloadI18nCommand.cs2
-rw-r--r--src/SMAPI/Framework/Content/AssetData.cs7
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForDictionary.cs4
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs4
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForMap.cs75
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForObject.cs25
-rw-r--r--src/SMAPI/Framework/Content/AssetEditOperation.cs6
-rw-r--r--src/SMAPI/Framework/Content/AssetInfo.cs16
-rw-r--r--src/SMAPI/Framework/Content/AssetInterceptorChange.cs7
-rw-r--r--src/SMAPI/Framework/Content/AssetLoadOperation.cs6
-rw-r--r--src/SMAPI/Framework/Content/AssetName.cs21
-rw-r--r--src/SMAPI/Framework/Content/AssetOperationGroup.cs2
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs17
-rw-r--r--src/SMAPI/Framework/Content/TilesheetReference.cs2
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs38
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs28
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs40
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs13
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs21
-rw-r--r--src/SMAPI/Framework/ContentPack.cs9
-rw-r--r--src/SMAPI/Framework/CursorPosition.cs4
-rw-r--r--src/SMAPI/Framework/DeprecationManager.cs85
-rw-r--r--src/SMAPI/Framework/DeprecationWarning.cs17
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs2
-rw-r--r--src/SMAPI/Framework/Events/IManagedEvent.cs2
-rw-r--r--src/SMAPI/Framework/Events/ManagedEvent.cs8
-rw-r--r--src/SMAPI/Framework/Events/ManagedEventHandler.cs4
-rw-r--r--src/SMAPI/Framework/Events/ModContentEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModDisplayEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModEventsBase.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModGameLoopEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModInputEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModMultiplayerEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModPlayerEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModSpecialisedEvents.cs2
-rw-r--r--src/SMAPI/Framework/Events/ModWorldEvents.cs2
-rw-r--r--src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs2
-rw-r--r--src/SMAPI/Framework/Exceptions/SContentLoadException.cs4
-rw-r--r--src/SMAPI/Framework/GameVersion.cs10
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs26
-rw-r--r--src/SMAPI/Framework/Input/GamePadStateBuilder.cs9
-rw-r--r--src/SMAPI/Framework/Input/IInputStateBuilder.cs2
-rw-r--r--src/SMAPI/Framework/Input/KeyboardStateBuilder.cs4
-rw-r--r--src/SMAPI/Framework/Input/MouseStateBuilder.cs2
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs6
-rw-r--r--src/SMAPI/Framework/InternalExtensions.cs7
-rw-r--r--src/SMAPI/Framework/Logging/InterceptingTextWriter.cs24
-rw-r--r--src/SMAPI/Framework/Logging/LogFileManager.cs4
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs34
-rw-r--r--src/SMAPI/Framework/ModHelpers/BaseHelper.cs17
-rw-r--r--src/SMAPI/Framework/ModHelpers/CommandHelper.cs10
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs46
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs8
-rw-r--r--src/SMAPI/Framework/ModHelpers/DataHelper.cs34
-rw-r--r--src/SMAPI/Framework/ModHelpers/GameContentHelper.cs30
-rw-r--r--src/SMAPI/Framework/ModHelpers/InputHelper.cs8
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModContentHelper.cs28
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModHelper.cs12
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs19
-rw-r--r--src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs14
-rw-r--r--src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs32
-rw-r--r--src/SMAPI/Framework/ModHelpers/TranslationHelper.cs12
-rw-r--r--src/SMAPI/Framework/ModLinked.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs8
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs35
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs15
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs15
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs13
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs8
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs25
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs10
-rw-r--r--src/SMAPI/Framework/ModLoading/IInstructionHandler.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/InvalidModStateException.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs38
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs91
-rw-r--r--src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs9
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs5
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/ArchitectureAssemblyRewriter.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs20
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HarmonyRewriter.cs16
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs19
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs15
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs13
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Symbols/SymbolReader.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/Symbols/SymbolReaderProvider.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Symbols/SymbolWriterProvider.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs14
-rw-r--r--src/SMAPI/Framework/ModRegistry.cs34
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs78
-rw-r--r--src/SMAPI/Framework/Monitor.cs2
-rw-r--r--src/SMAPI/Framework/Networking/ModMessageModel.cs21
-rw-r--r--src/SMAPI/Framework/Networking/MultiplayerPeer.cs16
-rw-r--r--src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs5
-rw-r--r--src/SMAPI/Framework/Networking/RemoteContextModModel.cs30
-rw-r--r--src/SMAPI/Framework/Networking/RemoteContextModel.cs33
-rw-r--r--src/SMAPI/Framework/Networking/SGalaxyNetClient.cs2
-rw-r--r--src/SMAPI/Framework/Networking/SGalaxyNetServer.cs2
-rw-r--r--src/SMAPI/Framework/Networking/SLidgrenClient.cs2
-rw-r--r--src/SMAPI/Framework/Networking/SLidgrenServer.cs2
-rw-r--r--src/SMAPI/Framework/Reflection/CacheEntry.cs12
-rw-r--r--src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs2
-rw-r--r--src/SMAPI/Framework/Reflection/ReflectedField.cs12
-rw-r--r--src/SMAPI/Framework/Reflection/ReflectedMethod.cs18
-rw-r--r--src/SMAPI/Framework/Reflection/ReflectedProperty.cs10
-rw-r--r--src/SMAPI/Framework/Reflection/Reflector.cs184
-rw-r--r--src/SMAPI/Framework/Rendering/SDisplayDevice.cs8
-rw-r--r--src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs4
-rw-r--r--src/SMAPI/Framework/RequestExitDelegate.cs9
-rw-r--r--src/SMAPI/Framework/SChatBox.cs4
-rw-r--r--src/SMAPI/Framework/SCore.cs210
-rw-r--r--src/SMAPI/Framework/SGame.cs31
-rw-r--r--src/SMAPI/Framework/SGameRunner.cs4
-rw-r--r--src/SMAPI/Framework/SModHooks.cs4
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs89
-rw-r--r--src/SMAPI/Framework/Serialization/KeybindConverter.cs4
-rw-r--r--src/SMAPI/Framework/Singleton.cs2
-rw-r--r--src/SMAPI/Framework/SnapshotDiff.cs6
-rw-r--r--src/SMAPI/Framework/SnapshotItemListDiff.cs5
-rw-r--r--src/SMAPI/Framework/SnapshotListDiff.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/ChestTracker.cs5
-rw-r--r--src/SMAPI/Framework/StateTracking/Comparers/EquatableComparer.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs5
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ImmutableCollectionWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs3
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetListWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs8
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs15
-rw-r--r--src/SMAPI/Framework/StateTracking/ICollectionWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/IDictionaryWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/IValueWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/IWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/LocationTracker.cs14
-rw-r--r--src/SMAPI/Framework/StateTracking/PlayerTracker.cs16
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs11
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs6
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs4
-rw-r--r--src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs23
-rw-r--r--src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs122
-rw-r--r--src/SMAPI/Framework/Translator.cs22
-rw-r--r--src/SMAPI/Framework/Utilities/ContextHash.cs2
-rw-r--r--src/SMAPI/Framework/Utilities/TickCacheDictionary.cs5
-rw-r--r--src/SMAPI/Framework/WatcherCore.cs4
-rw-r--r--src/SMAPI/GamePlatform.cs2
-rw-r--r--src/SMAPI/IAssetData.cs3
-rw-r--r--src/SMAPI/IAssetDataForDictionary.cs2
-rw-r--r--src/SMAPI/IAssetDataForImage.cs6
-rw-r--r--src/SMAPI/IAssetDataForMap.cs9
-rw-r--r--src/SMAPI/IAssetEditor.cs2
-rw-r--r--src/SMAPI/IAssetInfo.cs4
-rw-r--r--src/SMAPI/IAssetLoader.cs2
-rw-r--r--src/SMAPI/IAssetName.cs12
-rw-r--r--src/SMAPI/ICommandHelper.cs2
-rw-r--r--src/SMAPI/IContentHelper.cs14
-rw-r--r--src/SMAPI/IContentPack.cs11
-rw-r--r--src/SMAPI/IContentPackHelper.cs2
-rw-r--r--src/SMAPI/ICursorPosition.cs2
-rw-r--r--src/SMAPI/IDataHelper.cs20
-rw-r--r--src/SMAPI/IGameContentHelper.cs14
-rw-r--r--src/SMAPI/IInputHelper.cs2
-rw-r--r--src/SMAPI/IMod.cs4
-rw-r--r--src/SMAPI/IModContentHelper.cs8
-rw-r--r--src/SMAPI/IModHelper.cs2
-rw-r--r--src/SMAPI/IModInfo.cs2
-rw-r--r--src/SMAPI/IModLinked.cs2
-rw-r--r--src/SMAPI/IModRegistry.cs9
-rw-r--r--src/SMAPI/IMonitor.cs2
-rw-r--r--src/SMAPI/IMultiplayerHelper.cs6
-rw-r--r--src/SMAPI/IMultiplayerPeer.cs10
-rw-r--r--src/SMAPI/IMultiplayerPeerMod.cs2
-rw-r--r--src/SMAPI/IReflectedField.cs2
-rw-r--r--src/SMAPI/IReflectedMethod.cs6
-rw-r--r--src/SMAPI/IReflectedProperty.cs2
-rw-r--r--src/SMAPI/IReflectionHelper.cs30
-rw-r--r--src/SMAPI/ITranslationHelper.cs6
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs55
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs24
-rw-r--r--src/SMAPI/Mod.cs13
-rw-r--r--src/SMAPI/Patches/Game1Patcher.cs6
-rw-r--r--src/SMAPI/Patches/TitleMenuPatcher.cs4
-rw-r--r--src/SMAPI/Program.cs22
-rw-r--r--src/SMAPI/SemanticVersion.cs3
-rw-r--r--src/SMAPI/Translation.cs32
-rw-r--r--src/SMAPI/Utilities/CaseInsensitivePathCache.cs4
-rw-r--r--src/SMAPI/Utilities/Keybind.cs5
-rw-r--r--src/SMAPI/Utilities/KeybindList.cs11
-rw-r--r--src/SMAPI/Utilities/PerScreen.cs36
-rw-r--r--src/SMAPI/Utilities/SDate.cs55
459 files changed, 3130 insertions, 3028 deletions
diff --git a/build/common.targets b/build/common.targets
index c227190a..c04546d0 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -7,8 +7,8 @@
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
<!--enable nullable annotations, except in .NET Standard 2.0 where they aren't supported-->
- <Nullable Condition="'$(TargetFramework)' == 'net5.0'">enable</Nullable>
- <NoWarn Condition="'$(TargetFramework)' != 'net5.0'">$(NoWarn);CS8632</NoWarn>
+ <Nullable Condition="'$(TargetFramework)' != 'netstandard2.0'">enable</Nullable>
+ <NoWarn Condition="'$(TargetFramework)' == 'netstandard2.0'">$(NoWarn);CS8632</NoWarn>
<!--set platform-->
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 0687b888..de30a23b 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -4,6 +4,7 @@
## Upcoming release
### For players
* Changes:
+ * When scanning for mod folders, dot-prefixed files are now ignored (thanks to Nuztalgia!).
* On Linux, SMAPI now fixes many issues with case-sensitive mod paths automatically.
* On Linux/macOS, added `--use-current-shell` [command-line argument](technical/smapi.md#command-line-arguments) to avoid opening a separate terminal window.
* Dropped update checks for the unofficial 64-bit patcher (obsolete since SMAPI 3.12.6).
@@ -40,11 +41,14 @@ the C# mod that loads them is updated.
* Major changes:
* Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0.
_These include new features not supported by the old API like load conflict resolution, edit priority, and content pack labels. They also support new cases like easily detecting when an asset has changed, and avoid data corruption issues in some edge cases._
+ * Added [nullable reference type annotations](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0#Nullable_reference_type_annotations) for all APIs.
* Added `helper.GameContent` and `helper.ModContent`, which will replace `helper.Content` in SMAPI 4.0.0.
* Overhauled [mod-provided API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Mod-provided_APIs) proxying (thanks to Shockah!).
_This adds support for many previously unsupported cases: proxied interfaces in return values or input arguments, proxied enums if their values match, generic methods, and more. Existing mod APIs should work fine as-is._
* Mod files loaded through SMAPI APIs (including `helper.Content.Load`) are now case-insensitive, even on Linux.
+ * Enabled deprecation notices for all deprecated APIs. These will only be shown in `TRACE` logs for at least a month after SMAPI 3.14.0 releases.
* Other improvements:
+ * Added `IAssetDataForImage.ExtendMap` to resize maps in asset editors.
* Added [command-line arguments](technical/smapi.md#command-line-arguments) to toggle developer mode (thanks to Tondorian!).
* Added `IContentPack.ModContent` property.
* Added `Constants.ContentPath`.
@@ -53,10 +57,15 @@ the C# mod that loads them is updated.
* Added `helper.Content.ParseAssetName` to get an `IAssetName` for an arbitrary asset key.
* If an asset is loaded multiple times in the same tick, `IAssetLoader.CanLoad` and `IAssetEditor.CanEdit` are now cached unless invalidated by `helper.Content.InvalidateCache`.
* The `ISemanticVersion` comparison methods (`CompareTo`, `IsBetween`, `IsNewerThan`, and `IsOlderThan`) now allow null values. A null version is always considered older than any non-null version per [best practices](https://docs.microsoft.com/en-us/dotnet/api/system.icomparable-1.compareto#remarks).
+ * Deprecation notices now show a shorter stack trace in most cases, so it's clearer where the deprecated code is in the mod.
* Fixes:
* Fixed the `SDate` constructor being case-sensitive.
* Fixed support for using locale codes from custom languages in asset names (e.g. `Data/Achievements.eo-EU`).
* Fixed issue where suppressing `[Left|Right]Thumbstick[Down|Left]` keys would suppress the opposite direction instead.
+ * Fixed null handling in various edge cases.
+
+### For SMAPI contributors
+* You no longer need a Nexus API key to launch the SMAPI web project locally.
## 3.13.4
Released 16 January 2022 for Stardew Valley 1.5.6 or later.
diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md
index 4c31f69b..c632af84 100644
--- a/docs/technical/mod-package.md
+++ b/docs/technical/mod-package.md
@@ -412,8 +412,11 @@ The NuGet package is generated automatically in `StardewModdingAPI.ModBuildConfi
when you compile it.
## Release notes
-## Upcoming release
+## 4.0.1
+Released 14 April 2022.
+
* Added detection for Xbox app game folders.
+* Fixed "_conflicts between different versions of Microsoft.Win32.Registry_" warnings in recent SMAPI versions.
* Internal refactoring.
## 4.0.0
diff --git a/src/SMAPI.Installer/Framework/InstallerContext.cs b/src/SMAPI.Installer/Framework/InstallerContext.cs
index 23d5b17c..a2c63dd8 100644
--- a/src/SMAPI.Installer/Framework/InstallerContext.cs
+++ b/src/SMAPI.Installer/Framework/InstallerContext.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.IO;
using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.GameScanning;
@@ -46,7 +44,7 @@ namespace StardewModdingAPI.Installer.Framework
/// <summary>Get the installer's version number.</summary>
public ISemanticVersion GetInstallerVersion()
{
- var raw = this.GetType().Assembly.GetName().Version;
+ var raw = this.GetType().Assembly.GetName().Version!;
return new SemanticVersion(raw);
}
diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs
index fd9d1be6..0976eceb 100644
--- a/src/SMAPI.Installer/Framework/InstallerPaths.cs
+++ b/src/SMAPI.Installer/Framework/InstallerPaths.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.IO;
using StardewModdingAPI.Toolkit.Framework;
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs
index b07c0461..5138173a 100644
--- a/src/SMAPI.Installer/InteractiveInstaller.cs
+++ b/src/SMAPI.Installer/InteractiveInstaller.cs
@@ -1,8 +1,7 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -37,6 +36,7 @@ namespace StardewModdingApi.Installer
/// <summary>Get the absolute file or folder paths to remove when uninstalling SMAPI.</summary>
/// <param name="installDir">The folder for Stardew Valley and SMAPI.</param>
/// <param name="modsDir">The folder for SMAPI mods.</param>
+ [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are valid file names.")]
private IEnumerable<string> GetUninstallPaths(DirectoryInfo installDir, DirectoryInfo modsDir)
{
string GetInstallPath(string path) => Path.Combine(installDir.FullName, path);
@@ -166,7 +166,7 @@ namespace StardewModdingApi.Installer
}
// get game path from CLI
- string gamePathArg = null;
+ string? gamePathArg = null;
{
int pathIndex = Array.LastIndexOf(args, "--game-path") + 1;
if (pathIndex >= 1 && args.Length >= pathIndex)
@@ -191,8 +191,8 @@ namespace StardewModdingApi.Installer
** show theme selector
****/
// get theme writers
- var lightBackgroundWriter = new ColorfulConsoleWriter(context.Platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.LightBackground));
- var darkBackgroundWriter = new ColorfulConsoleWriter(context.Platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.DarkBackground));
+ ColorfulConsoleWriter lightBackgroundWriter = new(context.Platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.LightBackground));
+ ColorfulConsoleWriter darkBackgroundWriter = new(context.Platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.DarkBackground));
// print question
this.PrintPlain("Which text looks more readable?");
@@ -239,7 +239,7 @@ namespace StardewModdingApi.Installer
** collect details
****/
// get game path
- DirectoryInfo installDir = this.InteractivelyGetInstallPath(toolkit, context, gamePathArg);
+ DirectoryInfo? installDir = this.InteractivelyGetInstallPath(toolkit, context, gamePathArg);
if (installDir == null)
{
this.PrintError("Failed finding your game path.");
@@ -451,7 +451,7 @@ namespace StardewModdingApi.Installer
}
// find target folder
- ModFolder targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase) == true);
+ ModFolder? targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase) == true);
DirectoryInfo defaultTargetFolder = new(Path.Combine(paths.ModsPath, sourceMod.Directory.Name));
DirectoryInfo targetFolder = targetMod?.Directory ?? defaultTargetFolder;
this.PrintDebug(targetFolder.FullName == defaultTargetFolder.FullName
@@ -534,27 +534,45 @@ namespace StardewModdingApi.Installer
/// <summary>Print a message without formatting.</summary>
/// <param name="text">The text to print.</param>
- private void PrintPlain(string text) => Console.WriteLine(text);
+ private void PrintPlain(string text)
+ {
+ Console.WriteLine(text);
+ }
/// <summary>Print a debug message.</summary>
/// <param name="text">The text to print.</param>
- private void PrintDebug(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug);
+ private void PrintDebug(string text)
+ {
+ this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug);
+ }
/// <summary>Print a debug message.</summary>
/// <param name="text">The text to print.</param>
- private void PrintInfo(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Info);
+ private void PrintInfo(string text)
+ {
+ this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Info);
+ }
/// <summary>Print a warning message.</summary>
/// <param name="text">The text to print.</param>
- private void PrintWarning(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Warn);
+ private void PrintWarning(string text)
+ {
+ this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Warn);
+ }
/// <summary>Print a warning message.</summary>
/// <param name="text">The text to print.</param>
- private void PrintError(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Error);
+ private void PrintError(string text)
+ {
+ this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Error);
+ }
/// <summary>Print a success message.</summary>
/// <param name="text">The text to print.</param>
- private void PrintSuccess(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Success);
+ private void PrintSuccess(string text)
+ {
+ this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Success);
+ }
/// <summary>Interactively delete a file or folder path, and block until deletion completes.</summary>
/// <param name="path">The file or folder path.</param>
@@ -580,7 +598,7 @@ namespace StardewModdingApi.Installer
/// <param name="source">The file or folder to copy.</param>
/// <param name="targetFolder">The folder to copy into.</param>
/// <param name="filter">A filter which matches directories and files to copy, or <c>null</c> to match all.</param>
- private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool> filter = null)
+ private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool>? filter = null)
{
if (filter != null && !filter(source))
return;
@@ -596,7 +614,7 @@ namespace StardewModdingApi.Installer
case DirectoryInfo sourceDir:
DirectoryInfo targetSubfolder = new(Path.Combine(targetFolder.FullName, sourceDir.Name));
- foreach (var entry in sourceDir.EnumerateFileSystemInfos())
+ foreach (FileSystemInfo entry in sourceDir.EnumerateFileSystemInfos())
this.RecursiveCopy(entry, targetSubfolder, filter);
break;
@@ -610,7 +628,7 @@ namespace StardewModdingApi.Installer
/// <param name="message">The message to print.</param>
/// <param name="options">The allowed options (not case sensitive).</param>
/// <param name="indent">The indentation to prefix to output.</param>
- private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string> print = null)
+ private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? print = null)
{
print ??= this.PrintInfo;
@@ -618,8 +636,8 @@ namespace StardewModdingApi.Installer
{
print(indent + message);
Console.Write(indent);
- string input = Console.ReadLine()?.Trim().ToLowerInvariant();
- if (!options.Contains(input))
+ string? input = Console.ReadLine()?.Trim().ToLowerInvariant();
+ if (input == null || !options.Contains(input))
{
print($"{indent}That's not a valid option.");
continue;
@@ -632,7 +650,7 @@ namespace StardewModdingApi.Installer
/// <param name="toolkit">The mod toolkit.</param>
/// <param name="context">The installer context.</param>
/// <param name="specifiedPath">The path specified as a command-line argument (if any), which should override automatic path detection.</param>
- private DirectoryInfo InteractivelyGetInstallPath(ModToolkit toolkit, InstallerContext context, string specifiedPath)
+ private DirectoryInfo? InteractivelyGetInstallPath(ModToolkit toolkit, InstallerContext context, string? specifiedPath)
{
// use specified path
if (specifiedPath != null)
@@ -699,7 +717,7 @@ namespace StardewModdingApi.Installer
// get path from user
Console.WriteLine();
this.PrintInfo($"Type the file path to the game directory (the one containing '{Constants.GameDllName}'), then press enter.");
- string path = Console.ReadLine()?.Trim();
+ string? path = Console.ReadLine()?.Trim();
if (string.IsNullOrWhiteSpace(path))
{
this.PrintWarning("You must specify a directory path to continue.");
@@ -712,13 +730,13 @@ namespace StardewModdingApi.Installer
: path.Replace("\\ ", " "); // in Linux/macOS, spaces in paths may be escaped if copied from the command line
if (path.StartsWith("~/"))
{
- string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE");
+ string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE")!;
path = Path.Combine(home, path.Substring(2));
}
// get directory
if (File.Exists(path))
- path = Path.GetDirectoryName(path);
+ path = Path.GetDirectoryName(path)!;
DirectoryInfo directory = new(path);
// validate path
@@ -765,7 +783,7 @@ namespace StardewModdingApi.Installer
// game folder which contains the installer, if any
{
- DirectoryInfo curPath = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
+ DirectoryInfo? curPath = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
while (curPath?.Parent != null) // must be in a folder (not at the root)
{
if (context.LooksLikeGameFolder(curPath))
@@ -787,7 +805,7 @@ namespace StardewModdingApi.Installer
}
}
- /// <summary>Interactively move mods out of the appdata directory.</summary>
+ /// <summary>Interactively move mods out of the app data directory.</summary>
/// <param name="properModsDir">The directory which should contain all mods.</param>
/// <param name="packagedModsDir">The installer directory containing packaged mods.</param>
private void InteractivelyRemoveAppDataMods(DirectoryInfo properModsDir, DirectoryInfo packagedModsDir)
@@ -847,7 +865,7 @@ namespace StardewModdingApi.Installer
/// <summary>Move a filesystem entry to a new parent directory.</summary>
/// <param name="entry">The filesystem entry to move.</param>
/// <param name="newPath">The destination path.</param>
- /// <remarks>We can't use <see cref="FileInfo.MoveTo"/> or <see cref="DirectoryInfo.MoveTo"/>, because those don't work across partitions.</remarks>
+ /// <remarks>We can't use <see cref="FileInfo.MoveTo(string)"/> or <see cref="DirectoryInfo.MoveTo"/>, because those don't work across partitions.</remarks>
private void Move(FileSystemInfo entry, string newPath)
{
// file
@@ -874,15 +892,12 @@ namespace StardewModdingApi.Installer
/// <param name="entry">The file or folder info.</param>
private bool ShouldCopy(FileSystemInfo entry)
{
- switch (entry.Name)
+ return entry.Name switch
{
- case "mcs":
- return false; // ignore macOS symlink
- case "Mods":
- return false; // Mods folder handled separately
- default:
- return true;
- }
+ "mcs" => false, // ignore macOS symlink
+ "Mods" => false, // Mods folder handled separately
+ _ => true
+ };
}
}
}
diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs
index 5139513f..dc452a46 100644
--- a/src/SMAPI.Installer/Program.cs
+++ b/src/SMAPI.Installer/Program.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@@ -17,7 +15,7 @@ namespace StardewModdingApi.Installer
*********/
/// <summary>The absolute path of the installer folder.</summary>
[SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")]
- private static readonly string InstallerPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ private static readonly string InstallerPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
/// <summary>The absolute path of the folder containing the unzipped installer files.</summary>
private static readonly string ExtractedBundlePath = Path.Combine(Path.GetTempPath(), $"SMAPI-installer-{Guid.NewGuid():N}");
@@ -68,14 +66,14 @@ namespace StardewModdingApi.Installer
/// <summary>Method called when assembly resolution fails, which may return a manually resolved assembly.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
- private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e)
+ private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs e)
{
try
{
AssemblyName name = new(e.Name);
foreach (FileInfo dll in new DirectoryInfo(Program.InternalFilesPath).EnumerateFiles("*.dll"))
{
- if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.OrdinalIgnoreCase))
+ if (name.Name != null && name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.OrdinalIgnoreCase))
return Assembly.LoadFrom(dll.FullName);
}
return null;
diff --git a/src/SMAPI.Internal.Patching/BasePatcher.cs b/src/SMAPI.Internal.Patching/BasePatcher.cs
index 6d019b52..c1936ccc 100644
--- a/src/SMAPI.Internal.Patching/BasePatcher.cs
+++ b/src/SMAPI.Internal.Patching/BasePatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Reflection;
using HarmonyLib;
@@ -32,7 +30,7 @@ namespace StardewModdingAPI.Internal.Patching
/// <param name="name">The method name.</param>
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
/// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param>
- protected MethodInfo RequireMethod<TTarget>(string name, Type[] parameters = null, Type[] generics = null)
+ protected MethodInfo RequireMethod<TTarget>(string name, Type[]? parameters = null, Type[]? generics = null)
{
return PatchHelper.RequireMethod<TTarget>(name, parameters, generics);
}
@@ -42,7 +40,7 @@ namespace StardewModdingAPI.Internal.Patching
/// <param name="priority">The patch priority to apply, usually specified using Harmony's <see cref="Priority"/> enum, or <c>null</c> to keep the default value.</param>
protected HarmonyMethod GetHarmonyMethod(string name, int? priority = null)
{
- var method = new HarmonyMethod(
+ HarmonyMethod method = new(
AccessTools.Method(this.GetType(), name)
?? throw new InvalidOperationException($"Can't find patcher method {PatchHelper.GetMethodString(this.GetType(), name)}.")
);
diff --git a/src/SMAPI.Internal.Patching/HarmonyPatcher.cs b/src/SMAPI.Internal.Patching/HarmonyPatcher.cs
index fc239fd2..6f30c241 100644
--- a/src/SMAPI.Internal.Patching/HarmonyPatcher.cs
+++ b/src/SMAPI.Internal.Patching/HarmonyPatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using HarmonyLib;
diff --git a/src/SMAPI.Internal.Patching/IPatcher.cs b/src/SMAPI.Internal.Patching/IPatcher.cs
index 5b373117..a732d64f 100644
--- a/src/SMAPI.Internal.Patching/IPatcher.cs
+++ b/src/SMAPI.Internal.Patching/IPatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using HarmonyLib;
namespace StardewModdingAPI.Internal.Patching
diff --git a/src/SMAPI.Internal.Patching/PatchHelper.cs b/src/SMAPI.Internal.Patching/PatchHelper.cs
index 52b15fd1..edd8ef57 100644
--- a/src/SMAPI.Internal.Patching/PatchHelper.cs
+++ b/src/SMAPI.Internal.Patching/PatchHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Linq;
using System.Reflection;
@@ -18,7 +16,7 @@ namespace StardewModdingAPI.Internal.Patching
/// <typeparam name="TTarget">The type containing the method.</typeparam>
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
/// <exception cref="InvalidOperationException">The type has no matching constructor.</exception>
- public static ConstructorInfo RequireConstructor<TTarget>(Type[] parameters = null)
+ public static ConstructorInfo RequireConstructor<TTarget>(Type[]? parameters = null)
{
return
AccessTools.Constructor(typeof(TTarget), parameters)
@@ -31,7 +29,7 @@ namespace StardewModdingAPI.Internal.Patching
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
/// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param>
/// <exception cref="InvalidOperationException">The type has no matching method.</exception>
- public static MethodInfo RequireMethod<TTarget>(string name, Type[] parameters = null, Type[] generics = null)
+ public static MethodInfo RequireMethod<TTarget>(string name, Type[]? parameters = null, Type[]? generics = null)
{
return
AccessTools.Method(typeof(TTarget), name, parameters, generics)
@@ -43,7 +41,7 @@ namespace StardewModdingAPI.Internal.Patching
/// <param name="name">The method name, or <c>null</c> for a constructor.</param>
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
/// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param>
- public static string GetMethodString(Type type, string name, Type[] parameters = null, Type[] generics = null)
+ public static string GetMethodString(Type type, string? name, Type[]? parameters = null, Type[]? generics = null)
{
StringBuilder str = new();
diff --git a/src/SMAPI.Internal/ConsoleWriting/ColorSchemeConfig.cs b/src/SMAPI.Internal/ConsoleWriting/ColorSchemeConfig.cs
index b22aa231..4e5850ea 100644
--- a/src/SMAPI.Internal/ConsoleWriting/ColorSchemeConfig.cs
+++ b/src/SMAPI.Internal/ConsoleWriting/ColorSchemeConfig.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
@@ -8,10 +6,26 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
/// <summary>The console color scheme options.</summary>
internal class ColorSchemeConfig
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The default color scheme ID to use, or <see cref="MonitorColorScheme.AutoDetect"/> to select one automatically.</summary>
- public MonitorColorScheme UseScheme { get; set; }
+ public MonitorColorScheme UseScheme { get; }
/// <summary>The available console color schemes.</summary>
- public IDictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>> Schemes { get; set; }
+ public IDictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>> Schemes { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="useScheme">The default color scheme ID to use, or <see cref="MonitorColorScheme.AutoDetect"/> to select one automatically.</param>
+ /// <param name="schemes">The available console color schemes.</param>
+ public ColorSchemeConfig(MonitorColorScheme useScheme, IDictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>> schemes)
+ {
+ this.UseScheme = useScheme;
+ this.Schemes = schemes;
+ }
}
}
diff --git a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs
index 19a31c7b..78db0d65 100644
--- a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs
+++ b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Internal.ConsoleWriting
@@ -13,10 +12,11 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
** Fields
*********/
/// <summary>The console text color for each log level.</summary>
- private readonly IDictionary<ConsoleLogLevel, ConsoleColor> Colors;
+ private readonly IDictionary<ConsoleLogLevel, ConsoleColor>? Colors;
/// <summary>Whether the current console supports color formatting.</summary>
- private readonly bool SupportsColor;
+ [MemberNotNullWhen(true, nameof(ColorfulConsoleWriter.Colors))]
+ private bool SupportsColor { get; }
/*********
@@ -74,10 +74,9 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
/// <remarks>The colors here should be kept in sync with the SMAPI config file.</remarks>
public static ColorSchemeConfig GetDefaultColorSchemeConfig(MonitorColorScheme useScheme)
{
- return new ColorSchemeConfig
- {
- UseScheme = useScheme,
- Schemes = new Dictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>>
+ return new ColorSchemeConfig(
+ useScheme: useScheme,
+ schemes: new Dictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>>
{
[MonitorColorScheme.DarkBackground] = new Dictionary<ConsoleLogLevel, ConsoleColor>
{
@@ -100,7 +99,7 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
[ConsoleLogLevel.Success] = ConsoleColor.DarkGreen
}
}
- };
+ );
}
@@ -136,7 +135,7 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
}
// get colors for scheme
- return colorConfig.Schemes.TryGetValue(schemeID, out IDictionary<ConsoleLogLevel, ConsoleColor> scheme)
+ return colorConfig.Schemes.TryGetValue(schemeID, out IDictionary<ConsoleLogLevel, ConsoleColor>? scheme)
? scheme
: throw new NotSupportedException($"Unknown color scheme '{schemeID}'.");
}
diff --git a/src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs
index 84e17207..fbcf161c 100644
--- a/src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs
+++ b/src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Internal.ConsoleWriting
{
/// <summary>Writes text to the console.</summary>
diff --git a/src/SMAPI.Internal/ExceptionHelper.cs b/src/SMAPI.Internal/ExceptionHelper.cs
index a856cf71..7edc0f62 100644
--- a/src/SMAPI.Internal/ExceptionHelper.cs
+++ b/src/SMAPI.Internal/ExceptionHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Reflection;
using System.Text.RegularExpressions;
@@ -14,7 +12,7 @@ namespace StardewModdingAPI.Internal
*********/
/// <summary>Get a string representation of an exception suitable for writing to the error log.</summary>
/// <param name="exception">The error to summarize.</param>
- public static string GetLogSummary(this Exception exception)
+ public static string GetLogSummary(this Exception? exception)
{
try
{
@@ -27,7 +25,7 @@ namespace StardewModdingAPI.Internal
case ReflectionTypeLoadException ex:
string summary = ex.ToString();
- foreach (Exception childEx in ex.LoaderExceptions)
+ foreach (Exception? childEx in ex.LoaderExceptions)
summary += $"\n\n{childEx?.GetLogSummary()}";
message = summary;
break;
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticResult.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticResult.cs
index 8c24eda9..845149bd 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticResult.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticResult.cs
@@ -1,8 +1,7 @@
-#nullable disable
-
// <generated />
-using Microsoft.CodeAnalysis;
+// ReSharper disable All -- generated code
using System;
+using Microsoft.CodeAnalysis;
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.Helper.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.Helper.cs
index 68a892a9..4bda70ff 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.Helper.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.Helper.cs
@@ -1,14 +1,14 @@
-#nullable disable
-
// <generated />
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Diagnostics;
-using Microsoft.CodeAnalysis.Text;
+// ReSharper disable All -- generated code
+
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
{
@@ -61,7 +61,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
var diagnostics = new List<Diagnostic>();
foreach (Project project in projects)
{
- CompilationWithAnalyzers compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer));
+ CompilationWithAnalyzers compilationWithAnalyzers = project.GetCompilationAsync().Result!.WithAnalyzers(ImmutableArray.Create(analyzer));
var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
foreach (Diagnostic diag in diags)
{
@@ -74,7 +74,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
for (int i = 0; i < documents.Length; i++)
{
Document document = documents[i];
- SyntaxTree tree = document.GetSyntaxTreeAsync().Result;
+ SyntaxTree? tree = document.GetSyntaxTreeAsync().Result;
if (tree == diag.Location.SourceTree)
{
diagnostics.Add(diag);
@@ -127,17 +127,6 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
}
/// <summary>
- /// Create a Document from a string through creating a project that contains it.
- /// </summary>
- /// <param name="source">Classes in the form of a string</param>
- /// <param name="language">The language the source code is in</param>
- /// <returns>A Document created from the source string</returns>
- protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
- {
- return CreateProject(new[] { source }, language).Documents.First();
- }
-
- /// <summary>
/// Create a project using the inputted strings as sources.
/// </summary>
/// <param name="sources">Classes in the form of strings</param>
@@ -167,7 +156,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
count++;
}
- return solution.GetProject(projectId);
+ return solution.GetProject(projectId)!;
}
#endregion
}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs
index 4170042d..efe69e4a 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Framework/DiagnosticVerifier.cs
@@ -1,6 +1,6 @@
-#nullable disable
-
// <generated />
+// ReSharper disable All -- generated code
+
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -19,18 +19,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
/// <summary>
/// Get the CSharp analyzer being tested - to be implemented in non-abstract class
/// </summary>
- protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
- {
- return null;
- }
-
- /// <summary>
- /// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class
- /// </summary>
- protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
- {
- return null;
- }
+ protected abstract DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer();
#endregion
#region Verifier wrappers
@@ -47,17 +36,6 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
}
/// <summary>
- /// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source
- /// Note: input a DiagnosticResult for each Diagnostic expected
- /// </summary>
- /// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
- /// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
- protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected)
- {
- this.VerifyDiagnostics(sources, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzer(), expected);
- }
-
- /// <summary>
/// General method that gets a collection of actual diagnostics found in the source after the analyzer is run,
/// then verifies each of them.
/// </summary>
@@ -222,11 +200,10 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
Assert.IsTrue(location.IsInSource,
$"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
- string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt";
var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
builder.AppendFormat("{0}({1}, {2}, {3}.{4})",
- resultMethodName,
+ "GetCSharpResultAt",
linePosition.Line + 1,
linePosition.Character + 1,
analyzerType.Name,
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetCollection.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetCollection.cs
index 54aa1c6c..8bedd583 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetCollection.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetCollection.cs
@@ -1,12 +1,8 @@
-#nullable disable
-
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
-using System.Collections;
-using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Netcode
{
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetCollection</c> for unit testing.</summary>
- public class NetCollection<T> : Collection<T>, IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { }
+ public class NetCollection<T> : Collection<T> { }
}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetFieldBase.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetFieldBase.cs
index 1c349a0b..8f6b8987 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetFieldBase.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetFieldBase.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
namespace Netcode
{
@@ -9,10 +7,13 @@ namespace Netcode
public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf>
{
/// <summary>The synchronised value.</summary>
- public T Value { get; set; }
+ public T? Value { get; set; }
/// <summary>Implicitly convert a net field to the its type.</summary>
/// <param name="field">The field to convert.</param>
- public static implicit operator T(NetFieldBase<T, TSelf> field) => field.Value;
+ public static implicit operator T?(NetFieldBase<T, TSelf> field)
+ {
+ return field.Value;
+ }
}
}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetInt.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetInt.cs
index e8e1dc63..b3abc467 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetInt.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetInt.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
namespace Netcode
{
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetList.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetList.cs
index f7fb9617..33e616fb 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetList.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetList.cs
@@ -1,11 +1,8 @@
-#nullable disable
-
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
-using System.Collections;
using System.Collections.Generic;
namespace Netcode
{
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetObjectList</c> for unit testing.</summary>
- public class NetList<T> : List<T>, IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { }
+ public class NetList<T> : List<T> { }
}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetObjectList.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetObjectList.cs
index 74c17843..7814e7d6 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetObjectList.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetObjectList.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
namespace Netcode
{
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs
index bdbf9b45..dbd05792 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs
@@ -1,7 +1,5 @@
-#nullable disable
-
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
-#pragma warning disable 649 // (never assigned) -- only used to test type conversions
+// ReSharper disable UnusedMember.Global -- used dynamically for unit tests
using System.Collections.Generic;
namespace StardewValley
@@ -10,6 +8,6 @@ namespace StardewValley
internal class Farmer
{
/// <summary>A sample field which should be replaced with a different property.</summary>
- public readonly IDictionary<string, int[]> friendships;
+ public readonly IDictionary<string, int[]> friendships = new Dictionary<string, int[]>();
}
}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs
index d1f0afc4..d50deb72 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Item.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
+// ReSharper disable UnusedMember.Global -- used dynamically for unit tests
using Netcode;
namespace StardewValley
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Object.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Object.cs
index f54b22fe..151010a7 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Object.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Object.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
using Netcode;
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs
index 29f3b956..f11a59d3 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using NUnit.Framework;
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs
index 1cf7369f..76607b8e 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using NUnit.Framework;
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs b/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs
index 1cc37b38..2e34cf71 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer/AnalyzerUtilities.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -42,8 +40,8 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
// invalid
fromExpression = null;
- fromType = default(TypeInfo);
- toType = default(TypeInfo);
+ fromType = default;
+ toType = default;
return false;
}
@@ -76,7 +74,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
// invalid
declaringType = null;
- memberType = default(TypeInfo);
+ memberType = default;
memberName = null;
return false;
}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs
index cb2856da..553aae99 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
index 158d7243..ba089513 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs
index 43fac9d5..c7026ee1 100644
--- a/src/SMAPI.ModBuildConfig/DeployModTask.cs
+++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
index ad2c0de3..80955f67 100644
--- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
+++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
diff --git a/src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs b/src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs
index 588118ef..64e31c29 100644
--- a/src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs
+++ b/src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.ModBuildConfig.Framework
diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
index 82eac7f6..c5790186 100644
--- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
+++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj
@@ -10,7 +10,7 @@
<!--NuGet package-->
<PackageId>Pathoschild.Stardew.ModBuildConfig</PackageId>
<Title>Build package for SMAPI mods</Title>
- <Version>4.0.0</Version>
+ <Version>4.0.1</Version>
<Authors>Pathoschild</Authors>
<Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.13.0 or later.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@@ -25,6 +25,12 @@
<ItemGroup>
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.10" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+
+ <!--
+ This is imported through Microsoft.Build.Utilities.Core. When installed by a mod, NuGet
+ otherwise imports version 4.3.0 instead of 5.0.0, which conflicts with SMAPI's version.
+ -->
+ <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs
index 8fcbf711..66f2f105 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs
@@ -1,8 +1,7 @@
-#nullable disable
-
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
@@ -54,7 +53,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
/// <param name="value">The parsed value.</param>
/// <param name="required">Whether to show an error if the argument is missing.</param>
/// <param name="oneOf">Require that the argument match one of the given values (case-insensitive).</param>
- public bool TryGet(int index, string name, out string value, bool required = true, string[] oneOf = null)
+ public bool TryGet(int index, string name, [NotNullWhen(true)] out string? value, bool required = true, string[]? oneOf = null)
{
value = null;
@@ -88,7 +87,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
value = 0;
// get argument
- if (!this.TryGet(index, name, out string raw, required))
+ if (!this.TryGet(index, name, out string? raw, required))
return false;
// parse
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs
index a8dd41f5..44b7824e 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ConsoleCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/IConsoleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/IConsoleCommand.cs
index 4c6df538..9c82bbd3 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/IConsoleCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/IConsoleCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
{
/// <summary>A console command to register.</summary>
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs
index f31457ed..f2194cff 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -25,7 +23,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
// get fix ID
- if (!args.TryGet(0, "fix_id", out string rawFixId, required: false))
+ if (!args.TryGet(0, "fix_id", out string? rawFixId, required: false))
{
monitor.Log("Invalid usage. Type 'help apply_save_fix' for details.", LogLevel.Error);
return;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs
index f289c669..cf1dcbce 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/DebugCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs
index 81a8c570..159d7c4a 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs
index d762d8bf..a233d588 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowDataFilesCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs
index b5733eb9..745b821b 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ShowGameFilesCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs
index 5484fc7c..8bf9f5db 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs
index 0d8db870..74d3d9df 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Linq;
using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
@@ -42,7 +40,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
}
// read arguments
- if (!args.TryGet(0, "item type", out string type, oneOf: this.ValidTypes))
+ if (!args.TryGet(0, "item type", out string? type, oneOf: this.ValidTypes))
return;
if (!args.TryGetInt(2, "count", out int count, min: 1, required: false))
count = 1;
@@ -50,7 +48,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
quality = Object.lowQuality;
// find matching item
- SearchableItem match = Enum.TryParse(type, true, out ItemType itemType)
+ SearchableItem? match = Enum.TryParse(type, true, out ItemType itemType)
? this.FindItemByID(monitor, args, itemType)
: this.FindItemByName(monitor, args);
if (match == null)
@@ -78,14 +76,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="args">The command arguments.</param>
/// <param name="type">The item type.</param>
- private SearchableItem FindItemByID(IMonitor monitor, ArgumentParser args, ItemType type)
+ private SearchableItem? FindItemByID(IMonitor monitor, ArgumentParser args, ItemType type)
{
// read arguments
if (!args.TryGetInt(1, "item ID", out int id, min: 0))
return null;
// find matching item
- SearchableItem item = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id);
+ SearchableItem? item = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id);
if (item == null)
monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error);
return item;
@@ -94,10 +92,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
/// <summary>Get a matching item by its name.</summary>
/// <param name="monitor">Writes messages to the console and log file.</param>
/// <param name="args">The command arguments.</param>
- private SearchableItem FindItemByName(IMonitor monitor, ArgumentParser args)
+ private SearchableItem? FindItemByName(IMonitor monitor, ArgumentParser args)
{
// read arguments
- if (!args.TryGet(1, "item name", out string name))
+ if (!args.TryGet(1, "item name", out string? name))
return null;
// find matching items
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs
index e57d4065..ef35ad19 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemTypesCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs
index 5a21b459..73d5b79d 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -63,15 +61,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
private IEnumerable<SearchableItem> GetItems(string[] searchWords)
{
// normalize search term
- searchWords = searchWords?.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray();
- if (searchWords?.Any() == false)
- searchWords = null;
+ searchWords = searchWords.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray();
+ bool getAll = !searchWords.Any();
// find matches
return (
from item in this.Items.GetAll()
let term = $"{item.ID}|{item.Type}|{item.Name}|{item.DisplayName}"
- where searchWords == null || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1)
+ where getAll || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1)
select item
);
}
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs
index e8605163..12a51bc9 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework;
using StardewValley;
@@ -24,9 +22,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
// parse arguments
- if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "eyes", "pants" }))
+ if (!args.TryGet(0, "target", out string? target, oneOf: new[] { "hair", "eyes", "pants" }))
return;
- if (!args.TryGet(1, "color", out string rawColor))
+ if (!args.TryGet(1, "color", out string? rawColor))
return;
// parse color
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs
index 02670911..b2035d42 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections;
using System.Collections.Generic;
@@ -37,7 +35,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
}
// parse arguments
- if (!args.TryGet(0, "farm type", out string farmType))
+ if (!args.TryGet(0, "farm type", out string? farmType))
return;
bool isVanillaId = int.TryParse(farmType, out int vanillaId) && vanillaId is (>= 0 and < Farm.layout_max);
@@ -112,7 +110,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
return;
}
- if (!this.GetCustomFarmTypes().TryGetValue(id, out ModFarmType customFarmType))
+ if (!this.GetCustomFarmTypes().TryGetValue(id, out ModFarmType? customFarmType))
{
monitor.Log($"Invalid farm type '{id}'. Enter `help set_farm_type` for more info.", LogLevel.Error);
return;
@@ -125,7 +123,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
/// <summary>Change the farm type.</summary>
/// <param name="type">The farm type ID.</param>
/// <param name="customFarmData">The custom farm type data, if applicable.</param>
- private void SetFarmType(int type, ModFarmType customFarmData)
+ private void SetFarmType(int type, ModFarmType? customFarmData)
{
// set flags
Game1.whichFarm = type;
@@ -138,7 +136,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
farm.updateWarps();
// clear spouse area cache to avoid errors
- FieldInfo cacheField = farm.GetType().GetField("_baseSpouseAreaTiles", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ FieldInfo? cacheField = farm.GetType().GetField("_baseSpouseAreaTiles", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (cacheField == null)
throw new InvalidOperationException("Failed to access '_baseSpouseAreaTiles' field to clear spouse area cache.");
if (cacheField.GetValue(farm) is not IDictionary cache)
@@ -166,7 +164,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
/// <param name="type">The farm type.</param>
private string GetVanillaName(int type)
{
- string translationKey = type switch
+ string? translationKey = type switch
{
Farm.default_layout => "Character_FarmStandard",
Farm.riverlands_layout => "Character_FarmFishing",
@@ -199,7 +197,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
****/
/// <summary>Get the display name for a custom farm type.</summary>
/// <param name="farmType">The custom farm type.</param>
- private string GetCustomName(ModFarmType farmType)
+ private string? GetCustomName(ModFarmType? farmType)
{
if (string.IsNullOrWhiteSpace(farmType?.TooltipStringPath))
return farmType?.ID;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs
index 1a1a9eab..f169159f 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs
index d1dede1f..1065bd21 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetImmunityCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs
index 2b3b140c..c2c4931d 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs
index f9ed6c58..8c794e75 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs
index 56447a65..3afcc62b 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs
index 4ce7e1f8..37c02ed0 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using StardewValley;
@@ -23,9 +21,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
// parse arguments
- if (!args.TryGet(0, "target", out string target, oneOf: new[] { "player", "farm" }))
+ if (!args.TryGet(0, "target", out string? target, oneOf: new[] { "player", "farm" }))
return;
- args.TryGet(1, "name", out string name, required: false);
+ args.TryGet(1, "name", out string? name, required: false);
// handle
switch (target)
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs
index ea8d74c2..24718ace 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs
index 84625a34..8193ff27 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using StardewValley;
@@ -23,7 +21,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
{
// parse arguments
- if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "shirt", "acc", "skin", "shoe", "swim", "gender" }))
+ if (!args.TryGet(0, "target", out string? target, oneOf: new[] { "hair", "shirt", "acc", "skin", "shoe", "swim", "gender" }))
return;
if (!args.TryGetInt(1, "style ID", out int styleID))
return;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs
index 92c73e08..4905b89a 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/ClearCommand.cs
@@ -1,8 +1,7 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Locations;
using StardewValley.Objects;
@@ -53,13 +52,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
}
// parse arguments
- if (!args.TryGet(0, "location", out string locationName, required: true))
+ if (!args.TryGet(0, "location", out string? locationName, required: true))
return;
- if (!args.TryGet(1, "object type", out string type, required: true, oneOf: this.ValidTypes))
+ if (!args.TryGet(1, "object type", out string? type, required: true, oneOf: this.ValidTypes))
return;
// get target location
- GameLocation location = Game1.locations.FirstOrDefault(p => p.Name != null && p.Name.Equals(locationName, StringComparison.OrdinalIgnoreCase));
+ GameLocation? location = Game1.locations.FirstOrDefault(p => p.Name != null && p.Name.Equals(locationName, StringComparison.OrdinalIgnoreCase));
if (location == null && locationName == "current")
location = Game1.currentLocation;
if (location == null)
@@ -168,11 +167,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{
int removed = 0;
- foreach (var pair in location.Objects.Pairs.ToArray())
+ foreach ((Vector2 tile, SObject? obj) in location.Objects.Pairs.ToArray())
{
- if (shouldRemove(pair.Value))
+ if (shouldRemove(obj))
{
- location.Objects.Remove(pair.Key);
+ location.Objects.Remove(tile);
removed++;
}
}
@@ -188,11 +187,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{
int removed = 0;
- foreach (var pair in location.terrainFeatures.Pairs.ToArray())
+ foreach ((Vector2 tile, TerrainFeature? feature) in location.terrainFeatures.Pairs.ToArray())
{
- if (shouldRemove(pair.Value))
+ if (shouldRemove(feature))
{
- location.terrainFeatures.Remove(pair.Key);
+ location.terrainFeatures.Remove(tile);
removed++;
}
}
@@ -228,7 +227,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{
int removed = 0;
- foreach (var clump in location.resourceClumps.Where(shouldRemove).ToArray())
+ foreach (ResourceClump clump in location.resourceClumps.Where(shouldRemove).ToArray())
{
location.resourceClumps.Remove(clump);
removed++;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs
index 0f18c760..5b1a4a13 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/DownMineLevelCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using StardewValley;
using StardewValley.Locations;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs
index 8808fe35..16faa2fe 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Linq;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs
index f9810dc3..09531720 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/HurryAllCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs
index 8aa27d93..399fd934 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Utilities;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs
index ad6ac777..f977fce3 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs
index ebe58913..505c0d1d 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Utilities;
@@ -39,7 +37,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
}
// parse arguments
- if (!args.TryGet(0, "season", out string season, oneOf: this.ValidSeasons))
+ if (!args.TryGet(0, "season", out string? season, oneOf: this.ValidSeasons))
return;
// handle
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs
index 1e6bab96..8c4458dd 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Xna.Framework;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs
index 995f222e..a666a634 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Utilities;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs
index ab0b2e05..3675a963 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewValley;
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
index 7d2a1662..3722e155 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -33,7 +31,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
/// <param name="itemTypes">The item types to fetch (or null for any type).</param>
/// <param name="includeVariants">Whether to include flavored variants like "Sunflower Honey".</param>
[SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = "TryCreate invokes the lambda immediately.")]
- public IEnumerable<SearchableItem> GetAll(ItemType[] itemTypes = null, bool includeVariants = true)
+ public IEnumerable<SearchableItem> GetAll(ItemType[]? itemTypes = null, bool includeVariants = true)
{
//
//
@@ -45,9 +43,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
//
//
- IEnumerable<SearchableItem> GetAllRaw()
+ IEnumerable<SearchableItem?> GetAllRaw()
{
- HashSet<ItemType> types = itemTypes?.Any() == true ? new HashSet<ItemType>(itemTypes) : null;
+ HashSet<ItemType>? types = itemTypes?.Any() == true ? new HashSet<ItemType>(itemTypes) : null;
bool ShouldGet(ItemType type) => types == null || types.Contains(type);
// get tools
@@ -134,7 +132,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
{
foreach (int id in Game1.objectInformation.Keys)
{
- string[] fields = Game1.objectInformation[id]?.Split('/');
+ string[]? fields = Game1.objectInformation[id]?.Split('/');
// ring
if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable ring
@@ -148,7 +146,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
{
if (ShouldGet(ItemType.Object))
{
- foreach (SearchableItem journalScrap in this.GetSecretNotes(isJournalScrap: true))
+ foreach (SearchableItem? journalScrap in this.GetSecretNotes(isJournalScrap: true))
yield return journalScrap;
}
}
@@ -158,7 +156,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
{
if (ShouldGet(ItemType.Object))
{
- foreach (SearchableItem secretNote in this.GetSecretNotes(isJournalScrap: false))
+ foreach (SearchableItem? secretNote in this.GetSecretNotes(isJournalScrap: false))
yield return secretNote;
}
}
@@ -167,7 +165,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
else if (ShouldGet(ItemType.Object))
{
// spawn main item
- SObject item = null;
+ SObject? item = null;
yield return this.TryCreate(ItemType.Object, id, p =>
{
return item = (p.ID == 812 // roe
@@ -181,7 +179,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
// flavored items
if (includeVariants)
{
- foreach (SearchableItem variant in this.GetFlavoredObjectVariants(item))
+ foreach (SearchableItem? variant in this.GetFlavoredObjectVariants(item))
yield return variant;
}
}
@@ -189,7 +187,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
}
}
- return GetAllRaw().Where(p => p != null);
+ return (
+ from item in GetAllRaw()
+ where item != null
+ select item
+ );
}
@@ -199,7 +201,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
/// <summary>Get the individual secret note or journal scrap items.</summary>
/// <param name="isJournalScrap">Whether to get journal scraps.</param>
/// <remarks>Derived from <see cref="GameLocation.tryToCreateUnseenSecretNote"/>.</remarks>
- private IEnumerable<SearchableItem> GetSecretNotes(bool isJournalScrap)
+ private IEnumerable<SearchableItem?> GetSecretNotes(bool isJournalScrap)
{
// get base item ID
int baseId = isJournalScrap ? 842 : 79;
@@ -235,7 +237,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
/// <summary>Get flavored variants of a base item (like Blueberry Wine for Blueberry), if any.</summary>
/// <param name="item">A sample of the base item.</param>
- private IEnumerable<SearchableItem> GetFlavoredObjectVariants(SObject item)
+ private IEnumerable<SearchableItem?> GetFlavoredObjectVariants(SObject item)
{
int id = item.ParentSheetIndex;
@@ -305,9 +307,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
foreach (var pair in Game1.objectInformation)
{
// get input
- SObject input = this.TryCreate(ItemType.Object, pair.Key, p => new SObject(p.ID, 1))?.Item as SObject;
- var inputTags = input?.GetContextTags();
- if (inputTags?.Any() != true)
+ SObject? input = this.TryCreate(ItemType.Object, pair.Key, p => new SObject(p.ID, 1))?.Item as SObject;
+ if (input == null)
+ continue;
+
+ HashSet<string> inputTags = input.GetContextTags();
+ if (!inputTags.Any())
continue;
// check if roe-producing fish
@@ -315,7 +320,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
continue;
// yield roe
- SObject roe = null;
+ SObject? roe = null;
Color color = this.GetRoeColor(input);
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, _ =>
{
@@ -372,6 +377,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
/// <typeparam name="TValue">The asset value type.</typeparam>
/// <param name="assetName">The data asset name.</param>
private Dictionary<TKey, TValue> TryLoad<TKey, TValue>(string assetName)
+ where TKey : notnull
{
try
{
@@ -388,7 +394,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
/// <param name="type">The item type.</param>
/// <param name="id">The unique ID (if different from the item's parent sheet index).</param>
/// <param name="createItem">Create an item instance.</param>
- private SearchableItem TryCreate(ItemType type, int id, Func<SearchableItem, Item> createItem)
+ private SearchableItem? TryCreate(ItemType type, int id, Func<SearchableItem, Item> createItem)
{
try
{
diff --git a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs
index e3ca1a39..dbfca815 100644
--- a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -15,13 +13,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
** Fields
*********/
/// <summary>The commands to handle.</summary>
- private IConsoleCommand[] Commands;
+ private IConsoleCommand[] Commands = null!;
/// <summary>The commands which may need to handle update ticks.</summary>
- private IConsoleCommand[] UpdateHandlers;
+ private IConsoleCommand[] UpdateHandlers = null!;
/// <summary>The commands which may need to handle input.</summary>
- private IConsoleCommand[] InputHandlers;
+ private IConsoleCommand[] InputHandlers = null!;
/*********
@@ -52,7 +50,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
/// <summary>The method invoked when a button is pressed.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
- private void OnButtonPressed(object sender, ButtonPressedEventArgs e)
+ private void OnButtonPressed(object? sender, ButtonPressedEventArgs e)
{
foreach (IConsoleCommand command in this.InputHandlers)
command.OnButtonPressed(this.Monitor, e.Button);
@@ -61,7 +59,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
/// <summary>The method invoked when the game updates its state.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
- private void OnUpdateTicked(object sender, EventArgs e)
+ private void OnUpdateTicked(object? sender, EventArgs e)
{
foreach (IConsoleCommand command in this.UpdateHandlers)
command.OnUpdated(this.Monitor);
@@ -83,7 +81,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
return (
from type in this.GetType().Assembly.GetTypes()
where !type.IsAbstract && typeof(IConsoleCommand).IsAssignableFrom(type)
- select (IConsoleCommand)Activator.CreateInstance(type)
+ select (IConsoleCommand)Activator.CreateInstance(type)!
);
}
}
diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs
index fa171012..bfbfd2dc 100644
--- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs
+++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Reflection;
using StardewModdingAPI.Events;
@@ -59,7 +57,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler
/// <summary>The method invoked when a save is loaded.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
- private void OnSaveLoaded(object sender, SaveLoadedEventArgs e)
+ private void OnSaveLoaded(object? sender, SaveLoadedEventArgs e)
{
// show in-game warning for removed save content
if (this.IsSaveContentRemoved)
@@ -82,7 +80,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler
MethodInfo getMonitorForGame = coreType.GetMethod("GetMonitorForGame")
?? throw new InvalidOperationException("Can't access the SMAPI's 'GetMonitorForGame' method. This mod may not work correctly.");
- return (IMonitor)getMonitorForGame.Invoke(core, Array.Empty<object>()) ?? this.Monitor;
+ return (IMonitor?)getMonitorForGame.Invoke(core, Array.Empty<object>()) ?? this.Monitor;
}
}
}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs
index b05c8cca..e98eec3c 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/DialoguePatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using HarmonyLib;
@@ -19,10 +17,10 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
** Fields
*********/
/// <summary>Writes messages to the console and log file on behalf of the game.</summary>
- private static IMonitor MonitorForGame;
+ private static IMonitor MonitorForGame = null!;
/// <summary>Simplifies access to private code.</summary>
- private static IReflectionHelper Reflection;
+ private static IReflectionHelper Reflection = null!;
/*********
@@ -56,12 +54,12 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <param name="speaker">The NPC for which the dialogue is being parsed.</param>
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
/// <returns>Returns the exception to throw, if any.</returns>
- private static Exception Finalize_Constructor(Dialogue __instance, string masterDialogue, NPC speaker, Exception __exception)
+ private static Exception? Finalize_Constructor(Dialogue __instance, string masterDialogue, NPC? speaker, Exception? __exception)
{
if (__exception != null)
{
// log message
- string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null;
+ string? name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null;
DialoguePatcher.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{__exception.GetLogSummary()}", LogLevel.Error);
// set default dialogue
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs
index 63674d09..073c62cc 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using HarmonyLib;
@@ -18,7 +16,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
** Fields
*********/
/// <summary>Writes messages to the console and log file on behalf of the game.</summary>
- private static IMonitor MonitorForGame;
+ private static IMonitor MonitorForGame = null!;
/*********
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs
index 98aa4a38..9247fa48 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using HarmonyLib;
@@ -19,8 +17,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
** Fields
*********/
/// <summary>Writes messages to the console and log file on behalf of the game.</summary>
- private static IMonitor MonitorForGame;
-
+ private static IMonitor MonitorForGame = null!;
/*********
** Public methods
@@ -54,7 +51,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <param name="precondition">The precondition to be parsed.</param>
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
/// <returns>Returns the exception to throw, if any.</returns>
- private static Exception Finalize_CheckEventPrecondition(ref int __result, string precondition, Exception __exception)
+ private static Exception? Finalize_CheckEventPrecondition(ref int __result, string precondition, Exception? __exception)
{
if (__exception != null)
{
@@ -70,7 +67,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <param name="map">The map whose tilesheets to update.</param>
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
/// <returns>Returns the exception to throw, if any.</returns>
- private static Exception Finalize_UpdateSeasonalTileSheets(GameLocation __instance, Map map, Exception __exception)
+ private static Exception? Finalize_UpdateSeasonalTileSheets(GameLocation __instance, Map map, Exception? __exception)
{
if (__exception != null)
GameLocationPatcher.MonitorForGame.Log($"Failed updating seasonal tilesheets for location '{__instance.NameOrUniqueName}': \n{__exception}", LogLevel.Error);
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs
index 85ce8ac4..b65a695a 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/IClickableMenuPatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Diagnostics.CodeAnalysis;
using HarmonyLib;
using StardewModdingAPI.Internal.Patching;
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs
index 5354f724..11f7ec69 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/NpcPatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -20,7 +18,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
** Fields
*********/
/// <summary>Writes messages to the console and log file on behalf of the game.</summary>
- private static IMonitor MonitorForGame;
+ private static IMonitor MonitorForGame = null!;
/*********
@@ -56,7 +54,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <param name="__result">The return value of the original method.</param>
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
/// <returns>Returns the exception to throw, if any.</returns>
- private static Exception Finalize_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, Exception __exception)
+ private static Exception? Finalize_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, Exception? __exception)
{
if (__exception == null)
return null;
@@ -73,7 +71,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <param name="__result">The patched method's return value.</param>
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
/// <returns>Returns the exception to throw, if any.</returns>
- private static Exception Finalize_ParseMasterSchedule(string rawData, NPC __instance, ref Dictionary<int, SchedulePathDescription> __result, Exception __exception)
+ private static Exception? Finalize_ParseMasterSchedule(string rawData, NPC __instance, ref Dictionary<int, SchedulePathDescription> __result, Exception? __exception)
{
if (__exception != null)
{
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs
index 499718b0..09a6fbbd 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/ObjectPatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -59,7 +57,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <param name="__result">The patched method's return value.</param>
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
/// <returns>Returns the exception to throw, if any.</returns>
- private static Exception Finalize_Object_loadDisplayName(ref string __result, Exception __exception)
+ private static Exception? Finalize_Object_loadDisplayName(ref string __result, Exception? __exception)
{
if (__exception is KeyNotFoundException)
{
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs
index 1941d2a8..490bbfb6 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/SaveGamePatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -24,10 +22,10 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
** Fields
*********/
/// <summary>Writes messages to the console and log file.</summary>
- private static IMonitor Monitor;
+ private static IMonitor Monitor = null!;
/// <summary>A callback invoked when custom content is removed from the save data to avoid a crash.</summary>
- private static Action OnContentRemoved;
+ private static Action OnContentRemoved = null!;
/*********
@@ -76,7 +74,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <summary>The method to call after <see cref="SaveGame.LoadFarmType"/> throws an exception.</summary>
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
/// <returns>Returns the exception to throw, if any.</returns>
- private static Exception Finalize_LoadFarmType(Exception __exception)
+ private static Exception? Finalize_LoadFarmType(Exception? __exception)
{
// missing custom farm type
if (__exception?.Message.Contains("not a valid farm type") == true && !int.TryParse(SaveGame.loaded.whichFarm, out _))
@@ -110,7 +108,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <summary>Remove content which no longer exists in the game data.</summary>
/// <param name="location">The current game location.</param>
/// <param name="npcs">The NPC data.</param>
- private static bool RemoveBrokenContent(GameLocation location, IDictionary<string, string> npcs)
+ private static bool RemoveBrokenContent(GameLocation? location, IDictionary<string, string> npcs)
{
bool removedAny = false;
if (location == null)
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs
index b4c03bb9..d369e0ef 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchPatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using HarmonyLib;
@@ -30,9 +28,9 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/*********
** Private methods
*********/
- /// <summary>The method to call after <see cref="SpriteBatch.CheckValid"/>.</summary>
+ /// <summary>The method to call after <see cref="SpriteBatch.CheckValid(Texture2D)"/>.</summary>
/// <param name="texture">The texture to validate.</param>
- private static void After_CheckValid(Texture2D texture)
+ private static void After_CheckValid(Texture2D? texture)
{
if (texture?.IsDisposed == true)
throw new ObjectDisposedException("Cannot draw this texture because it's disposed.");
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs b/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs
index 108ed585..6d75a581 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/UtilityPatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using HarmonyLib;
@@ -35,7 +33,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <param name="delimiter">The delimiter by which to split the text description.</param>
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
/// <returns>Returns the exception to throw, if any.</returns>
- private static Exception Finalize_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception __exception)
+ private static Exception? Finalize_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception? __exception)
{
return __exception != null
? new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", __exception)
diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs
index b2b41ca6..a79c092f 100644
--- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs
+++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;
using System.Linq;
@@ -82,7 +81,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
}
// compress backup if possible
- if (!this.TryCompress(fallbackDir.FullName, targetFile, out Exception compressError))
+ if (!this.TryCompress(fallbackDir.FullName, targetFile, out Exception? compressError))
{
this.Monitor.Log(Constants.TargetPlatform != GamePlatform.Android
? $"Backed up to {fallbackDir.FullName}." // expected to fail on Android
@@ -142,7 +141,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
/// <param name="destination">The destination file to create.</param>
/// <param name="error">The error which occurred trying to compress, if applicable. This is <see cref="NotSupportedException"/> if compression isn't supported on this platform.</param>
/// <returns>Returns whether compression succeeded.</returns>
- private bool TryCompress(string sourcePath, FileInfo destination, out Exception error)
+ private bool TryCompress(string sourcePath, FileInfo destination, [NotNullWhen(false)] out Exception? error)
{
try
{
@@ -210,7 +209,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
/// <param name="filter">A filter which matches the files or directories to copy, or <c>null</c> to copy everything.</param>
/// <remarks>Derived from the SMAPI installer code.</remarks>
/// <returns>Returns whether any files were copied.</returns>
- private bool RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool> filter, bool copyRoot = true)
+ private bool RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool>? filter, bool copyRoot = true)
{
if (!source.Exists || filter?.Invoke(source) == false)
return false;
@@ -244,7 +243,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
private bool MatchSaveFolders(DirectoryInfo savesFolder, FileSystemInfo entry)
{
// only need to filter top-level entries
- string parentPath = (entry as FileInfo)?.DirectoryName ?? (entry as DirectoryInfo)?.Parent?.FullName;
+ string? parentPath = (entry as FileInfo)?.DirectoryName ?? (entry as DirectoryInfo)?.Parent?.FullName;
if (parentPath != savesFolder.FullName)
return true;
diff --git a/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs b/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs
index 285dd259..ac7bd338 100644
--- a/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs
+++ b/src/SMAPI.Tests.ModApiConsumer/ApiConsumer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using SMAPI.Tests.ModApiConsumer.Interfaces;
diff --git a/src/SMAPI.Tests.ModApiConsumer/Interfaces/ISimpleApi.cs b/src/SMAPI.Tests.ModApiConsumer/Interfaces/ISimpleApi.cs
index 23491fd1..7f94e137 100644
--- a/src/SMAPI.Tests.ModApiConsumer/Interfaces/ISimpleApi.cs
+++ b/src/SMAPI.Tests.ModApiConsumer/Interfaces/ISimpleApi.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Reflection;
diff --git a/src/SMAPI.Tests.ModApiProvider/Framework/BaseApi.cs b/src/SMAPI.Tests.ModApiProvider/Framework/BaseApi.cs
index b5870baa..77001e4c 100644
--- a/src/SMAPI.Tests.ModApiProvider/Framework/BaseApi.cs
+++ b/src/SMAPI.Tests.ModApiProvider/Framework/BaseApi.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace SMAPI.Tests.ModApiProvider.Framework
{
/// <summary>The base class for <see cref="SimpleApi"/>.</summary>
@@ -9,6 +7,6 @@ namespace SMAPI.Tests.ModApiProvider.Framework
** Test interface
*********/
/// <summary>A property inherited from a base class.</summary>
- public string InheritedProperty { get; set; }
+ public string? InheritedProperty { get; set; }
}
}
diff --git a/src/SMAPI.Tests.ModApiProvider/Framework/SimpleApi.cs b/src/SMAPI.Tests.ModApiProvider/Framework/SimpleApi.cs
index 82e902f5..e7e1ccef 100644
--- a/src/SMAPI.Tests.ModApiProvider/Framework/SimpleApi.cs
+++ b/src/SMAPI.Tests.ModApiProvider/Framework/SimpleApi.cs
@@ -1,4 +1,4 @@
-#nullable disable
+// ReSharper disable UnusedMember.Global -- used dynamically through proxies
using System;
using System.Collections.Generic;
@@ -16,7 +16,7 @@ namespace SMAPI.Tests.ModApiProvider.Framework
** Events
****/
/// <summary>A simple event field.</summary>
- public event EventHandler<int> OnEventRaised;
+ public event EventHandler<int>? OnEventRaised;
/// <summary>A simple event property with custom add/remove logic.</summary>
public event EventHandler<int> OnEventRaisedProperty
@@ -33,16 +33,16 @@ namespace SMAPI.Tests.ModApiProvider.Framework
public int NumberProperty { get; set; }
/// <summary>A simple object property.</summary>
- public object ObjectProperty { get; set; }
+ public object? ObjectProperty { get; set; }
/// <summary>A simple list property.</summary>
- public List<string> ListProperty { get; set; }
+ public List<string>? ListProperty { get; set; }
/// <summary>A simple list property with an interface.</summary>
- public IList<string> ListPropertyWithInterface { get; set; }
+ public IList<string>? ListPropertyWithInterface { get; set; }
/// <summary>A property with nested generics.</summary>
- public IDictionary<string, IList<string>> GenericsProperty { get; set; }
+ public IDictionary<string, IList<string>>? GenericsProperty { get; set; }
/// <summary>A property using an enum available to both mods.</summary>
public BindingFlags EnumProperty { get; set; }
diff --git a/src/SMAPI.Tests.ModApiProvider/ProviderMod.cs b/src/SMAPI.Tests.ModApiProvider/ProviderMod.cs
index 3fc8d749..c36e1c6d 100644
--- a/src/SMAPI.Tests.ModApiProvider/ProviderMod.cs
+++ b/src/SMAPI.Tests.ModApiProvider/ProviderMod.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Reflection;
using SMAPI.Tests.ModApiProvider.Framework;
diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs
index ef8a08ef..a1712726 100644
--- a/src/SMAPI.Tests/Core/AssetNameTests.cs
+++ b/src/SMAPI.Tests/Core/AssetNameTests.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using FluentAssertions;
@@ -28,7 +26,7 @@ namespace SMAPI.Tests.Core
[TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)]
[TestCase("Characters/Dialogue\\Abigail.fr-FR", "Characters/Dialogue/Abigail.fr-FR", null, null)]
[TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)]
- public void Constructor_Valid(string name, string expectedBaseName, string expectedLocale, LocalizedContentManager.LanguageCode? expectedLanguageCode)
+ public void Constructor_Valid(string name, string expectedBaseName, string? expectedLocale, LocalizedContentManager.LanguageCode? expectedLanguageCode)
{
// arrange
name = PathUtilities.NormalizeAssetName(name);
@@ -55,13 +53,13 @@ namespace SMAPI.Tests.Core
[TestCase(" ")]
[TestCase("\t")]
[TestCase(" \t ")]
- public void Constructor_NullOrWhitespace(string name)
+ public void Constructor_NullOrWhitespace(string? name)
{
// act
- ArgumentException exception = Assert.Throws<ArgumentException>(() => _ = AssetName.Parse(name, null));
+ ArgumentException exception = Assert.Throws<ArgumentException>(() => _ = AssetName.Parse(name!, _ => null))!;
// assert
- exception!.ParamName.Should().Be("rawName");
+ exception.ParamName.Should().Be("rawName");
exception.Message.Should().Be("The asset name can't be null or empty. (Parameter 'rawName')");
}
diff --git a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs
index 1bf2ed68..0b4919ed 100644
--- a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs
+++ b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -41,7 +39,7 @@ namespace SMAPI.Tests.Core
public void CanProxy_EventField()
{
// arrange
- var providerMod = new ProviderMod();
+ ProviderMod providerMod = new();
object implementation = providerMod.GetModApi();
int expectedValue = this.Random.Next();
@@ -61,7 +59,7 @@ namespace SMAPI.Tests.Core
public void CanProxy_EventProperty()
{
// arrange
- var providerMod = new ProviderMod();
+ ProviderMod providerMod = new();
object implementation = providerMod.GetModApi();
int expectedValue = this.Random.Next();
@@ -86,7 +84,7 @@ namespace SMAPI.Tests.Core
public void CanProxy_Properties(string setVia)
{
// arrange
- var providerMod = new ProviderMod();
+ ProviderMod providerMod = new();
object implementation = providerMod.GetModApi();
int expectedNumber = this.Random.Next();
int expectedObject = this.Random.Next();
@@ -317,13 +315,13 @@ namespace SMAPI.Tests.Core
/// <summary>Get a property value from an instance.</summary>
/// <param name="parent">The instance whose property to read.</param>
/// <param name="name">The property name.</param>
- private object GetPropertyValue(object parent, string name)
+ private object? GetPropertyValue(object parent, string name)
{
if (parent is null)
throw new ArgumentNullException(nameof(parent));
Type type = parent.GetType();
- PropertyInfo property = type.GetProperty(name);
+ PropertyInfo? property = type.GetProperty(name);
if (property is null)
throw new InvalidOperationException($"The '{type.FullName}' type has no public property named '{name}'.");
diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs
index e1b56559..bd621bbf 100644
--- a/src/SMAPI.Tests/Core/ModResolverTests.cs
+++ b/src/SMAPI.Tests/Core/ModResolverTests.cs
@@ -145,7 +145,7 @@ namespace SMAPI.Tests.Core
{
// arrange
Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
- this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields
+ this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields(this.GetModDataRecord())
{
Status = ModStatus.AssumeBroken
});
@@ -216,9 +216,9 @@ namespace SMAPI.Tests.Core
File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll!), "");
// arrange
- Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict);
+ Mock<IModMetadata> mock = new(MockBehavior.Strict);
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
- mock.Setup(p => p.DataRecord).Returns(() => null);
+ mock.Setup(p => p.DataRecord).Returns(this.GetModDataRecordVersionedFields());
mock.Setup(p => p.Manifest).Returns(manifest);
mock.Setup(p => p.DirectoryPath).Returns(modFolder);
@@ -265,7 +265,7 @@ namespace SMAPI.Tests.Core
public void ProcessDependencies_Skips_Failed()
{
// arrange
- Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict);
+ Mock<IModMetadata> mock = new(MockBehavior.Strict);
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
// act
@@ -380,7 +380,7 @@ namespace SMAPI.Tests.Core
Mock<IModMetadata> modA = this.GetMetadata("Mod A");
Mock<IModMetadata> modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" });
Mock<IModMetadata> modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true);
- Mock<IModMetadata> modD = new Mock<IModMetadata>(MockBehavior.Strict);
+ Mock<IModMetadata> modD = new(MockBehavior.Strict);
modD.Setup(p => p.Manifest).Returns<IManifest>(null);
modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
@@ -507,7 +507,7 @@ namespace SMAPI.Tests.Core
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
private Mock<IModMetadata> GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false)
{
- IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null as ISemanticVersion)).ToArray());
+ IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null as ISemanticVersion)).ToArray());
return this.GetMetadata(manifest, allowStatusChange);
}
@@ -516,8 +516,8 @@ namespace SMAPI.Tests.Core
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false)
{
- Mock<IModMetadata> mod = new Mock<IModMetadata>(MockBehavior.Strict);
- mod.Setup(p => p.DataRecord).Returns(() => null);
+ Mock<IModMetadata> mod = new(MockBehavior.Strict);
+ mod.Setup(p => p.DataRecord).Returns(this.GetModDataRecordVersionedFields());
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID);
mod.Setup(p => p.Manifest).Returns(manifest);
@@ -538,11 +538,22 @@ namespace SMAPI.Tests.Core
private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModDataRecordVersionedFields? modRecord = null)
{
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
- mod.Setup(p => p.DataRecord).Returns(() => null);
mod.Setup(p => p.Manifest).Returns(this.GetManifest());
mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath());
- mod.Setup(p => p.DataRecord).Returns(modRecord);
+ mod.Setup(p => p.DataRecord).Returns(modRecord ?? this.GetModDataRecordVersionedFields());
mod.Setup(p => p.GetUpdateKeys(It.IsAny<bool>())).Returns(Enumerable.Empty<UpdateKey>());
}
+
+ /// <summary>Generate a default mod data record.</summary>
+ private ModDataRecord GetModDataRecord()
+ {
+ return new("Default Display Name", new ModDataModel("Sample ID", null, ModWarning.None));
+ }
+
+ /// <summary>Generate a default mod data versioned fields instance.</summary>
+ private ModDataRecordVersionedFields GetModDataRecordVersionedFields()
+ {
+ return new ModDataRecordVersionedFields(this.GetModDataRecord());
+ }
}
}
diff --git a/src/SMAPI.Tests/Core/TranslationTests.cs b/src/SMAPI.Tests/Core/TranslationTests.cs
index f8f0e315..a52df607 100644
--- a/src/SMAPI.Tests/Core/TranslationTests.cs
+++ b/src/SMAPI.Tests/Core/TranslationTests.cs
@@ -1,11 +1,14 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
using System.Linq;
using NUnit.Framework;
using StardewModdingAPI;
+using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModHelpers;
+using StardewModdingAPI.Framework.ModLoading;
+using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewValley;
namespace SMAPI.Tests.Core
@@ -18,7 +21,7 @@ namespace SMAPI.Tests.Core
** Data
*********/
/// <summary>Sample translation text for unit tests.</summary>
- public static string[] Samples = { null, "", " ", "boop", " boop " };
+ public static string?[] Samples = { null, "", " ", "boop", " boop " };
/*********
@@ -34,15 +37,15 @@ namespace SMAPI.Tests.Core
var data = new Dictionary<string, IDictionary<string, string>>();
// act
- ITranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
+ ITranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
Translation translation = helper.Get("key");
- Translation[] translationList = helper.GetTranslations()?.ToArray();
+ Translation[]? translationList = helper.GetTranslations()?.ToArray();
// assert
Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value.");
Assert.AreEqual(LocalizedContentManager.LanguageCode.en, helper.LocaleEnum, "The locale enum doesn't match the input value.");
Assert.IsNotNull(translationList, "The full list of translations is unexpectedly null.");
- Assert.AreEqual(0, translationList.Length, "The full list of translations is unexpectedly not empty.");
+ Assert.AreEqual(0, translationList!.Length, "The full list of translations is unexpectedly not empty.");
Assert.IsNotNull(translation, "The translation helper unexpectedly returned a null translation.");
Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value.");
@@ -56,8 +59,8 @@ namespace SMAPI.Tests.Core
var expected = this.GetExpectedTranslations();
// act
- var actual = new Dictionary<string, Translation[]>();
- TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
+ var actual = new Dictionary<string, Translation[]?>();
+ TranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
foreach (string locale in expected.Keys)
{
this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);
@@ -81,7 +84,7 @@ namespace SMAPI.Tests.Core
// act
var actual = new Dictionary<string, Translation[]>();
- TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
+ TranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
foreach (string locale in expected.Keys)
{
this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);
@@ -109,13 +112,13 @@ namespace SMAPI.Tests.Core
[TestCase(" ", ExpectedResult = true)]
[TestCase("boop", ExpectedResult = true)]
[TestCase(" boop ", ExpectedResult = true)]
- public bool Translation_HasValue(string text)
+ public bool Translation_HasValue(string? text)
{
return new Translation("pt-BR", "key", text).HasValue();
}
[Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")]
- public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text)
+ public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string? text)
{
// act
Translation translation = new("pt-BR", "key", text);
@@ -128,20 +131,20 @@ namespace SMAPI.Tests.Core
}
[Test(Description = "Assert that the translation's implicit string conversion returns the expected text for various inputs.")]
- public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text)
+ public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string? text)
{
// act
Translation translation = new("pt-BR", "key", text);
// assert
if (translation.HasValue())
- Assert.AreEqual(text, (string)translation, "The translation returned an unexpected value given a valid input.");
+ Assert.AreEqual(text, (string?)translation, "The translation returned an unexpected value given a valid input.");
else
- Assert.AreEqual(this.GetPlaceholderText("key"), (string)translation, "The translation returned an unexpected value given a null or empty input.");
+ Assert.AreEqual(this.GetPlaceholderText("key"), (string?)translation, "The translation returned an unexpected value given a null or empty input.");
}
[Test(Description = "Assert that the translation returns the expected text given a use-placeholder setting.")]
- public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text)
+ public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string? text)
{
// act
Translation translation = new Translation("pt-BR", "key", text).UsePlaceholder(value);
@@ -156,7 +159,7 @@ namespace SMAPI.Tests.Core
}
[Test(Description = "Assert that the translation returns the expected text after setting the default.")]
- public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default)
+ public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string? text, [ValueSource(nameof(TranslationTests.Samples))] string? @default)
{
// act
Translation translation = new Translation("pt-BR", "key", text).Default(@default);
@@ -192,7 +195,7 @@ namespace SMAPI.Tests.Core
break;
case "class":
- translation = translation.Tokens(new TokenModel { Start = start, Middle = middle, End = end });
+ translation = translation.Tokens(new TokenModel(start, middle, end));
break;
case "IDictionary<string, object>":
@@ -326,21 +329,63 @@ namespace SMAPI.Tests.Core
return string.Format(Translation.PlaceholderText, key);
}
+ /// <summary>Create a fake mod manifest.</summary>
+ private IModMetadata CreateModMetadata()
+ {
+ string id = $"smapi.unit-tests.fake-mod-{Guid.NewGuid():N}";
+
+ string tempPath = Path.Combine(Path.GetTempPath(), id);
+ return new ModMetadata(
+ displayName: "Mod Display Name",
+ directoryPath: tempPath,
+ rootPath: tempPath,
+ manifest: new Manifest(
+ uniqueID: id,
+ name: "Mod Name",
+ author: "Mod Author",
+ description: "Mod Description",
+ version: new SemanticVersion(1, 0, 0)
+ ),
+ dataRecord: null,
+ isIgnored: false
+ );
+ }
+
/*********
** Test models
*********/
/// <summary>A model used to test token support.</summary>
+ [SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "Used dynamically via translation helper.")]
+ [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used dynamically via translation helper.")]
private class TokenModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>A sample token property.</summary>
- public string Start { get; set; }
+ public string Start { get; }
/// <summary>A sample token property.</summary>
- public string Middle { get; set; }
+ public string Middle { get; }
/// <summary>A sample token field.</summary>
public string End;
+
+
+ /*********
+ ** public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="start">A sample token property.</param>
+ /// <param name="middle">A sample token field.</param>
+ /// <param name="end">A sample token property.</param>
+ public TokenModel(string start, string middle, string end)
+ {
+ this.Start = start;
+ this.Middle = middle;
+ this.End = end;
+ }
}
}
}
diff --git a/src/SMAPI.Tests/Sample.cs b/src/SMAPI.Tests/Sample.cs
index 6d4339ca..9587a100 100644
--- a/src/SMAPI.Tests/Sample.cs
+++ b/src/SMAPI.Tests/Sample.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace SMAPI.Tests
diff --git a/src/SMAPI.Tests/Utilities/KeybindListTests.cs b/src/SMAPI.Tests/Utilities/KeybindListTests.cs
index f5c156c4..c4c086de 100644
--- a/src/SMAPI.Tests/Utilities/KeybindListTests.cs
+++ b/src/SMAPI.Tests/Utilities/KeybindListTests.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using NUnit.Framework;
@@ -23,12 +21,12 @@ namespace SMAPI.Tests.Utilities
public void TryParse_SimpleValue(SButton button)
{
// act
- bool success = KeybindList.TryParse($"{button}", out KeybindList parsed, out string[] errors);
+ bool success = KeybindList.TryParse($"{button}", out KeybindList? parsed, out string[] errors);
// assert
Assert.IsTrue(success, "Parsing unexpectedly failed.");
Assert.IsNotNull(parsed, "The parsed result should not be null.");
- Assert.AreEqual(parsed.ToString(), $"{button}");
+ Assert.AreEqual(parsed!.ToString(), $"{button}");
Assert.IsNotNull(errors, message: "The errors should never be null.");
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
}
@@ -46,17 +44,17 @@ namespace SMAPI.Tests.Utilities
[TestCase(",", ExpectedResult = "None")]
[TestCase("A,", ExpectedResult = "A")]
[TestCase(",A", ExpectedResult = "A")]
- public string TryParse_MultiValues(string input)
+ public string TryParse_MultiValues(string? input)
{
// act
- bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
+ bool success = KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors);
// assert
Assert.IsTrue(success, "Parsing unexpectedly failed.");
Assert.IsNotNull(parsed, "The parsed result should not be null.");
Assert.IsNotNull(errors, message: "The errors should never be null.");
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
- return parsed.ToString();
+ return parsed!.ToString();
}
/// <summary>Assert invalid values are rejected.</summary>
@@ -69,7 +67,7 @@ namespace SMAPI.Tests.Utilities
public void TryParse_InvalidValues(string input, string expectedError)
{
// act
- bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
+ bool success = KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors);
// assert
Assert.IsFalse(success, "Parsing unexpectedly succeeded.");
@@ -100,13 +98,15 @@ namespace SMAPI.Tests.Utilities
public SButtonState GetState(string input, string stateMap)
{
// act
- bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
+ bool success = KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors);
if (success && parsed?.Keybinds != null)
{
- foreach (var keybind in parsed.Keybinds)
+ foreach (Keybind? keybind in parsed.Keybinds)
+ {
#pragma warning disable 618 // method is marked obsolete because it should only be used in unit tests
keybind.GetButtonState = key => this.GetStateFromMap(key, stateMap);
#pragma warning restore 618
+ }
}
// assert
@@ -114,7 +114,7 @@ namespace SMAPI.Tests.Utilities
Assert.IsNotNull(parsed, "The parsed result should not be null.");
Assert.IsNotNull(errors, message: "The errors should never be null.");
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
- return parsed.GetState();
+ return parsed!.GetState();
}
diff --git a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs
index ae2cc6ce..3219d89d 100644
--- a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs
+++ b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using NUnit.Framework;
using StardewModdingAPI.Toolkit.Utilities;
@@ -8,6 +7,7 @@ namespace SMAPI.Tests.Utilities
{
/// <summary>Unit tests for <see cref="PathUtilities"/>.</summary>
[TestFixture]
+ [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are standard game install paths.")]
internal class PathUtilitiesTests
{
/*********
@@ -16,136 +16,125 @@ namespace SMAPI.Tests.Utilities
/// <summary>Sample paths used in unit tests.</summary>
public static readonly SamplePath[] SamplePaths = {
// Windows absolute path
- new()
- {
- OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
+ new(
+ OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
- Segments = new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
- SegmentsLimit3 = new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley" },
+ Segments: new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
+ SegmentsLimit3: new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley" },
- NormalizedOnWindows = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
- NormalizedOnUnix = @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley"
- },
+ NormalizedOnWindows: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
+ NormalizedOnUnix: @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley"
+ ),
// Windows absolute path (with trailing slash)
- new()
- {
- OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
+ new(
+ OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
- Segments = new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
- SegmentsLimit3 = new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley\" },
+ Segments: new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
+ SegmentsLimit3: new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley\" },
- NormalizedOnWindows = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
- NormalizedOnUnix = @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley/"
- },
+ NormalizedOnWindows: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
+ NormalizedOnUnix: @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley/"
+ ),
// Windows relative path
- new()
- {
- OriginalPath = @"Content\Characters\Dialogue\Abigail",
+ new(
+ OriginalPath: @"Content\Characters\Dialogue\Abigail",
- Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" },
- SegmentsLimit3 = new [] { "Content", "Characters", @"Dialogue\Abigail" },
+ Segments: new [] { "Content", "Characters", "Dialogue", "Abigail" },
+ SegmentsLimit3: new [] { "Content", "Characters", @"Dialogue\Abigail" },
- NormalizedOnWindows = @"Content\Characters\Dialogue\Abigail",
- NormalizedOnUnix = @"Content/Characters/Dialogue/Abigail"
- },
+ NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
+ NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
+ ),
// Windows relative path (with directory climbing)
- new()
- {
- OriginalPath = @"..\..\Content",
+ new(
+ OriginalPath: @"..\..\Content",
- Segments = new [] { "..", "..", "Content" },
- SegmentsLimit3 = new [] { "..", "..", "Content" },
+ Segments: new [] { "..", "..", "Content" },
+ SegmentsLimit3: new [] { "..", "..", "Content" },
- NormalizedOnWindows = @"..\..\Content",
- NormalizedOnUnix = @"../../Content"
- },
+ NormalizedOnWindows: @"..\..\Content",
+ NormalizedOnUnix: @"../../Content"
+ ),
// Windows UNC path
- new()
- {
- OriginalPath = @"\\unc\path",
+ new(
+ OriginalPath: @"\\unc\path",
- Segments = new [] { "unc", "path" },
- SegmentsLimit3 = new [] { "unc", "path" },
+ Segments: new [] { "unc", "path" },
+ SegmentsLimit3: new [] { "unc", "path" },
- NormalizedOnWindows = @"\\unc\path",
- NormalizedOnUnix = "/unc/path" // there's no good way to normalize this on Unix since UNC paths aren't supported; path normalization is meant for asset names anyway, so this test only ensures it returns some sort of sane value
- },
+ NormalizedOnWindows: @"\\unc\path",
+ NormalizedOnUnix: "/unc/path" // there's no good way to normalize this on Unix since UNC paths aren't supported; path normalization is meant for asset names anyway, so this test only ensures it returns some sort of sane value
+ ),
// Linux absolute path
- new()
- {
- OriginalPath = @"/home/.steam/steam/steamapps/common/Stardew Valley",
+ new(
+ OriginalPath: @"/home/.steam/steam/steamapps/common/Stardew Valley",
- Segments = new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
- SegmentsLimit3 = new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley" },
+ Segments: new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
+ SegmentsLimit3: new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley" },
- NormalizedOnWindows = @"\home\.steam\steam\steamapps\common\Stardew Valley",
- NormalizedOnUnix = @"/home/.steam/steam/steamapps/common/Stardew Valley"
- },
+ NormalizedOnWindows: @"\home\.steam\steam\steamapps\common\Stardew Valley",
+ NormalizedOnUnix: @"/home/.steam/steam/steamapps/common/Stardew Valley"
+ ),
// Linux absolute path (with trailing slash)
- new()
- {
- OriginalPath = @"/home/.steam/steam/steamapps/common/Stardew Valley/",
+ new(
+ OriginalPath: @"/home/.steam/steam/steamapps/common/Stardew Valley/",
- Segments = new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
- SegmentsLimit3 = new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley/" },
+ Segments: new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
+ SegmentsLimit3: new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley/" },
- NormalizedOnWindows = @"\home\.steam\steam\steamapps\common\Stardew Valley\",
- NormalizedOnUnix = @"/home/.steam/steam/steamapps/common/Stardew Valley/"
- },
+ NormalizedOnWindows: @"\home\.steam\steam\steamapps\common\Stardew Valley\",
+ NormalizedOnUnix: @"/home/.steam/steam/steamapps/common/Stardew Valley/"
+ ),
// Linux absolute path (with ~)
- new()
- {
- OriginalPath = @"~/.steam/steam/steamapps/common/Stardew Valley",
+ new(
+ OriginalPath: @"~/.steam/steam/steamapps/common/Stardew Valley",
- Segments = new [] { "~", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
- SegmentsLimit3 = new [] { "~", ".steam", "steam/steamapps/common/Stardew Valley" },
+ Segments: new [] { "~", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
+ SegmentsLimit3: new [] { "~", ".steam", "steam/steamapps/common/Stardew Valley" },
- NormalizedOnWindows = @"~\.steam\steam\steamapps\common\Stardew Valley",
- NormalizedOnUnix = @"~/.steam/steam/steamapps/common/Stardew Valley"
- },
+ NormalizedOnWindows: @"~\.steam\steam\steamapps\common\Stardew Valley",
+ NormalizedOnUnix: @"~/.steam/steam/steamapps/common/Stardew Valley"
+ ),
// Linux relative path
- new()
- {
- OriginalPath = @"Content/Characters/Dialogue/Abigail",
+ new(
+ OriginalPath: @"Content/Characters/Dialogue/Abigail",
- Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" },
- SegmentsLimit3 = new [] { "Content", "Characters", "Dialogue/Abigail" },
+ Segments: new [] { "Content", "Characters", "Dialogue", "Abigail" },
+ SegmentsLimit3: new [] { "Content", "Characters", "Dialogue/Abigail" },
- NormalizedOnWindows = @"Content\Characters\Dialogue\Abigail",
- NormalizedOnUnix = @"Content/Characters/Dialogue/Abigail"
- },
+ NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
+ NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
+ ),
// Linux relative path (with directory climbing)
- new()
- {
- OriginalPath = @"../../Content",
+ new(
+ OriginalPath: @"../../Content",
- Segments = new [] { "..", "..", "Content" },
- SegmentsLimit3 = new [] { "..", "..", "Content" },
+ Segments: new [] { "..", "..", "Content" },
+ SegmentsLimit3: new [] { "..", "..", "Content" },
- NormalizedOnWindows = @"..\..\Content",
- NormalizedOnUnix = @"../../Content"
- },
+ NormalizedOnWindows: @"..\..\Content",
+ NormalizedOnUnix: @"../../Content"
+ ),
// Mixed directory separators
- new()
- {
- OriginalPath = @"C:\some/mixed\path/separators",
+ new(
+ OriginalPath: @"C:\some/mixed\path/separators",
- Segments = new [] { "C:", "some", "mixed", "path", "separators" },
- SegmentsLimit3 = new [] { "C:", "some", @"mixed\path/separators" },
+ Segments: new [] { "C:", "some", "mixed", "path", "separators" },
+ SegmentsLimit3: new [] { "C:", "some", @"mixed\path/separators" },
- NormalizedOnWindows = @"C:\some\mixed\path\separators",
- NormalizedOnUnix = @"C:/some/mixed/path/separators"
- },
+ NormalizedOnWindows: @"C:\some\mixed\path\separators",
+ NormalizedOnUnix: @"C:/some/mixed/path/separators"
+ )
};
@@ -283,14 +272,14 @@ namespace SMAPI.Tests.Utilities
/*********
** Private classes
*********/
- public class SamplePath
+ /// <summary>A sample path in multiple formats.</summary>
+ /// <param name="OriginalPath">The original path to pass to the <see cref="PathUtilities"/>.</param>
+ /// <param name="Segments">The normalized path segments.</param>
+ /// <param name="SegmentsLimit3">The normalized path segments, if we stop segmenting after the second one.</param>
+ /// <param name="NormalizedOnWindows">The normalized form on Windows.</param>
+ /// <param name="NormalizedOnUnix">The normalized form on Linux or macOS.</param>
+ public record SamplePath(string OriginalPath, string[] Segments, string[] SegmentsLimit3, string NormalizedOnWindows, string NormalizedOnUnix)
{
- public string OriginalPath { get; set; }
- public string[] Segments { get; set; }
- public string[] SegmentsLimit3 { get; set; }
- public string NormalizedOnWindows { get; set; }
- public string NormalizedOnUnix { get; set; }
-
public override string ToString()
{
return this.OriginalPath;
diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs
index a4a36828..b9c3d202 100644
--- a/src/SMAPI.Tests/Utilities/SDateTests.cs
+++ b/src/SMAPI.Tests/Utilities/SDateTests.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -258,7 +256,7 @@ namespace SMAPI.Tests.Utilities
{
SDate date = new(day, season, year);
int hash = date.GetHashCode();
- if (hashes.TryGetValue(hash, out SDate otherDate))
+ if (hashes.TryGetValue(hash, out SDate? otherDate))
Assert.Fail($"Received identical hash code {hash} for dates {otherDate} and {date}.");
if (hash < lastHash)
Assert.Fail($"Received smaller hash code for date {date} ({hash}) relative to {hashes[lastHash]} ({lastHash}).");
@@ -298,7 +296,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
- public bool Operators_Equals(string now, string other)
+ public bool Operators_Equals(string? now, string other)
{
return this.GetDate(now) == this.GetDate(other);
}
@@ -312,7 +310,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
- public bool Operators_NotEquals(string now, string other)
+ public bool Operators_NotEquals(string? now, string other)
{
return this.GetDate(now) != this.GetDate(other);
}
@@ -326,7 +324,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
- public bool Operators_LessThan(string now, string other)
+ public bool Operators_LessThan(string? now, string other)
{
return this.GetDate(now) < this.GetDate(other);
}
@@ -340,7 +338,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
- public bool Operators_LessThanOrEqual(string now, string other)
+ public bool Operators_LessThanOrEqual(string? now, string other)
{
return this.GetDate(now) <= this.GetDate(other);
}
@@ -354,7 +352,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
- public bool Operators_MoreThan(string now, string other)
+ public bool Operators_MoreThan(string? now, string other)
{
return this.GetDate(now) > this.GetDate(other);
}
@@ -368,7 +366,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
- public bool Operators_MoreThanOrEqual(string now, string other)
+ public bool Operators_MoreThanOrEqual(string? now, string other)
{
return this.GetDate(now) > this.GetDate(other);
}
@@ -379,7 +377,8 @@ namespace SMAPI.Tests.Utilities
*********/
/// <summary>Convert a string date into a game date, to make unit tests easier to read.</summary>
/// <param name="dateStr">The date string like "dd MMMM yy".</param>
- private SDate GetDate(string dateStr)
+ [return: NotNullIfNotNull("dateStr")]
+ private SDate? GetDate(string? dateStr)
{
if (dateStr == null)
return null;
diff --git a/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs b/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs
index 7695fbf8..8e7e1fb8 100644
--- a/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs
+++ b/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using NUnit.Framework;
using StardewModdingAPI;
@@ -22,15 +20,14 @@ namespace SMAPI.Tests.WikiClient
{
// arrange
string rawDescriptor = "-Nexus:2400, -B, XX → YY, Nexus:451,+A, XXX → YYY, invalidA →, → invalidB";
- string[] expectedAdd = new[] { "Nexus:451", "A" };
- string[] expectedRemove = new[] { "Nexus:2400", "B" };
+ string[] expectedAdd = { "Nexus:451", "A" };
+ string[] expectedRemove = { "Nexus:2400", "B" };
IDictionary<string, string> expectedReplace = new Dictionary<string, string>
{
["XX"] = "YY",
["XXX"] = "YYY"
};
- string[] expectedErrors = new[]
- {
+ string[] expectedErrors = {
"Failed parsing ' invalidA →': can't map to a blank value. Use the '-value' format to remove a value.",
"Failed parsing ' → invalidB': can't map from a blank old value. Use the '+value' format to add a value."
};
@@ -50,15 +47,14 @@ namespace SMAPI.Tests.WikiClient
{
// arrange
string rawDescriptor = "-1.0.1, -2.0-beta, 1.00 → 1.0, 1.0.0,+2.0-beta.15, 2.0 → 2.0-beta, invalidA →, → invalidB";
- string[] expectedAdd = new[] { "1.0.0", "2.0.0-beta.15" };
- string[] expectedRemove = new[] { "1.0.1", "2.0.0-beta" };
+ string[] expectedAdd = { "1.0.0", "2.0.0-beta.15" };
+ string[] expectedRemove = { "1.0.1", "2.0.0-beta" };
IDictionary<string, string> expectedReplace = new Dictionary<string, string>
{
["1.00"] = "1.0.0",
["2.0.0"] = "2.0.0-beta"
};
- string[] expectedErrors = new[]
- {
+ string[] expectedErrors = {
"Failed parsing ' invalidA →': can't map to a blank value. Use the '-value' format to remove a value.",
"Failed parsing ' → invalidB': can't map from a blank old value. Use the '+value' format to add a value."
};
@@ -67,7 +63,7 @@ namespace SMAPI.Tests.WikiClient
ChangeDescriptor parsed = ChangeDescriptor.Parse(
rawDescriptor,
out string[] errors,
- formatValue: raw => SemanticVersion.TryParse(raw, out ISemanticVersion version)
+ formatValue: raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version)
? version.ToString()
: raw
);
@@ -111,9 +107,9 @@ namespace SMAPI.Tests.WikiClient
[TestCase("", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:A, Nexus:B")]
[TestCase("Nexus:2400", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:2401, Nexus:A, Nexus:B")]
[TestCase("Nexus:2400, Nexus:2401, Nexus:B,Chucklefish:14", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:2401, Nexus:2401, Nexus:B, Nexus:A")]
- public string Apply_Raw(string input, string descriptor)
+ public string Apply_Raw(string input, string? descriptor)
{
- var parsed = ChangeDescriptor.Parse(descriptor, out string[] errors);
+ ChangeDescriptor parsed = ChangeDescriptor.Parse(descriptor, out string[] errors);
Assert.IsEmpty(errors, "Parsing the descriptor failed.");
@@ -128,7 +124,7 @@ namespace SMAPI.Tests.WikiClient
[TestCase("-Nexus:2400", ExpectedResult = "-Nexus:2400")]
[TestCase(" Nexus:2400 →Nexus:2401 ", ExpectedResult = "Nexus:2400 → Nexus:2401")]
[TestCase("+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "+Nexus:A, +Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A → Nexus:B")]
- public string ToString(string descriptor)
+ public string ToString(string? descriptor)
{
var parsed = ChangeDescriptor.Parse(descriptor, out string[] errors);
diff --git a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs
index 7998272f..dc226b7c 100644
--- a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs
+++ b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI
{
@@ -28,6 +29,9 @@ namespace StardewModdingAPI
** Accessors
*********/
/// <summary>Whether this is a prerelease version.</summary>
+#if NET5_0_OR_GREATER
+ [MemberNotNullWhen(true, nameof(ISemanticVersion.PrereleaseTag))]
+#endif
bool IsPrerelease();
/// <summary>Get whether this version is older than the specified version.</summary>
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
index d5ca2034..4fc4ea54 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
@@ -11,15 +9,26 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
** Accessors
*********/
/// <summary>The mod's unique ID (if known).</summary>
- public string ID { get; set; }
+ public string ID { get; }
/// <summary>The update version recommended by the web API based on its version update and mapping rules.</summary>
- public ModEntryVersionModel SuggestedUpdate { get; set; }
+ public ModEntryVersionModel? SuggestedUpdate { get; set; }
/// <summary>Optional extended data which isn't needed for update checks.</summary>
- public ModExtendedMetadataModel Metadata { get; set; }
+ public ModExtendedMetadataModel? Metadata { get; set; }
/// <summary>The errors that occurred while fetching update data.</summary>
public string[] Errors { get; set; } = Array.Empty<string>();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The mod's unique ID (if known).</param>
+ public ModEntryModel(string id)
+ {
+ this.ID = id;
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs
index 9aac7fd3..a1e78986 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialization.Converters;
@@ -13,19 +11,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
*********/
/// <summary>The version number.</summary>
[JsonConverter(typeof(NonStandardSemanticVersionConverter))]
- public ISemanticVersion Version { get; set; }
+ public ISemanticVersion Version { get; }
/// <summary>The mod page URL.</summary>
- public string Url { get; set; }
+ public string Url { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public ModEntryVersionModel() { }
-
- /// <summary>Construct an instance.</summary>
/// <param name="version">The version number.</param>
/// <param name="url">The mod page URL.</param>
public ModEntryVersionModel(ISemanticVersion version, string url)
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
index eb54ec78..272a2063 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -23,7 +21,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
public string[] ID { get; set; } = Array.Empty<string>();
/// <summary>The mod's display name.</summary>
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>The mod ID on Nexus.</summary>
public int? NexusID { get; set; }
@@ -35,31 +33,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
public int? CurseForgeID { get; set; }
/// <summary>The mod key in the CurseForge mod repo (used in mod page URLs).</summary>
- public string CurseForgeKey { get; set; }
+ public string? CurseForgeKey { get; set; }
/// <summary>The mod ID in the ModDrop mod repo.</summary>
public int? ModDropID { get; set; }
/// <summary>The GitHub repository in the form 'owner/repo'.</summary>
- public string GitHubRepo { get; set; }
+ public string? GitHubRepo { get; set; }
/// <summary>The URL to a non-GitHub source repo.</summary>
- public string CustomSourceUrl { get; set; }
+ public string? CustomSourceUrl { get; set; }
/// <summary>The custom mod page URL (if applicable).</summary>
- public string CustomUrl { get; set; }
+ public string? CustomUrl { get; set; }
/// <summary>The main version.</summary>
- public ModEntryVersionModel Main { get; set; }
+ public ModEntryVersionModel? Main { get; set; }
/// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary>
- public ModEntryVersionModel Optional { get; set; }
+ public ModEntryVersionModel? Optional { get; set; }
/// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary>
- public ModEntryVersionModel Unofficial { get; set; }
+ public ModEntryVersionModel? Unofficial { get; set; }
/// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any.</summary>
- public ModEntryVersionModel UnofficialForBeta { get; set; }
+ public ModEntryVersionModel? UnofficialForBeta { get; set; }
/****
** Stable compatibility
@@ -69,10 +67,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
public WikiCompatibilityStatus? CompatibilityStatus { get; set; }
/// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
- public string CompatibilitySummary { get; set; }
+ public string? CompatibilitySummary { get; set; }
/// <summary>The game or SMAPI version which broke this mod, if applicable.</summary>
- public string BrokeIn { get; set; }
+ public string? BrokeIn { get; set; }
/****
** Beta compatibility
@@ -82,22 +80,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; }
/// <summary>The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatting.</summary>
- public string BetaCompatibilitySummary { get; set; }
+ public string? BetaCompatibilitySummary { get; set; }
/// <summary>The beta game or SMAPI version which broke this mod, if applicable.</summary>
- public string BetaBrokeIn { get; set; }
+ public string? BetaBrokeIn { get; set; }
/****
** Version mappings
****/
/// <summary>A serialized change descriptor to apply to the local version during update checks (see <see cref="ChangeDescriptor"/>).</summary>
- public string ChangeLocalVersions { get; set; }
+ public string? ChangeLocalVersions { get; set; }
/// <summary>A serialized change descriptor to apply to the remote version during update checks (see <see cref="ChangeDescriptor"/>).</summary>
- public string ChangeRemoteVersions { get; set; }
+ public string? ChangeRemoteVersions { get; set; }
/// <summary>A serialized change descriptor to apply to the update keys during update checks (see <see cref="ChangeDescriptor"/>).</summary>
- public string ChangeUpdateKeys { get; set; }
+ public string? ChangeUpdateKeys { get; set; }
/*********
@@ -113,7 +111,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <param name="optional">The latest optional version, if newer than <paramref name="main"/>.</param>
/// <param name="unofficial">The latest unofficial version, if newer than <paramref name="main"/> and <paramref name="optional"/>.</param>
/// <param name="unofficialForBeta">The latest unofficial version for the current Stardew Valley or SMAPI beta, if any.</param>
- public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db, ModEntryVersionModel main, ModEntryVersionModel optional, ModEntryVersionModel unofficial, ModEntryVersionModel unofficialForBeta)
+ public ModExtendedMetadataModel(WikiModEntry? wiki, ModDataRecord? db, ModEntryVersionModel? main, ModEntryVersionModel? optional, ModEntryVersionModel? unofficial, ModEntryVersionModel? unofficialForBeta)
{
// versions
this.Main = main;
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs
index 8fe8fa2a..9c11e1db 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System;
+using System.Linq;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{
@@ -11,37 +10,39 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
** Accessors
*********/
/// <summary>The unique mod ID.</summary>
- public string ID { get; set; }
+ public string ID { get; }
/// <summary>The namespaced mod update keys (if available).</summary>
- public string[] UpdateKeys { get; set; }
+ public string[] UpdateKeys { get; private set; }
/// <summary>The mod version installed by the local player. This is used for version mapping in some cases.</summary>
- public ISemanticVersion InstalledVersion { get; set; }
+ public ISemanticVersion? InstalledVersion { get; }
/// <summary>Whether the installed version is broken or could not be loaded.</summary>
- public bool IsBroken { get; set; }
+ public bool IsBroken { get; }
/*********
** Public methods
*********/
- /// <summary>Construct an empty instance.</summary>
- public ModSearchEntryModel()
- {
- // needed for JSON deserializing
- }
-
/// <summary>Construct an instance.</summary>
/// <param name="id">The unique mod ID.</param>
/// <param name="installedVersion">The version installed by the local player. This is used for version mapping in some cases.</param>
/// <param name="updateKeys">The namespaced mod update keys (if available).</param>
/// <param name="isBroken">Whether the installed version is broken or could not be loaded.</param>
- public ModSearchEntryModel(string id, ISemanticVersion installedVersion, string[] updateKeys, bool isBroken = false)
+ public ModSearchEntryModel(string id, ISemanticVersion? installedVersion, string[]? updateKeys, bool isBroken = false)
{
this.ID = id;
this.InstalledVersion = installedVersion;
this.UpdateKeys = updateKeys ?? Array.Empty<string>();
+ this.IsBroken = isBroken;
+ }
+
+ /// <summary>Add update keys for the mod.</summary>
+ /// <param name="updateKeys">The update keys to add.</param>
+ public void AddUpdateKeys(params string[] updateKeys)
+ {
+ this.UpdateKeys = this.UpdateKeys.Concat(updateKeys).ToArray();
}
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs
index 393391f7..a0cd9d4d 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Linq;
using StardewModdingAPI.Toolkit.Utilities;
@@ -24,18 +22,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
public ISemanticVersion GameVersion { get; set; }
/// <summary>The OS on which the player plays.</summary>
- public Platform? Platform { get; set; }
+ public Platform Platform { get; set; }
/*********
** Public methods
*********/
- /// <summary>Construct an empty instance.</summary>
- public ModSearchModel()
- {
- // needed for JSON deserializing
- }
-
/// <summary>Construct an instance.</summary>
/// <param name="mods">The mods to search.</param>
/// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param>
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
index 56acb768..d4282617 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -72,7 +70,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
client.Headers["Content-Type"] = "application/json";
client.Headers["User-Agent"] = $"SMAPI/{this.Version}";
string response = client.UploadString(fullUrl, data);
- return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings);
+ return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings)
+ ?? throw new InvalidOperationException($"Could not parse the response from POST {url}.");
}
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs
index 910bf793..8646f1cc 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs
@@ -1,8 +1,7 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
@@ -49,7 +48,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>Apply the change descriptors to a comma-delimited field.</summary>
/// <param name="rawField">The raw field text.</param>
/// <returns>Returns the modified field.</returns>
- public string ApplyToCopy(string rawField)
+#if NET5_0_OR_GREATER
+ [return: NotNullIfNotNull("rawField")]
+#endif
+ public string? ApplyToCopy(string? rawField)
{
// get list
List<string> values = !string.IsNullOrWhiteSpace(rawField)
@@ -75,12 +77,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
for (int i = values.Count - 1; i >= 0; i--)
{
- string value = this.FormatValue(values[i]?.Trim() ?? string.Empty);
+ string value = this.FormatValue(values[i].Trim());
if (this.Remove.Contains(value))
values.RemoveAt(i);
- else if (this.Replace.TryGetValue(value, out string newValue))
+ else if (this.Replace.TryGetValue(value, out string? newValue))
values[i] = newValue;
}
}
@@ -88,7 +90,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
// add values
if (this.Add.Any())
{
- HashSet<string> curValues = new HashSet<string>(values.Select(p => p?.Trim() ?? string.Empty), StringComparer.OrdinalIgnoreCase);
+ HashSet<string> curValues = new HashSet<string>(values.Select(p => p.Trim()), StringComparer.OrdinalIgnoreCase);
foreach (string add in this.Add)
{
if (!curValues.Contains(add))
@@ -121,7 +123,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="descriptor">The raw change descriptor.</param>
/// <param name="errors">The human-readable error message describing any invalid values that were ignored.</param>
/// <param name="formatValue">Format a raw value into a normalized form if needed.</param>
- public static ChangeDescriptor Parse(string descriptor, out string[] errors, Func<string, string> formatValue = null)
+ public static ChangeDescriptor Parse(string? descriptor, out string[] errors, Func<string, string>? formatValue = null)
{
// init
formatValue ??= p => p;
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
index 86c3bd75..7f06d170 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -53,8 +51,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
doc.LoadHtml(html);
// fetch game versions
- string stableVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-stable-version']")?.InnerText;
- string betaVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-beta-version']")?.InnerText;
+ string? stableVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-stable-version']")?.InnerText;
+ string? betaVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-beta-version']")?.InnerText;
if (betaVersion == stableVersion)
betaVersion = null;
@@ -65,7 +63,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
if (modNodes == null)
throw new InvalidOperationException("Can't parse wiki compatibility list, no mod data overrides section found.");
- foreach (var entry in this.ParseOverrideEntries(modNodes))
+ foreach (WikiDataOverrideEntry entry in this.ParseOverrideEntries(modNodes))
{
if (entry.Ids.Any() != true || !entry.HasChanges)
continue;
@@ -85,18 +83,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
}
// build model
- return new WikiModList
- {
- StableVersion = stableVersion,
- BetaVersion = betaVersion,
- Mods = mods
- };
+ return new WikiModList(
+ stableVersion: stableVersion,
+ betaVersion: betaVersion,
+ mods: mods
+ );
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.Client?.Dispose();
+ this.Client.Dispose();
}
@@ -118,71 +115,68 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id");
int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id");
int? curseForgeID = this.GetAttributeAsNullableInt(node, "data-curseforge-id");
- string curseForgeKey = this.GetAttribute(node, "data-curseforge-key");
+ string? curseForgeKey = this.GetAttribute(node, "data-curseforge-key");
int? modDropID = this.GetAttributeAsNullableInt(node, "data-moddrop-id");
- string githubRepo = this.GetAttribute(node, "data-github");
- string customSourceUrl = this.GetAttribute(node, "data-custom-source");
- string customUrl = this.GetAttribute(node, "data-url");
- string anchor = this.GetAttribute(node, "id");
- string contentPackFor = this.GetAttribute(node, "data-content-pack-for");
- string devNote = this.GetAttribute(node, "data-dev-note");
- string pullRequestUrl = this.GetAttribute(node, "data-pr");
+ string? githubRepo = this.GetAttribute(node, "data-github");
+ string? customSourceUrl = this.GetAttribute(node, "data-custom-source");
+ string? customUrl = this.GetAttribute(node, "data-url");
+ string? anchor = this.GetAttribute(node, "id");
+ string? contentPackFor = this.GetAttribute(node, "data-content-pack-for");
+ string? devNote = this.GetAttribute(node, "data-dev-note");
+ string? pullRequestUrl = this.GetAttribute(node, "data-pr");
// parse stable compatibility
- WikiCompatibilityInfo compatibility = new()
- {
- Status = this.GetAttributeAsEnum<WikiCompatibilityStatus>(node, "data-status") ?? WikiCompatibilityStatus.Ok,
- BrokeIn = this.GetAttribute(node, "data-broke-in"),
- UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-unofficial-version"),
- UnofficialUrl = this.GetAttribute(node, "data-unofficial-url"),
- Summary = this.GetInnerHtml(node, "mod-summary")?.Trim()
- };
+ WikiCompatibilityInfo compatibility = new(
+ status: this.GetAttributeAsEnum<WikiCompatibilityStatus>(node, "data-status") ?? WikiCompatibilityStatus.Ok,
+ brokeIn: this.GetAttribute(node, "data-broke-in"),
+ unofficialVersion: this.GetAttributeAsSemanticVersion(node, "data-unofficial-version"),
+ unofficialUrl: this.GetAttribute(node, "data-unofficial-url"),
+ summary: this.GetInnerHtml(node, "mod-summary")?.Trim()
+ );
// parse beta compatibility
- WikiCompatibilityInfo betaCompatibility = null;
+ WikiCompatibilityInfo? betaCompatibility = null;
{
WikiCompatibilityStatus? betaStatus = this.GetAttributeAsEnum<WikiCompatibilityStatus>(node, "data-beta-status");
if (betaStatus.HasValue)
{
- betaCompatibility = new WikiCompatibilityInfo
- {
- Status = betaStatus.Value,
- BrokeIn = this.GetAttribute(node, "data-beta-broke-in"),
- UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-beta-unofficial-version"),
- UnofficialUrl = this.GetAttribute(node, "data-beta-unofficial-url"),
- Summary = this.GetInnerHtml(node, "mod-beta-summary")
- };
+ betaCompatibility = new WikiCompatibilityInfo(
+ status: betaStatus.Value,
+ brokeIn: this.GetAttribute(node, "data-beta-broke-in"),
+ unofficialVersion: this.GetAttributeAsSemanticVersion(node, "data-beta-unofficial-version"),
+ unofficialUrl: this.GetAttribute(node, "data-beta-unofficial-url"),
+ summary: this.GetInnerHtml(node, "mod-beta-summary")
+ );
}
}
// find data overrides
- WikiDataOverrideEntry overrides = ids
+ WikiDataOverrideEntry? overrides = ids
.Select(id => overridesById.TryGetValue(id, out overrides) ? overrides : null)
.FirstOrDefault(p => p != null);
// yield model
- yield return new WikiModEntry
- {
- ID = ids,
- Name = names,
- Author = authors,
- NexusID = nexusID,
- ChucklefishID = chucklefishID,
- CurseForgeID = curseForgeID,
- CurseForgeKey = curseForgeKey,
- ModDropID = modDropID,
- GitHubRepo = githubRepo,
- CustomSourceUrl = customSourceUrl,
- CustomUrl = customUrl,
- ContentPackFor = contentPackFor,
- Compatibility = compatibility,
- BetaCompatibility = betaCompatibility,
- Warnings = warnings,
- PullRequestUrl = pullRequestUrl,
- DevNote = devNote,
- Overrides = overrides,
- Anchor = anchor
- };
+ yield return new WikiModEntry(
+ id: ids,
+ name: names,
+ author: authors,
+ nexusId: nexusID,
+ chucklefishId: chucklefishID,
+ curseForgeId: curseForgeID,
+ curseForgeKey: curseForgeKey,
+ modDropId: modDropID,
+ githubRepo: githubRepo,
+ customSourceUrl: customSourceUrl,
+ customUrl: customUrl,
+ contentPackFor: contentPackFor,
+ compatibility: compatibility,
+ betaCompatibility: betaCompatibility,
+ warnings: warnings,
+ pullRequestUrl: pullRequestUrl,
+ devNote: devNote,
+ overrides: overrides,
+ anchor: anchor
+ );
}
}
@@ -196,10 +190,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
Ids = this.GetAttributeAsCsv(node, "data-id"),
ChangeLocalVersions = this.GetAttributeAsChangeDescriptor(node, "data-local-version",
- raw => SemanticVersion.TryParse(raw, out ISemanticVersion version) ? version.ToString() : raw
+ raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw
),
ChangeRemoteVersions = this.GetAttributeAsChangeDescriptor(node, "data-remote-version",
- raw => SemanticVersion.TryParse(raw, out ISemanticVersion version) ? version.ToString() : raw
+ raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw
),
ChangeUpdateKeys = this.GetAttributeAsChangeDescriptor(node, "data-update-keys",
@@ -212,7 +206,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>Get an attribute value.</summary>
/// <param name="element">The element whose attributes to read.</param>
/// <param name="name">The attribute name.</param>
- private string GetAttribute(HtmlNode element, string name)
+ private string? GetAttribute(HtmlNode element, string name)
{
string value = element.GetAttributeValue(name, null);
if (string.IsNullOrWhiteSpace(value))
@@ -225,9 +219,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="element">The element whose attributes to read.</param>
/// <param name="name">The attribute name.</param>
/// <param name="formatValue">Format an raw entry value when applying changes.</param>
- private ChangeDescriptor GetAttributeAsChangeDescriptor(HtmlNode element, string name, Func<string, string> formatValue)
+ private ChangeDescriptor? GetAttributeAsChangeDescriptor(HtmlNode element, string name, Func<string, string> formatValue)
{
- string raw = this.GetAttribute(element, name);
+ string? raw = this.GetAttribute(element, name);
return raw != null
? ChangeDescriptor.Parse(raw, out _, formatValue)
: null;
@@ -238,7 +232,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="name">The attribute name.</param>
private string[] GetAttributeAsCsv(HtmlNode element, string name)
{
- string raw = this.GetAttribute(element, name);
+ string? raw = this.GetAttribute(element, name);
return !string.IsNullOrWhiteSpace(raw)
? raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray()
: Array.Empty<string>();
@@ -250,7 +244,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="name">The attribute name.</param>
private TEnum? GetAttributeAsEnum<TEnum>(HtmlNode element, string name) where TEnum : struct
{
- string raw = this.GetAttribute(element, name);
+ string? raw = this.GetAttribute(element, name);
if (raw == null)
return null;
if (!Enum.TryParse(raw, true, out TEnum value) && Enum.IsDefined(typeof(TEnum), value))
@@ -261,10 +255,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>Get an attribute value and parse it as a semantic version.</summary>
/// <param name="element">The element whose attributes to read.</param>
/// <param name="name">The attribute name.</param>
- private ISemanticVersion GetAttributeAsSemanticVersion(HtmlNode element, string name)
+ private ISemanticVersion? GetAttributeAsSemanticVersion(HtmlNode element, string name)
{
- string raw = this.GetAttribute(element, name);
- return SemanticVersion.TryParse(raw, out ISemanticVersion version)
+ string? raw = this.GetAttribute(element, name);
+ return SemanticVersion.TryParse(raw, out ISemanticVersion? version)
? version
: null;
}
@@ -274,7 +268,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="name">The attribute name.</param>
private int? GetAttributeAsNullableInt(HtmlNode element, string name)
{
- string raw = this.GetAttribute(element, name);
+ string? raw = this.GetAttribute(element, name);
if (raw != null && int.TryParse(raw, out int value))
return value;
return null;
@@ -283,7 +277,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>Get the text of an element with the given class name.</summary>
/// <param name="container">The metadata container.</param>
/// <param name="className">The field name.</param>
- private string GetInnerHtml(HtmlNode container, string className)
+ private string? GetInnerHtml(HtmlNode container, string className)
{
return container.Descendants().FirstOrDefault(p => p.HasClass(className))?.InnerHtml;
}
@@ -293,8 +287,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
private class ResponseModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The parse API results.</summary>
- public ResponseParseModel Parse { get; set; }
+ public ResponseParseModel Parse { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="parse">The parse API results.</param>
+ public ResponseModel(ResponseParseModel parse)
+ {
+ this.Parse = parse;
+ }
}
/// <summary>The inner response model for the MediaWiki parse API.</summary>
@@ -303,8 +311,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
private class ResponseParseModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The parsed text.</summary>
- public IDictionary<string, string> Text { get; set; }
+ public IDictionary<string, string> Text { get; } = new Dictionary<string, string>();
}
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs
index 30e76d04..71c90d0c 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
/// <summary>Compatibility info for a mod.</summary>
@@ -9,18 +7,37 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
** Accessors
*********/
/// <summary>The compatibility status.</summary>
- public WikiCompatibilityStatus Status { get; set; }
+ public WikiCompatibilityStatus Status { get; }
/// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
- public string Summary { get; set; }
+ public string? Summary { get; }
- /// <summary>The game or SMAPI version which broke this mod (if applicable).</summary>
- public string BrokeIn { get; set; }
+ /// <summary>The game or SMAPI version which broke this mod, if applicable.</summary>
+ public string? BrokeIn { get; }
/// <summary>The version of the latest unofficial update, if applicable.</summary>
- public ISemanticVersion UnofficialVersion { get; set; }
+ public ISemanticVersion? UnofficialVersion { get; }
/// <summary>The URL to the latest unofficial update, if applicable.</summary>
- public string UnofficialUrl { get; set; }
+ public string? UnofficialUrl { get; }
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="status">The compatibility status.</param>
+ /// <param name="summary">The human-readable summary of the compatibility status or workaround, without HTML formatting.</param>
+ /// <param name="brokeIn">The game or SMAPI version which broke this mod, if applicable.</param>
+ /// <param name="unofficialVersion">The version of the latest unofficial update, if applicable.</param>
+ /// <param name="unofficialUrl">The URL to the latest unofficial update, if applicable.</param>
+ public WikiCompatibilityInfo(WikiCompatibilityStatus status, string? summary, string? brokeIn, ISemanticVersion? unofficialVersion, string? unofficialUrl)
+ {
+ this.Status = status;
+ this.Summary = summary;
+ this.BrokeIn = brokeIn;
+ this.UnofficialVersion = unofficialVersion;
+ this.UnofficialUrl = unofficialUrl;
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs
index 2c222b71..5cdf489f 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
/// <summary>The compatibility status for a mod.</summary>
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
index 91943ff9..fc50125f 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
@@ -1,4 +1,4 @@
-#nullable disable
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
@@ -8,64 +8,114 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/*********
** Accessors
*********/
- /// <summary>The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order.</summary>
- public string[] ID { get; set; }
+ /// <summary>The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to oldest order.</summary>
+ public string[] ID { get; }
/// <summary>The mod's display name. If the mod has multiple names, the first one is the most canonical name.</summary>
- public string[] Name { get; set; }
+ public string[] Name { get; }
- /// <summary>The mod's author name. If the author has multiple names, the first one is the most canonical name.</summary>
- public string[] Author { get; set; }
+ /// <summary>The mod's author name. If the author has multiple names, the first one is the most canonical name.</summary>
+ public string[] Author { get; }
/// <summary>The mod ID on Nexus.</summary>
- public int? NexusID { get; set; }
+ public int? NexusID { get; }
/// <summary>The mod ID in the Chucklefish mod repo.</summary>
- public int? ChucklefishID { get; set; }
+ public int? ChucklefishID { get; }
/// <summary>The mod ID in the CurseForge mod repo.</summary>
- public int? CurseForgeID { get; set; }
+ public int? CurseForgeID { get; }
/// <summary>The mod key in the CurseForge mod repo (used in mod page URLs).</summary>
- public string CurseForgeKey { get; set; }
+ public string? CurseForgeKey { get; }
/// <summary>The mod ID in the ModDrop mod repo.</summary>
- public int? ModDropID { get; set; }
+ public int? ModDropID { get; }
/// <summary>The GitHub repository in the form 'owner/repo'.</summary>
- public string GitHubRepo { get; set; }
+ public string? GitHubRepo { get; }
/// <summary>The URL to a non-GitHub source repo.</summary>
- public string CustomSourceUrl { get; set; }
+ public string? CustomSourceUrl { get; }
/// <summary>The custom mod page URL (if applicable).</summary>
- public string CustomUrl { get; set; }
+ public string? CustomUrl { get; }
/// <summary>The name of the mod which loads this content pack, if applicable.</summary>
- public string ContentPackFor { get; set; }
+ public string? ContentPackFor { get; }
/// <summary>The mod's compatibility with the latest stable version of the game.</summary>
- public WikiCompatibilityInfo Compatibility { get; set; }
+ public WikiCompatibilityInfo Compatibility { get; }
/// <summary>The mod's compatibility with the latest beta version of the game (if any).</summary>
- public WikiCompatibilityInfo BetaCompatibility { get; set; }
+ public WikiCompatibilityInfo? BetaCompatibility { get; }
/// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="BetaCompatibility"/> should be used for beta versions of SMAPI instead of <see cref="Compatibility"/>.</summary>
+#if NET5_0_OR_GREATER
+ [MemberNotNullWhen(true, nameof(WikiModEntry.BetaCompatibility))]
+#endif
public bool HasBetaInfo => this.BetaCompatibility != null;
/// <summary>The human-readable warnings for players about this mod.</summary>
- public string[] Warnings { get; set; }
+ public string[] Warnings { get; }
/// <summary>The URL of the pull request which submits changes for an unofficial update to the author, if any.</summary>
- public string PullRequestUrl { get; set; }
+ public string? PullRequestUrl { get; }
- /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary>
- public string DevNote { get; set; }
+ /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests.</summary>
+ public string? DevNote { get; }
/// <summary>The data overrides to apply to the mod's manifest or remote mod page data, if any.</summary>
- public WikiDataOverrideEntry Overrides { get; set; }
+ public WikiDataOverrideEntry? Overrides { get; }
/// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary>
- public string Anchor { get; set; }
+ public string? Anchor { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to oldest order.</param>
+ /// <param name="name">The mod's display name. If the mod has multiple names, the first one is the most canonical name.</param>
+ /// <param name="author">The mod's author name. If the author has multiple names, the first one is the most canonical name.</param>
+ /// <param name="nexusId">The mod ID on Nexus.</param>
+ /// <param name="chucklefishId">The mod ID in the Chucklefish mod repo.</param>
+ /// <param name="curseForgeId">The mod ID in the CurseForge mod repo.</param>
+ /// <param name="curseForgeKey">The mod ID in the CurseForge mod repo.</param>
+ /// <param name="modDropId">The mod ID in the ModDrop mod repo.</param>
+ /// <param name="githubRepo">The GitHub repository in the form 'owner/repo'.</param>
+ /// <param name="customSourceUrl">The URL to a non-GitHub source repo.</param>
+ /// <param name="customUrl">The custom mod page URL (if applicable).</param>
+ /// <param name="contentPackFor">The name of the mod which loads this content pack, if applicable.</param>
+ /// <param name="compatibility">The mod's compatibility with the latest stable version of the game.</param>
+ /// <param name="betaCompatibility">The mod's compatibility with the latest beta version of the game (if any).</param>
+ /// <param name="warnings">The human-readable warnings for players about this mod.</param>
+ /// <param name="pullRequestUrl">The URL of the pull request which submits changes for an unofficial update to the author, if any.</param>
+ /// <param name="devNote">Special notes intended for developers who maintain unofficial updates or submit pull requests.</param>
+ /// <param name="overrides">The data overrides to apply to the mod's manifest or remote mod page data, if any.</param>
+ /// <param name="anchor">The link anchor for the mod entry in the wiki compatibility list.</param>
+ public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, string? curseForgeKey, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, WikiCompatibilityInfo compatibility, WikiCompatibilityInfo? betaCompatibility, string[] warnings, string? pullRequestUrl, string? devNote, WikiDataOverrideEntry? overrides, string? anchor)
+ {
+ this.ID = id;
+ this.Name = name;
+ this.Author = author;
+ this.NexusID = nexusId;
+ this.ChucklefishID = chucklefishId;
+ this.CurseForgeID = curseForgeId;
+ this.CurseForgeKey = curseForgeKey;
+ this.ModDropID = modDropId;
+ this.GitHubRepo = githubRepo;
+ this.CustomSourceUrl = customSourceUrl;
+ this.CustomUrl = customUrl;
+ this.ContentPackFor = contentPackFor;
+ this.Compatibility = compatibility;
+ this.BetaCompatibility = betaCompatibility;
+ this.Warnings = warnings;
+ this.PullRequestUrl = pullRequestUrl;
+ this.DevNote = devNote;
+ this.Overrides = overrides;
+ this.Anchor = anchor;
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs
index 1787197a..24548078 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
/// <summary>Metadata from the wiki's mod compatibility list.</summary>
@@ -9,12 +7,27 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
** Accessors
*********/
/// <summary>The stable game version.</summary>
- public string StableVersion { get; set; }
+ public string? StableVersion { get; }
/// <summary>The beta game version (if any).</summary>
- public string BetaVersion { get; set; }
+ public string? BetaVersion { get; }
/// <summary>The mods on the wiki.</summary>
- public WikiModEntry[] Mods { get; set; }
+ public WikiModEntry[] Mods { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="stableVersion">The stable game version.</param>
+ /// <param name="betaVersion">The beta game version (if any).</param>
+ /// <param name="mods">The mods on the wiki.</param>
+ public WikiModList(string? stableVersion, string? betaVersion, WikiModEntry[] mods)
+ {
+ this.StableVersion = stableVersion;
+ this.BetaVersion = betaVersion;
+ this.Mods = mods;
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
index 4f872f1c..8e1538a5 100644
--- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
+++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Xml.Linq;
@@ -13,6 +14,7 @@ using Microsoft.Win32;
namespace StardewModdingAPI.Toolkit.Framework.GameScanning
{
/// <summary>Finds installed game folders.</summary>
+ [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are valid game install paths.")]
public class GameScanner
{
/*********
diff --git a/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs
index 3fa70615..da678ac9 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
namespace StardewModdingAPI.Toolkit.Framework.ModData
@@ -11,6 +9,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
** Accessors
********/
/// <summary>Extra metadata about mods.</summary>
- public IDictionary<string, ModDataModel> ModData { get; set; }
+ public IDictionary<string, ModDataModel> ModData { get; } = new Dictionary<string, ModDataModel>();
}
}
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs
index 46cb81e1..9674d283 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Linq;
namespace StardewModdingAPI.Toolkit.Framework.ModData
@@ -20,10 +18,10 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
public bool IsDefault { get; }
/// <summary>The lowest version in the range, or <c>null</c> for all past versions.</summary>
- public ISemanticVersion LowerVersion { get; }
+ public ISemanticVersion? LowerVersion { get; }
/// <summary>The highest version in the range, or <c>null</c> for all future versions.</summary>
- public ISemanticVersion UpperVersion { get; }
+ public ISemanticVersion? UpperVersion { get; }
/*********
@@ -35,7 +33,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <param name="isDefault">Whether this field should only be applied if it's not already set.</param>
/// <param name="lowerVersion">The lowest version in the range, or <c>null</c> for all past versions.</param>
/// <param name="upperVersion">The highest version in the range, or <c>null</c> for all future versions.</param>
- public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion)
+ public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion? lowerVersion, ISemanticVersion? upperVersion)
{
this.Key = key;
this.Value = value;
@@ -46,7 +44,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>Get whether this data field applies for the given manifest.</summary>
/// <param name="manifest">The mod manifest.</param>
- public bool IsMatch(IManifest manifest)
+ public bool IsMatch(IManifest? manifest)
{
return
manifest?.Version != null // ignore invalid manifest
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs
index 4d96a555..5912fb87 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -16,7 +14,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
** Accessors
*********/
/// <summary>The mod's current unique ID.</summary>
- public string ID { get; set; }
+ public string ID { get; }
/// <summary>The former mod IDs (if any).</summary>
/// <remarks>
@@ -25,14 +23,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// ID, if any. If the mod's ID changed over time, multiple variants can be separated by the
/// <c>|</c> character.
/// </remarks>
- public string FormerIDs { get; set; }
+ public string? FormerIDs { get; }
/// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary>
- public ModWarning SuppressWarnings { get; set; }
+ public ModWarning SuppressWarnings { get; }
/// <summary>This field stores properties that aren't mapped to another field before they're parsed into <see cref="Fields"/>.</summary>
[JsonExtensionData]
- public IDictionary<string, JToken> ExtensionData { get; set; }
+ public IDictionary<string, JToken> ExtensionData { get; } = new Dictionary<string, JToken>();
/// <summary>The versioned field data.</summary>
/// <remarks>
@@ -52,6 +50,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/*********
** Public methods
*********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The mod's current unique ID.</param>
+ /// <param name="formerIds">The former mod IDs (if any).</param>
+ /// <param name="suppressWarnings">The mod warnings to suppress, even if they'd normally be shown.</param>
+ public ModDataModel(string id, string? formerIds, ModWarning suppressWarnings)
+ {
+ this.ID = id;
+ this.FormerIDs = formerIds;
+ this.SuppressWarnings = suppressWarnings;
+ }
+
/// <summary>Get a parsed representation of the <see cref="Fields"/>.</summary>
public IEnumerable<ModDataField> GetFields()
{
@@ -61,8 +70,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
string packedKey = pair.Key;
string value = pair.Value;
bool isDefault = false;
- ISemanticVersion lowerVersion = null;
- ISemanticVersion upperVersion = null;
+ ISemanticVersion? lowerVersion = null;
+ ISemanticVersion? upperVersion = null;
// parse
string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray();
@@ -113,11 +122,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
- if (this.ExtensionData != null)
- {
- this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString());
- this.ExtensionData = null;
- }
+ this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString());
+ this.ExtensionData.Clear();
}
}
}
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs
index 4c09e1ba..ab0e4377 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -22,7 +20,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
public string[] FormerIDs { get; }
/// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary>
- public ModWarning SuppressWarnings { get; set; }
+ public ModWarning SuppressWarnings { get; }
/// <summary>The versioned field data.</summary>
public ModDataField[] Fields { get; }
@@ -72,9 +70,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
}
/// <summary>Get the default update key for this mod, if any.</summary>
- public string GetDefaultUpdateKey()
+ public string? GetDefaultUpdateKey()
{
- string updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value;
+ string? updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value;
return !string.IsNullOrWhiteSpace(updateKey)
? updateKey
: null;
@@ -82,9 +80,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>Get a parsed representation of the <see cref="ModDataRecord.Fields"/> which match a given manifest.</summary>
/// <param name="manifest">The manifest to match.</param>
- public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest)
+ public ModDataRecordVersionedFields GetVersionedFields(IManifest? manifest)
{
- ModDataRecordVersionedFields parsed = new() { DisplayName = this.DisplayName, DataRecord = this };
+ ModDataRecordVersionedFields parsed = new(this);
foreach (ModDataField field in this.Fields.Where(field => field.IsMatch(manifest)))
{
switch (field.Key)
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs
index b599b343..65fa424e 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Toolkit.Framework.ModData
{
/// <summary>The versioned fields from a <see cref="ModDataRecord"/> for a specific manifest.</summary>
@@ -9,24 +7,32 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
** Accessors
*********/
/// <summary>The underlying data record.</summary>
- public ModDataRecord DataRecord { get; set; }
-
- /// <summary>The default mod name to display when the name isn't available (e.g. during dependency checks).</summary>
- public string DisplayName { get; set; }
+ public ModDataRecord DataRecord { get; }
- /// <summary>The update key to apply.</summary>
- public string UpdateKey { get; set; }
+ /// <summary>The update key to apply (if any).</summary>
+ public string? UpdateKey { get; set; }
/// <summary>The predefined compatibility status.</summary>
public ModStatus Status { get; set; } = ModStatus.None;
/// <summary>A reason phrase for the <see cref="Status"/>, or <c>null</c> to use the default reason.</summary>
- public string StatusReasonPhrase { get; set; }
+ public string? StatusReasonPhrase { get; set; }
/// <summary>Technical details shown in TRACE logs for the <see cref="Status"/>, or <c>null</c> to omit it.</summary>
- public string StatusReasonDetails { get; set; }
+ public string? StatusReasonDetails { get; set; }
/// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary>
- public ISemanticVersion StatusUpperVersion { get; set; }
+ public ISemanticVersion? StatusUpperVersion { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="dataRecord">The underlying data record.</param>
+ public ModDataRecordVersionedFields(ModDataRecord dataRecord)
+ {
+ this.DataRecord = dataRecord;
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs
index a5237334..168b8aac 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -16,7 +14,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
private readonly ModDataRecord[] Records;
/// <summary>Get an update URL for an update key (if valid).</summary>
- private readonly Func<string, string> GetUpdateUrl;
+ private readonly Func<string, string?> GetUpdateUrl;
/*********
@@ -29,7 +27,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>Construct an instance.</summary>
/// <param name="records">The underlying mod data records indexed by default display name.</param>
/// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param>
- public ModDatabase(IEnumerable<ModDataRecord> records, Func<string, string> getUpdateUrl)
+ public ModDatabase(IEnumerable<ModDataRecord> records, Func<string, string?> getUpdateUrl)
{
this.Records = records.ToArray();
this.GetUpdateUrl = getUpdateUrl;
@@ -43,7 +41,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>Get a mod data record.</summary>
/// <param name="modID">The unique mod ID.</param>
- public ModDataRecord Get(string modID)
+ public ModDataRecord? Get(string? modID)
{
return !string.IsNullOrWhiteSpace(modID)
? this.Records.FirstOrDefault(p => p.HasID(modID))
@@ -52,11 +50,11 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>Get the mod page URL for a mod (if available).</summary>
/// <param name="id">The unique mod ID.</param>
- public string GetModPageUrlFor(string id)
+ public string? GetModPageUrlFor(string? id)
{
// get update key
- ModDataRecord record = this.Get(id);
- ModDataField updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey);
+ ModDataRecord? record = this.Get(id);
+ ModDataField? updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey);
if (updateKeyField == null)
return null;
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
index 2af30092..12333c4e 100644
--- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
+++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
@@ -306,8 +306,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <param name="entry">The file or folder.</param>
private bool IsRelevant(FileSystemInfo entry)
{
- // ignored file extension
- if (entry is FileInfo file && this.IgnoreFileExtensions.Contains(file.Extension))
+ // ignored file extensions and any files starting with "."
+ if ((entry is FileInfo file) && (this.IgnoreFileExtensions.Contains(file.Extension) || file.Name.StartsWith(".")))
return false;
// ignored entry name
diff --git a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs
index eede4562..6f5dffbe 100644
--- a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs
+++ b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs
@@ -2,4 +2,5 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StardewModdingAPI")]
[assembly: InternalsVisibleTo("SMAPI.Installer")]
+[assembly: InternalsVisibleTo("SMAPI.Tests")]
[assembly: InternalsVisibleTo("SMAPI.Web")]
diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs
index 2cb27e11..3713758f 100644
--- a/src/SMAPI.Toolkit/SemanticVersion.cs
+++ b/src/SMAPI.Toolkit/SemanticVersion.cs
@@ -119,6 +119,9 @@ namespace StardewModdingAPI.Toolkit
}
/// <inheritdoc />
+#if NET5_0_OR_GREATER
+ [MemberNotNullWhen(true, nameof(SemanticVersion.PrereleaseTag))]
+#endif
public bool IsPrerelease()
{
return !string.IsNullOrWhiteSpace(this.PrereleaseTag);
diff --git a/src/SMAPI.Toolkit/SemanticVersionComparer.cs b/src/SMAPI.Toolkit/SemanticVersionComparer.cs
index 85c974bd..2eca30df 100644
--- a/src/SMAPI.Toolkit/SemanticVersionComparer.cs
+++ b/src/SMAPI.Toolkit/SemanticVersionComparer.cs
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace StardewModdingAPI.Toolkit
{
/// <summary>A comparer for semantic versions based on the <see cref="SemanticVersion.CompareTo(ISemanticVersion)"/> field.</summary>
- public class SemanticVersionComparer : IComparer<ISemanticVersion>
+ public class SemanticVersionComparer : IComparer<ISemanticVersion?>
{
/*********
** Accessors
diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
index 01010602..da3ad608 100644
--- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
@@ -46,7 +46,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
/// <summary>Any manifest fields which didn't match a valid field.</summary>
[JsonExtensionData]
- public IDictionary<string, object> ExtraFields { get; set; } = new Dictionary<string, object>();
+ public IDictionary<string, object> ExtraFields { get; } = new Dictionary<string, object>();
/*********
diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs
index 7706b276..49356f76 100644
--- a/src/SMAPI.Web/BackgroundService.cs
+++ b/src/SMAPI.Web/BackgroundService.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
@@ -21,13 +19,17 @@ namespace StardewModdingAPI.Web
** Fields
*********/
/// <summary>The background task server.</summary>
- private static BackgroundJobServer JobServer;
+ private static BackgroundJobServer? JobServer;
/// <summary>The cache in which to store wiki metadata.</summary>
- private static IWikiCacheRepository WikiCache;
+ private static IWikiCacheRepository? WikiCache;
/// <summary>The cache in which to store mod data.</summary>
- private static IModCacheRepository ModCache;
+ private static IModCacheRepository? ModCache;
+
+ /// <summary>Whether the service has been started.</summary>
+ [MemberNotNullWhen(true, nameof(BackgroundService.JobServer), nameof(BackgroundService.WikiCache), nameof(BackgroundService.ModCache))]
+ private static bool IsStarted { get; set; }
/*********
@@ -61,6 +63,8 @@ namespace StardewModdingAPI.Web
RecurringJob.AddOrUpdate(() => BackgroundService.UpdateWikiAsync(), "*/10 * * * *"); // every 10 minutes
RecurringJob.AddOrUpdate(() => BackgroundService.RemoveStaleModsAsync(), "0 * * * *"); // hourly
+ BackgroundService.IsStarted = true;
+
return Task.CompletedTask;
}
@@ -68,6 +72,8 @@ namespace StardewModdingAPI.Web
/// <param name="cancellationToken">Tracks whether the shutdown process should no longer be graceful.</param>
public async Task StopAsync(CancellationToken cancellationToken)
{
+ BackgroundService.IsStarted = false;
+
if (BackgroundService.JobServer != null)
await BackgroundService.JobServer.WaitForShutdownAsync(cancellationToken);
}
@@ -75,6 +81,8 @@ namespace StardewModdingAPI.Web
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
+ BackgroundService.IsStarted = false;
+
BackgroundService.JobServer?.Dispose();
}
@@ -85,6 +93,9 @@ namespace StardewModdingAPI.Web
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 30, 60, 120 })]
public static async Task UpdateWikiAsync()
{
+ if (!BackgroundService.IsStarted)
+ throw new InvalidOperationException($"Must call {nameof(BackgroundService.StartAsync)} before scheduling tasks.");
+
WikiModList wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync();
BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods);
}
@@ -92,6 +103,9 @@ namespace StardewModdingAPI.Web
/// <summary>Remove mods which haven't been requested in over 48 hours.</summary>
public static Task RemoveStaleModsAsync()
{
+ if (!BackgroundService.IsStarted)
+ throw new InvalidOperationException($"Must call {nameof(BackgroundService.StartAsync)} before scheduling tasks.");
+
BackgroundService.ModCache.RemoveStaleMods(TimeSpan.FromHours(48));
return Task.CompletedTask;
}
diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs
index f7834b9c..522d77cd 100644
--- a/src/SMAPI.Web/Controllers/IndexController.cs
+++ b/src/SMAPI.Web/Controllers/IndexController.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -59,8 +57,8 @@ namespace StardewModdingAPI.Web.Controllers
{
// choose versions
ReleaseVersion[] versions = await this.GetReleaseVersionsAsync();
- ReleaseVersion stableVersion = versions.LastOrDefault(version => !version.IsForDevs);
- ReleaseVersion stableVersionForDevs = versions.LastOrDefault(version => version.IsForDevs);
+ ReleaseVersion? stableVersion = versions.LastOrDefault(version => !version.IsForDevs);
+ ReleaseVersion? stableVersionForDevs = versions.LastOrDefault(version => version.IsForDevs);
// render view
IndexVersionModel stableVersionModel = stableVersion != null
@@ -91,7 +89,7 @@ namespace StardewModdingAPI.Web.Controllers
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime);
// get latest stable release
- GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false);
+ GitRelease? release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false);
// strip 'noinclude' blocks from release description
if (release != null)
@@ -113,7 +111,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get a parsed list of SMAPI downloads for a release.</summary>
/// <param name="release">The GitHub release.</param>
- private IEnumerable<ReleaseVersion> ParseReleaseVersions(GitRelease release)
+ private IEnumerable<ReleaseVersion> ParseReleaseVersions(GitRelease? release)
{
if (release?.Assets == null)
yield break;
@@ -124,7 +122,7 @@ namespace StardewModdingAPI.Web.Controllers
continue;
Match match = Regex.Match(asset.FileName, @"SMAPI-(?<version>[\d\.]+(?:-.+)?)-installer(?<forDevs>-for-developers)?.zip");
- if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version))
+ if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion? version))
continue;
bool isForDevs = match.Groups["forDevs"].Success;
diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs
index 5791d834..e78aeeb6 100644
--- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs
+++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -66,7 +64,7 @@ namespace StardewModdingAPI.Web.Controllers
[Route("json/{schemaName}")]
[Route("json/{schemaName}/{id}")]
[Route("json/{schemaName}/{id}/{operation}")]
- public async Task<ViewResult> Index(string schemaName = null, string id = null, string operation = null)
+ public async Task<ViewResult> Index(string? schemaName = null, string? id = null, string? operation = null)
{
// parse arguments
schemaName = this.NormalizeSchemaName(schemaName);
@@ -81,7 +79,7 @@ namespace StardewModdingAPI.Web.Controllers
return this.View("Index", result);
// fetch raw JSON
- StoredFileInfo file = await this.Storage.GetAsync(id, renew);
+ StoredFileInfo file = await this.Storage.GetAsync(id!, renew);
if (string.IsNullOrWhiteSpace(file.Content))
return this.View("Index", result.SetUploadError("The JSON file seems to be empty."));
result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning);
@@ -105,7 +103,7 @@ namespace StardewModdingAPI.Web.Controllers
}
catch (JsonReaderException ex)
{
- return this.View("Index", result.AddErrors(new JsonValidatorErrorModel(ex.LineNumber, ex.Path, ex.Message, ErrorType.None)));
+ return this.View("Index", result.AddErrors(new JsonValidatorErrorModel(ex.LineNumber, ex.Path!, ex.Message, ErrorType.None)));
}
// format JSON
@@ -121,7 +119,7 @@ namespace StardewModdingAPI.Web.Controllers
// load schema
JSchema schema;
{
- FileInfo schemaFile = this.FindSchemaFile(schemaName);
+ FileInfo? schemaFile = this.FindSchemaFile(schemaName);
if (schemaFile == null)
return this.View("Index", result.SetParseError($"Invalid schema '{schemaName}'."));
schema = JSchema.Parse(System.IO.File.ReadAllText(schemaFile.FullName));
@@ -144,7 +142,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Save raw JSON data.</summary>
[HttpPost, AllowLargePosts]
[Route("json")]
- public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request)
+ public async Task<ActionResult> PostAsync(JsonValidatorRequestModel? request)
{
if (request == null)
return this.View("Index", this.GetModel(null, null, isEditView: true).SetUploadError("The request seems to be invalid."));
@@ -163,7 +161,7 @@ namespace StardewModdingAPI.Web.Controllers
return this.View("Index", this.GetModel(result.ID, schemaName, isEditView: true).SetContent(input, null).SetUploadError(result.UploadError));
// redirect to view
- return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName, id = result.ID }));
+ return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName, id = result.ID })!);
}
@@ -174,14 +172,14 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="pasteID">The stored file ID.</param>
/// <param name="schemaName">The schema name with which the JSON was validated.</param>
/// <param name="isEditView">Whether to show the edit view.</param>
- private JsonValidatorModel GetModel(string pasteID, string schemaName, bool isEditView)
+ private JsonValidatorModel GetModel(string? pasteID, string? schemaName, bool isEditView)
{
return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats, isEditView);
}
/// <summary>Get a normalized schema name, or the <see cref="DefaultSchemaID"/> if blank.</summary>
/// <param name="schemaName">The raw schema name to normalize.</param>
- private string NormalizeSchemaName(string schemaName)
+ private string NormalizeSchemaName(string? schemaName)
{
schemaName = schemaName?.Trim().ToLower();
return !string.IsNullOrWhiteSpace(schemaName)
@@ -191,7 +189,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get the schema file given its unique ID.</summary>
/// <param name="id">The schema ID.</param>
- private FileInfo FindSchemaFile(string id)
+ private FileInfo? FindSchemaFile(string? id)
{
// normalize ID
id = id?.Trim().ToLower();
@@ -216,13 +214,13 @@ namespace StardewModdingAPI.Web.Controllers
// skip through transparent errors
if (this.IsTransparentError(error))
{
- foreach (var model in error.ChildErrors.SelectMany(this.GetErrorModels))
+ foreach (JsonValidatorErrorModel model in error.ChildErrors.SelectMany(this.GetErrorModels))
yield return model;
yield break;
}
// get message
- string message = this.GetOverrideError(error);
+ string? message = this.GetOverrideError(error);
if (message == null || message == this.TransparentToken)
message = this.FlattenErrorMessage(error);
@@ -236,7 +234,7 @@ namespace StardewModdingAPI.Web.Controllers
private string FlattenErrorMessage(ValidationError error, int indent = 0)
{
// get override
- string message = this.GetOverrideError(error);
+ string? message = this.GetOverrideError(error);
if (message != null && message != this.TransparentToken)
return message;
@@ -257,7 +255,7 @@ namespace StardewModdingAPI.Web.Controllers
break;
case ErrorType.Required:
- message = $"Missing required fields: {string.Join(", ", (List<string>)error.Value)}.";
+ message = $"Missing required fields: {string.Join(", ", (List<string>)error.Value!)}.";
break;
}
@@ -274,7 +272,7 @@ namespace StardewModdingAPI.Web.Controllers
if (!error.ChildErrors.Any())
return false;
- string @override = this.GetOverrideError(error);
+ string? @override = this.GetOverrideError(error);
return
@override == this.TransparentToken
|| (error.ErrorType == ErrorType.Then && @override == null);
@@ -282,18 +280,18 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get an override error from the JSON schema, if any.</summary>
/// <param name="error">The schema validation error.</param>
- private string GetOverrideError(ValidationError error)
+ private string? GetOverrideError(ValidationError error)
{
- string GetRawOverrideError()
+ string? GetRawOverrideError()
{
// get override errors
- IDictionary<string, string> errors = this.GetExtensionField<Dictionary<string, string>>(error.Schema, "@errorMessages");
+ IDictionary<string, string?>? errors = this.GetExtensionField<Dictionary<string, string?>>(error.Schema, "@errorMessages");
if (errors == null)
return null;
- errors = new Dictionary<string, string>(errors, StringComparer.OrdinalIgnoreCase);
+ errors = new Dictionary<string, string?>(errors, StringComparer.OrdinalIgnoreCase);
// match error by type and message
- foreach ((string target, string errorMessage) in errors)
+ foreach ((string target, string? errorMessage) in errors)
{
if (!target.Contains(":"))
continue;
@@ -304,7 +302,7 @@ namespace StardewModdingAPI.Web.Controllers
}
// match by type
- return errors.TryGetValue(error.ErrorType.ToString(), out string message)
+ return errors.TryGetValue(error.ErrorType.ToString(), out string? message)
? message?.Trim()
: null;
}
@@ -317,7 +315,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <typeparam name="T">The field type.</typeparam>
/// <param name="schema">The schema whose extension fields to search.</param>
/// <param name="key">The case-insensitive field key.</param>
- private T GetExtensionField<T>(JSchema schema, string key)
+ private T? GetExtensionField<T>(JSchema schema, string key)
{
foreach ((string curKey, JToken value) in schema.ExtensionData)
{
@@ -330,7 +328,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Format a schema value for display.</summary>
/// <param name="value">The value to format.</param>
- private string FormatValue(object value)
+ private string FormatValue(object? value)
{
return value switch
{
diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs
index 93f2613e..33af5a81 100644
--- a/src/SMAPI.Web/Controllers/LogParserController.cs
+++ b/src/SMAPI.Web/Controllers/LogParserController.cs
@@ -69,7 +69,7 @@ namespace StardewModdingAPI.Web.Controllers
case LogViewFormat.RawDownload:
{
- string content = file.Error ?? file.Content;
+ string content = file.Error ?? file.Content ?? string.Empty;
return this.File(Encoding.UTF8.GetBytes(content), "plain/text", $"SMAPI log ({id}).txt");
}
@@ -97,7 +97,7 @@ namespace StardewModdingAPI.Web.Controllers
return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError));
// redirect to view
- return this.Redirect(this.Url.PlainAction("Index", "LogParser", new { id = uploadResult.ID }));
+ return this.Redirect(this.Url.PlainAction("Index", "LogParser", new { id = uploadResult.ID })!);
}
@@ -109,7 +109,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="expiry">When the uploaded file will no longer be available.</param>
/// <param name="uploadWarning">A non-blocking warning while uploading the log.</param>
/// <param name="uploadError">An error which occurred while uploading the log.</param>
- private LogParserModel GetModel(string? pasteID, DateTime? expiry = null, string? uploadWarning = null, string? uploadError = null)
+ private LogParserModel GetModel(string? pasteID, DateTimeOffset? expiry = null, string? uploadWarning = null, string? uploadError = null)
{
Platform? platform = this.DetectClientPlatform();
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index 3dc1e366..401bba4f 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -78,7 +77,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="model">The mod search criteria.</param>
/// <param name="version">The requested API version.</param>
[HttpPost]
- public async Task<IEnumerable<ModEntryModel>> PostAsync([FromBody] ModSearchModel model, [FromRoute] string version)
+ public async Task<IEnumerable<ModEntryModel>> PostAsync([FromBody] ModSearchModel? model, [FromRoute] string version)
{
if (model?.Mods == null)
return Array.Empty<ModEntryModel>();
@@ -94,16 +93,16 @@ namespace StardewModdingAPI.Web.Controllers
continue;
// special case: if this is an update check for the official SMAPI repo, check the Nexus mod page for beta versions
- if (mod.ID == config.SmapiInfo.ID && mod.UpdateKeys?.Any(key => key == config.SmapiInfo.DefaultUpdateKey) == true && mod.InstalledVersion?.IsPrerelease() == true)
- mod.UpdateKeys = mod.UpdateKeys.Concat(config.SmapiInfo.AddBetaUpdateKeys).ToArray();
+ if (mod.ID == config.SmapiInfo.ID && mod.UpdateKeys.Any(key => key == config.SmapiInfo.DefaultUpdateKey) && mod.InstalledVersion?.IsPrerelease() == true)
+ mod.AddUpdateKeys(config.SmapiInfo.AddBetaUpdateKeys);
// fetch result
ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata, model.ApiVersion);
if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null))
{
- var errors = new List<string>(result.Errors);
- errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage.");
- result.Errors = errors.ToArray();
+ result.Errors = result.Errors
+ .Concat(new[] { $"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage." })
+ .ToArray();
}
mods[mod.ID] = result;
@@ -123,26 +122,26 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
/// <param name="apiVersion">The SMAPI version installed by the player.</param>
/// <returns>Returns the mod data if found, else <c>null</c>.</returns>
- private async Task<ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion apiVersion)
+ private async Task<ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion? apiVersion)
{
// cross-reference data
- ModDataRecord record = this.ModDatabase.Get(search.ID);
- WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.OrdinalIgnoreCase));
+ ModDataRecord? record = this.ModDatabase.Get(search.ID);
+ WikiModEntry? wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.OrdinalIgnoreCase));
UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray();
- ModOverrideConfig overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID?.Trim(), StringComparison.OrdinalIgnoreCase));
+ ModOverrideConfig? overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID.Trim(), StringComparison.OrdinalIgnoreCase));
bool allowNonStandardVersions = overrides?.AllowNonStandardVersions ?? false;
// SMAPI versions with a '-beta' tag indicate major changes that may need beta mod versions.
// This doesn't apply to normal prerelease versions which have an '-alpha' tag.
- bool isSmapiBeta = apiVersion.IsPrerelease() && apiVersion.PrereleaseTag.StartsWith("beta");
+ bool isSmapiBeta = apiVersion != null && apiVersion.IsPrerelease() && apiVersion.PrereleaseTag.StartsWith("beta");
// get latest versions
- ModEntryModel result = new() { ID = search.ID };
+ ModEntryModel result = new(search.ID);
IList<string> errors = new List<string>();
- ModEntryVersionModel main = null;
- ModEntryVersionModel optional = null;
- ModEntryVersionModel unofficial = null;
- ModEntryVersionModel unofficialForBeta = null;
+ ModEntryVersionModel? main = null;
+ ModEntryVersionModel? optional = null;
+ ModEntryVersionModel? unofficial = null;
+ ModEntryVersionModel? unofficialForBeta = null;
foreach (UpdateKey updateKey in updateKeys)
{
// validate update key
@@ -162,9 +161,9 @@ namespace StardewModdingAPI.Web.Controllers
// handle versions
if (this.IsNewer(data.Version, main?.Version))
- main = new ModEntryVersionModel(data.Version, data.Url);
+ main = new ModEntryVersionModel(data.Version, data.Url!);
if (this.IsNewer(data.PreviewVersion, optional?.Version))
- optional = new ModEntryVersionModel(data.PreviewVersion, data.Url);
+ optional = new ModEntryVersionModel(data.PreviewVersion, data.Url!);
}
// get unofficial version
@@ -172,7 +171,7 @@ namespace StardewModdingAPI.Web.Controllers
unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}");
// get unofficial version for beta
- if (wikiEntry?.HasBetaInfo == true)
+ if (wikiEntry is { HasBetaInfo: true })
{
if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial)
{
@@ -198,13 +197,13 @@ namespace StardewModdingAPI.Web.Controllers
if (overrides?.SetUrl != null)
{
if (main != null)
- main.Url = overrides.SetUrl;
+ main = new(main.Version, overrides.SetUrl);
if (optional != null)
- optional.Url = overrides.SetUrl;
+ optional = new(optional.Version, overrides.SetUrl);
}
// get recommended update (if any)
- ISemanticVersion installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.Overrides?.ChangeLocalVersions, allowNonStandard: allowNonStandardVersions);
+ ISemanticVersion? installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.Overrides?.ChangeLocalVersions, allowNonStandard: allowNonStandardVersions);
if (apiVersion != null && installedVersion != null)
{
// get newer versions
@@ -219,7 +218,7 @@ namespace StardewModdingAPI.Web.Controllers
updates.Add(unofficialForBeta);
// get newest version
- ModEntryVersionModel newest = null;
+ ModEntryVersionModel? newest = null;
foreach (ModEntryVersionModel update in updates)
{
if (newest == null || update.Version.IsNewerThan(newest.Version))
@@ -245,7 +244,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="currentVersion">The current semantic version.</param>
/// <param name="newVersion">The target semantic version.</param>
/// <param name="useBetaChannel">Whether the user enabled the beta channel and should be offered prerelease updates.</param>
- private bool IsRecommendedUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel)
+ private bool IsRecommendedUpdate(ISemanticVersion currentVersion, [NotNullWhen(true)] ISemanticVersion? newVersion, bool useBetaChannel)
{
return
newVersion != null
@@ -256,7 +255,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get whether a <paramref name="current"/> version is newer than an <paramref name="other"/> version.</summary>
/// <param name="current">The current version.</param>
/// <param name="other">The other version.</param>
- private bool IsNewer(ISemanticVersion current, ISemanticVersion other)
+ private bool IsNewer([NotNullWhen(true)] ISemanticVersion? current, ISemanticVersion? other)
{
return current != null && (other == null || other.IsOlderThan(current));
}
@@ -265,17 +264,20 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="updateKey">The namespaced update key.</param>
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
/// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param>
- private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor mapRemoteVersions)
+ private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions)
{
+ if (!updateKey.LooksValid)
+ return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"Invalid update key '{updateKey}'.");
+
// get mod page
IModPage page;
{
bool isCached =
- this.ModCache.TryGetMod(updateKey.Site, updateKey.ID, out Cached<IModPage> cachedMod)
+ this.ModCache.TryGetMod(updateKey.Site, updateKey.ID, out Cached<IModPage>? cachedMod)
&& !this.ModCache.IsStale(cachedMod.LastUpdated, cachedMod.Data.Status == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes);
if (isCached)
- page = cachedMod.Data;
+ page = cachedMod!.Data;
else
{
page = await this.ModSites.GetModPageAsync(updateKey);
@@ -291,7 +293,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="specifiedKeys">The specified update keys.</param>
/// <param name="record">The mod's entry in SMAPI's internal database.</param>
/// <param name="entry">The mod's entry in the wiki list.</param>
- private IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
+ private IEnumerable<UpdateKey> GetUpdateKeys(string[]? specifiedKeys, ModDataRecord? record, WikiModEntry? entry)
{
// get unique update keys
List<UpdateKey> updateKeys = this.GetUnfilteredUpdateKeys(specifiedKeys, record, entry)
@@ -310,7 +312,7 @@ namespace StardewModdingAPI.Web.Controllers
// if the list has both an update key (like "Nexus:2400") and subkey (like "Nexus:2400@subkey") for the same page, the subkey takes priority
{
var removeKeys = new HashSet<UpdateKey>();
- foreach (var key in updateKeys)
+ foreach (UpdateKey key in updateKeys)
{
if (key.Subkey != null)
removeKeys.Add(new UpdateKey(key.Site, key.ID, null));
@@ -326,7 +328,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="specifiedKeys">The specified update keys.</param>
/// <param name="record">The mod's entry in SMAPI's internal database.</param>
/// <param name="entry">The mod's entry in the wiki list.</param>
- private IEnumerable<string> GetUnfilteredUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
+ private IEnumerable<string> GetUnfilteredUpdateKeys(string[]? specifiedKeys, ModDataRecord? record, WikiModEntry? entry)
{
// specified update keys
foreach (string key in specifiedKeys ?? Array.Empty<string>())
@@ -337,7 +339,7 @@ namespace StardewModdingAPI.Web.Controllers
// default update key
{
- string defaultKey = record?.GetDefaultUpdateKey();
+ string? defaultKey = record?.GetDefaultUpdateKey();
if (!string.IsNullOrWhiteSpace(defaultKey))
yield return defaultKey;
}
diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs
index 5292e1ce..919afa5b 100644
--- a/src/SMAPI.Web/Controllers/ModsController.cs
+++ b/src/SMAPI.Web/Controllers/ModsController.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
@@ -54,8 +53,8 @@ namespace StardewModdingAPI.Web.Controllers
public ModListModel FetchData()
{
// fetch cached data
- if (!this.Cache.TryGetWikiMetadata(out Cached<WikiMetadata> metadata))
- return new ModListModel();
+ if (!this.Cache.TryGetWikiMetadata(out Cached<WikiMetadata>? metadata))
+ return new ModListModel(null, null, Array.Empty<ModModel>(), lastUpdated: DateTimeOffset.UtcNow, isStale: true);
// build model
return new ModListModel(
diff --git a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs
index 108ceff7..bd414ea2 100644
--- a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs
+++ b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Filters;
@@ -42,7 +40,7 @@ namespace StardewModdingAPI.Web.Framework
public void OnAuthorization(AuthorizationFilterContext context)
{
IFeatureCollection features = context.HttpContext.Features;
- IFormFeature formFeature = features.Get<IFormFeature>();
+ IFormFeature? formFeature = features.Get<IFormFeature>();
if (formFeature?.Form == null)
{
diff --git a/src/SMAPI.Web/Framework/Caching/Cached.cs b/src/SMAPI.Web/Framework/Caching/Cached.cs
index aabbf146..b393e1e1 100644
--- a/src/SMAPI.Web/Framework/Caching/Cached.cs
+++ b/src/SMAPI.Web/Framework/Caching/Cached.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Web.Framework.Caching
@@ -12,21 +10,18 @@ namespace StardewModdingAPI.Web.Framework.Caching
** Accessors
*********/
/// <summary>The cached data.</summary>
- public T Data { get; set; }
+ public T Data { get; }
/// <summary>When the data was last updated.</summary>
- public DateTimeOffset LastUpdated { get; set; }
+ public DateTimeOffset LastUpdated { get; }
/// <summary>When the data was last requested through the mod API.</summary>
- public DateTimeOffset LastRequested { get; set; }
+ public DateTimeOffset LastRequested { get; internal set; }
/*********
** Public methods
*********/
- /// <summary>Construct an empty instance.</summary>
- public Cached() { }
-
/// <summary>Construct an instance.</summary>
/// <param name="data">The cached data.</param>
public Cached(T data)
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
index 2020d747..fb74e9da 100644
--- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System;
+using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
namespace StardewModdingAPI.Web.Framework.Caching.Mods
@@ -16,7 +15,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
/// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
/// <param name="mod">The fetched mod.</param>
/// <param name="markRequested">Whether to update the mod's 'last requested' date.</param>
- bool TryGetMod(ModSiteKey site, string id, out Cached<IModPage> mod, bool markRequested = true);
+ bool TryGetMod(ModSiteKey site, string id, [NotNullWhen(true)] out Cached<IModPage>? mod, bool markRequested = true);
/// <summary>Save data fetched for a mod.</summary>
/// <param name="site">The mod site on which the mod is found.</param>
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs
index 338562d8..4ba0bd20 100644
--- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
@@ -25,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
/// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
/// <param name="mod">The fetched mod.</param>
/// <param name="markRequested">Whether to update the mod's 'last requested' date.</param>
- public bool TryGetMod(ModSiteKey site, string id, out Cached<IModPage> mod, bool markRequested = true)
+ public bool TryGetMod(ModSiteKey site, string id, [NotNullWhen(true)] out Cached<IModPage>? mod, bool markRequested = true)
{
// get mod
if (!this.Mods.TryGetValue(this.GetKey(site, id), out var cachedMod))
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
index 6edafddc..b8a0df34 100644
--- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
@@ -14,16 +13,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
*********/
/// <summary>Get the cached wiki metadata.</summary>
/// <param name="metadata">The fetched metadata.</param>
- bool TryGetWikiMetadata(out Cached<WikiMetadata> metadata);
+ bool TryGetWikiMetadata([NotNullWhen(true)] out Cached<WikiMetadata>? metadata);
/// <summary>Get the cached wiki mods.</summary>
/// <param name="filter">A filter to apply, if any.</param>
- IEnumerable<Cached<WikiModEntry>> GetWikiMods(Func<WikiModEntry, bool> filter = null);
+ IEnumerable<Cached<WikiModEntry>> GetWikiMods(Func<WikiModEntry, bool>? filter = null);
/// <summary>Save data fetched from the wiki compatibility list.</summary>
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
/// <param name="mods">The mod data.</param>
- void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods);
+ void SaveWikiData(string? stableVersion, string? betaVersion, IEnumerable<WikiModEntry> mods);
}
}
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs
index d1ccb9c7..8b4338e2 100644
--- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
@@ -14,7 +13,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
** Fields
*********/
/// <summary>The saved wiki metadata.</summary>
- private Cached<WikiMetadata> Metadata;
+ private Cached<WikiMetadata>? Metadata;
/// <summary>The cached wiki data.</summary>
private Cached<WikiModEntry>[] Mods = Array.Empty<Cached<WikiModEntry>>();
@@ -25,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
*********/
/// <summary>Get the cached wiki metadata.</summary>
/// <param name="metadata">The fetched metadata.</param>
- public bool TryGetWikiMetadata(out Cached<WikiMetadata> metadata)
+ public bool TryGetWikiMetadata([NotNullWhen(true)] out Cached<WikiMetadata>? metadata)
{
metadata = this.Metadata;
return metadata != null;
@@ -33,7 +32,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
/// <summary>Get the cached wiki mods.</summary>
/// <param name="filter">A filter to apply, if any.</param>
- public IEnumerable<Cached<WikiModEntry>> GetWikiMods(Func<WikiModEntry, bool> filter = null)
+ public IEnumerable<Cached<WikiModEntry>> GetWikiMods(Func<WikiModEntry, bool>? filter = null)
{
foreach (var mod in this.Mods)
{
@@ -46,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
/// <param name="mods">The mod data.</param>
- public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods)
+ public void SaveWikiData(string? stableVersion, string? betaVersion, IEnumerable<WikiModEntry> mods)
{
this.Metadata = new Cached<WikiMetadata>(new WikiMetadata(stableVersion, betaVersion));
this.Mods = mods.Select(mod => new Cached<WikiModEntry>(mod)).ToArray();
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs
index 6ae42488..f53ea201 100644
--- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
{
/// <summary>The model for cached wiki metadata.</summary>
@@ -9,22 +7,19 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
** Accessors
*********/
/// <summary>The current stable Stardew Valley version.</summary>
- public string StableVersion { get; set; }
+ public string? StableVersion { get; }
/// <summary>The current beta Stardew Valley version.</summary>
- public string BetaVersion { get; set; }
+ public string? BetaVersion { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public WikiMetadata() { }
-
- /// <summary>Construct an instance.</summary>
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
- public WikiMetadata(string stableVersion, string betaVersion)
+ public WikiMetadata(string? stableVersion, string? betaVersion)
{
this.StableVersion = stableVersion;
this.BetaVersion = betaVersion;
diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs
index 4d041c1b..ce0f1122 100644
--- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Net;
using System.Threading.Tasks;
@@ -44,7 +42,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- public async Task<IModPage> GetModData(string id)
+ public async Task<IModPage?> GetModData(string id)
{
IModPage page = new GenericModPage(this.SiteKey, id);
@@ -53,7 +51,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
// fetch HTML
- string html;
+ string? html;
try
{
html = await this.Client
@@ -69,7 +67,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
// extract mod info
string url = this.GetModUrl(parsedId);
- string version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText;
+ string? version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText;
string name = doc.DocumentNode.SelectSingleNode("//h1").ChildNodes[0].InnerText.Trim();
if (name.StartsWith("[SMAPI]"))
name = name.Substring("[SMAPI]".Length).TrimStart();
@@ -81,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.Client?.Dispose();
+ this.Client.Dispose();
}
@@ -92,7 +90,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
/// <param name="id">The mod ID.</param>
private string GetModUrl(uint id)
{
- UriBuilder builder = new(this.Client.BaseClient.BaseAddress);
+ UriBuilder builder = new(this.Client.BaseClient.BaseAddress!);
builder.Path += string.Format(this.ModPageUrlFormat, id);
return builder.Uri.ToString();
}
diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
index 5ef369d5..d351b42d 100644
--- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@@ -42,7 +40,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- public async Task<IModPage> GetModData(string id)
+ public async Task<IModPage?> GetModData(string id)
{
IModPage page = new GenericModPage(this.SiteKey, id);
@@ -51,9 +49,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID.");
// get raw data
- ModModel mod = await this.Client
+ ModModel? mod = await this.Client
.GetAsync($"addon/{parsedId}")
- .As<ModModel>();
+ .As<ModModel?>();
if (mod == null)
return page.SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID.");
@@ -73,7 +71,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.Client?.Dispose();
+ this.Client.Dispose();
}
@@ -82,9 +80,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
*********/
/// <summary>Get a raw version string for a mod file, if available.</summary>
/// <param name="file">The file whose version to get.</param>
- private string GetRawVersion(ModFileModel file)
+ private string? GetRawVersion(ModFileModel file)
{
- Match match = this.VersionInNamePattern.Match(file.DisplayName);
+ Match match = this.VersionInNamePattern.Match(file.DisplayName ?? "");
if (!match.Success)
match = this.VersionInNamePattern.Match(file.FileName);
diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs
index eabef9f0..e9adcf20 100644
--- a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs
@@ -1,14 +1,28 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels
{
/// <summary>Metadata from the CurseForge API about a mod file.</summary>
public class ModFileModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The file name as downloaded.</summary>
- public string FileName { get; set; }
+ public string FileName { get; }
/// <summary>The file display name.</summary>
- public string DisplayName { get; set; }
+ public string? DisplayName { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fileName">The file name as downloaded.</param>
+ /// <param name="displayName">The file display name.</param>
+ public ModFileModel(string fileName, string? displayName)
+ {
+ this.FileName = fileName;
+ this.DisplayName = displayName;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs
index a95df7f1..fd7796f2 100644
--- a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs
@@ -1,20 +1,38 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels
{
/// <summary>An mod from the CurseForge API.</summary>
public class ModModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The mod's unique ID on CurseForge.</summary>
- public int ID { get; set; }
+ public int ID { get; }
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The web URL for the mod page.</summary>
- public string WebsiteUrl { get; set; }
+ public string WebsiteUrl { get; }
/// <summary>The available file downloads.</summary>
- public ModFileModel[] LatestFiles { get; set; }
+ public ModFileModel[] LatestFiles { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The mod's unique ID on CurseForge.</param>
+ /// <param name="name">The mod name.</param>
+ /// <param name="websiteUrl">The web URL for the mod page.</param>
+ /// <param name="latestFiles">The available file downloads.</param>
+ public ModModel(int id, string name, string websiteUrl, ModFileModel[] latestFiles)
+ {
+ this.ID = id;
+ this.Name = name;
+ this.WebsiteUrl = websiteUrl;
+ this.LatestFiles = latestFiles;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
index 919072b0..548f17c3 100644
--- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
+++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.Framework.Clients
{
/// <summary>Generic metadata about a file download on a mod page.</summary>
@@ -9,26 +7,23 @@ namespace StardewModdingAPI.Web.Framework.Clients
** Accessors
*********/
/// <summary>The download's display name.</summary>
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The download's description.</summary>
- public string Description { get; set; }
+ public string? Description { get; }
/// <summary>The download's file version.</summary>
- public string Version { get; set; }
+ public string? Version { get; }
/*********
** Public methods
*********/
- /// <summary>Construct an empty instance.</summary>
- public GenericModDownload() { }
-
/// <summary>Construct an instance.</summary>
/// <param name="name">The download's display name.</param>
/// <param name="description">The download's description.</param>
/// <param name="version">The download's file version.</param>
- public GenericModDownload(string name, string description, string version)
+ public GenericModDownload(string name, string? description, string? version)
{
this.Name = name;
this.Description = description;
diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
index 4788aa2a..5353c7e1 100644
--- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
+++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
@@ -20,30 +19,31 @@ namespace StardewModdingAPI.Web.Framework.Clients
public string Id { get; set; }
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>The mod's semantic version number.</summary>
- public string Version { get; set; }
+ public string? Version { get; set; }
/// <summary>The mod's web URL.</summary>
- public string Url { get; set; }
+ public string? Url { get; set; }
/// <summary>The mod downloads.</summary>
public IModDownload[] Downloads { get; set; } = Array.Empty<IModDownload>();
/// <summary>The mod availability status on the remote site.</summary>
- public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok;
+ public RemoteModStatus Status { get; set; } = RemoteModStatus.InvalidData;
/// <summary>A user-friendly error which indicates why fetching the mod info failed (if applicable).</summary>
- public string Error { get; set; }
+ public string? Error { get; set; }
+
+ /// <summary>Whether the mod data is valid.</summary>
+ [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))]
+ public bool IsValid => this.Status == RemoteModStatus.Ok;
/*********
** Public methods
*********/
- /// <summary>Construct an empty instance.</summary>
- public GenericModPage() { }
-
/// <summary>Construct an instance.</summary>
/// <param name="site">The mod site containing the mod.</param>
/// <param name="id">The mod's unique ID within the site.</param>
@@ -58,12 +58,13 @@ namespace StardewModdingAPI.Web.Framework.Clients
/// <param name="version">The mod's semantic version number.</param>
/// <param name="url">The mod's web URL.</param>
/// <param name="downloads">The mod downloads.</param>
- public IModPage SetInfo(string name, string version, string url, IEnumerable<IModDownload> downloads)
+ public IModPage SetInfo(string name, string? version, string url, IEnumerable<IModDownload> downloads)
{
this.Name = name;
this.Version = version;
this.Url = url;
this.Downloads = downloads.ToArray();
+ this.Status = RemoteModStatus.Ok;
return this;
}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs
index 39ebf94e..dbce9368 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
@@ -7,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>A GitHub download attached to a release.</summary>
internal class GitAsset
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The file name.</summary>
[JsonProperty("name")]
- public string FileName { get; set; }
+ public string FileName { get; }
/// <summary>The file content type.</summary>
[JsonProperty("content_type")]
- public string ContentType { get; set; }
+ public string ContentType { get; }
/// <summary>The download URL.</summary>
[JsonProperty("browser_download_url")]
- public string DownloadUrl { get; set; }
+ public string DownloadUrl { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fileName">The file name.</param>
+ /// <param name="contentType">The file content type.</param>
+ /// <param name="downloadUrl">The download URL.</param>
+ public GitAsset(string fileName, string contentType, string downloadUrl)
+ {
+ this.FileName = fileName;
+ this.ContentType = contentType;
+ this.DownloadUrl = downloadUrl;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs
index 0e68e2c2..785979a5 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Linq;
using System.Net;
@@ -35,26 +33,26 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
/// <param name="username">The username with which to authenticate to the GitHub API.</param>
/// <param name="password">The password with which to authenticate to the GitHub API.</param>
- public GitHubClient(string baseUrl, string userAgent, string acceptHeader, string username, string password)
+ public GitHubClient(string baseUrl, string userAgent, string acceptHeader, string? username, string? password)
{
this.Client = new FluentClient(baseUrl)
.SetUserAgent(userAgent)
.AddDefault(req => req.WithHeader("Accept", acceptHeader));
if (!string.IsNullOrWhiteSpace(username))
- this.Client = this.Client.SetBasicAuthentication(username, password);
+ this.Client = this.Client.SetBasicAuthentication(username, password!);
}
/// <summary>Get basic metadata for a GitHub repository, if available.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <returns>Returns the repository info if it exists, else <c>null</c>.</returns>
- public async Task<GitRepo> GetRepositoryAsync(string repo)
+ public async Task<GitRepo?> GetRepositoryAsync(string repo)
{
this.AssertKeyFormat(repo);
try
{
return await this.Client
.GetAsync($"repos/{repo}")
- .As<GitRepo>();
+ .As<GitRepo?>();
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
@@ -66,7 +64,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param>
/// <returns>Returns the release if found, else <c>null</c>.</returns>
- public async Task<GitRelease> GetLatestReleaseAsync(string repo, bool includePrerelease = false)
+ public async Task<GitRelease?> GetLatestReleaseAsync(string repo, bool includePrerelease = false)
{
this.AssertKeyFormat(repo);
try
@@ -81,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
return await this.Client
.GetAsync($"repos/{repo}/releases/latest")
- .As<GitRelease>();
+ .As<GitRelease?>();
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
@@ -91,7 +89,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- public async Task<IModPage> GetModData(string id)
+ public async Task<IModPage?> GetModData(string id)
{
IModPage page = new GenericModPage(this.SiteKey, id);
@@ -99,15 +97,15 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/SMAPI'.");
// fetch repo info
- GitRepo repository = await this.GetRepositoryAsync(id);
+ GitRepo? repository = await this.GetRepositoryAsync(id);
if (repository == null)
return page.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID.");
string name = repository.FullName;
string url = $"{repository.WebUrl}/releases";
// get releases
- GitRelease latest;
- GitRelease preview;
+ GitRelease? latest;
+ GitRelease? preview;
{
// get latest release (whether preview or stable)
latest = await this.GetLatestReleaseAsync(id, includePrerelease: true);
@@ -118,7 +116,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
preview = null;
if (latest.IsPrerelease)
{
- GitRelease release = await this.GetLatestReleaseAsync(id, includePrerelease: false);
+ GitRelease? release = await this.GetLatestReleaseAsync(id, includePrerelease: false);
if (release != null)
{
preview = latest;
@@ -129,8 +127,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
// get downloads
IModDownload[] downloads = new[] { latest, preview }
- .Where(release => release != null)
- .Select(release => (IModDownload)new GenericModDownload(release.Name, release.Body, release.Tag))
+ .Where(release => release is not null)
+ .Select(release => (IModDownload)new GenericModDownload(release!.Name, release.Body, release.Tag))
.ToArray();
// return info
@@ -140,7 +138,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.Client?.Dispose();
+ this.Client.Dispose();
}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs
index 275c775a..24d6c3c5 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
@@ -7,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>The license info for a GitHub project.</summary>
internal class GitLicense
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The license display name.</summary>
[JsonProperty("name")]
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The SPDX ID for the license.</summary>
[JsonProperty("spdx_id")]
- public string SpdxId { get; set; }
+ public string SpdxId { get; }
/// <summary>The URL for the license info.</summary>
[JsonProperty("url")]
- public string Url { get; set; }
+ public string Url { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The license display name.</param>
+ /// <param name="spdxId">The SPDX ID for the license.</param>
+ /// <param name="url">The URL for the license info.</param>
+ public GitLicense(string name, string spdxId, string url)
+ {
+ this.Name = name;
+ this.SpdxId = spdxId;
+ this.Url = url;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs
index 383775d2..9de6f020 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
@@ -12,24 +11,45 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
*********/
/// <summary>The display name.</summary>
[JsonProperty("name")]
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The semantic version string.</summary>
[JsonProperty("tag_name")]
- public string Tag { get; set; }
+ public string Tag { get; }
/// <summary>The Markdown description for the release.</summary>
- public string Body { get; set; }
+ public string Body { get; internal set; }
/// <summary>Whether this is a draft version.</summary>
[JsonProperty("draft")]
- public bool IsDraft { get; set; }
+ public bool IsDraft { get; }
/// <summary>Whether this is a prerelease version.</summary>
[JsonProperty("prerelease")]
- public bool IsPrerelease { get; set; }
+ public bool IsPrerelease { get; }
/// <summary>The attached files.</summary>
- public GitAsset[] Assets { get; set; }
+ public GitAsset[] Assets { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The display name.</param>
+ /// <param name="tag">The semantic version string.</param>
+ /// <param name="body">The Markdown description for the release.</param>
+ /// <param name="isDraft">Whether this is a draft version.</param>
+ /// <param name="isPrerelease">Whether this is a prerelease version.</param>
+ /// <param name="assets">The attached files.</param>
+ public GitRelease(string name, string tag, string? body, bool isDraft, bool isPrerelease, GitAsset[]? assets)
+ {
+ this.Name = name;
+ this.Tag = tag;
+ this.Body = body ?? string.Empty;
+ this.IsDraft = isDraft;
+ this.IsPrerelease = isPrerelease;
+ this.Assets = assets ?? Array.Empty<GitAsset>();
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs
index 5b5ce6a6..879b5e49 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
@@ -7,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>Basic metadata about a GitHub project.</summary>
internal class GitRepo
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The full repository name, including the owner.</summary>
[JsonProperty("full_name")]
- public string FullName { get; set; }
+ public string FullName { get; }
/// <summary>The URL to the repository web page, if any.</summary>
[JsonProperty("html_url")]
- public string WebUrl { get; set; }
+ public string? WebUrl { get; }
/// <summary>The code license, if any.</summary>
[JsonProperty("license")]
- public GitLicense License { get; set; }
+ public GitLicense? License { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fullName">The full repository name, including the owner.</param>
+ /// <param name="webUrl">The URL to the repository web page, if any.</param>
+ /// <param name="license">The code license, if any.</param>
+ public GitRepo(string fullName, string? webUrl, GitLicense? license)
+ {
+ this.FullName = fullName;
+ this.WebUrl = webUrl;
+ this.License = license;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs
index e1961416..886e32d3 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Threading.Tasks;
@@ -14,12 +12,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>Get basic metadata for a GitHub repository, if available.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <returns>Returns the repository info if it exists, else <c>null</c>.</returns>
- Task<GitRepo> GetRepositoryAsync(string repo);
+ Task<GitRepo?> GetRepositoryAsync(string repo);
/// <summary>Get the latest release for a GitHub repository.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param>
/// <returns>Returns the release if found, else <c>null</c>.</returns>
- Task<GitRelease> GetLatestReleaseAsync(string repo, bool includePrerelease = false);
+ Task<GitRelease?> GetLatestReleaseAsync(string repo, bool includePrerelease = false);
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs
index 2cd1f635..3697ffae 100644
--- a/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Threading.Tasks;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
@@ -20,6 +18,6 @@ namespace StardewModdingAPI.Web.Framework.Clients
*********/
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- Task<IModPage> GetModData(string id);
+ Task<IModPage?> GetModData(string id);
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs
index 1a11a606..c60b2c90 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Pathoschild.Http.Client;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
@@ -43,9 +42,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- public async Task<IModPage> GetModData(string id)
+ [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The nullability is validated in this method.")]
+ public async Task<IModPage?> GetModData(string id)
{
- var page = new GenericModPage(this.SiteKey, id);
+ IModPage page = new GenericModPage(this.SiteKey, id);
if (!long.TryParse(id, out long parsedId))
return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID.");
@@ -60,9 +60,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
Mods = true
})
.As<ModListModel>();
- ModModel mod = response.Mods[parsedId];
- if (mod.Mod?.Title == null || mod.Mod.ErrorCode.HasValue)
- return null;
+
+ if (!response.Mods.TryGetValue(parsedId, out ModModel? mod) || mod?.Mod is null)
+ return page.SetError(RemoteModStatus.DoesNotExist, "Found no ModDrop page with this ID.");
+ if (mod.Mod.ErrorCode is not null)
+ return page.SetError(RemoteModStatus.InvalidData, $"ModDrop returned error code {mod.Mod.ErrorCode} for mod ID '{id}'.");
// get files
var downloads = new List<IModDownload>();
@@ -77,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
}
// return info
- string name = mod.Mod?.Title;
+ string name = mod.Mod.Title;
string url = string.Format(this.ModUrlFormat, id);
return page.SetInfo(name: name, version: null, url: url, downloads: downloads);
}
@@ -85,7 +87,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.Client?.Dispose();
+ this.Client.Dispose();
}
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs
index dd6a95e0..31905338 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
@@ -7,27 +5,53 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
/// <summary>Metadata from the ModDrop API about a mod file.</summary>
public class FileDataModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The file title.</summary>
[JsonProperty("title")]
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The file description.</summary>
[JsonProperty("desc")]
- public string Description { get; set; }
+ public string Description { get; }
/// <summary>The file version.</summary>
- public string Version { get; set; }
+ public string Version { get; }
/// <summary>Whether the file is deleted.</summary>
- public bool IsDeleted { get; set; }
+ public bool IsDeleted { get; }
/// <summary>Whether the file is hidden from users.</summary>
- public bool IsHidden { get; set; }
+ public bool IsHidden { get; }
/// <summary>Whether this is the default file for the mod.</summary>
- public bool IsDefault { get; set; }
+ public bool IsDefault { get; }
/// <summary>Whether this is an archived file.</summary>
- public bool IsOld { get; set; }
+ public bool IsOld { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The file title.</param>
+ /// <param name="description">The file description.</param>
+ /// <param name="version">The file version.</param>
+ /// <param name="isDeleted">Whether the file is deleted.</param>
+ /// <param name="isHidden">Whether the file is hidden from users.</param>
+ /// <param name="isDefault">Whether this is the default file for the mod.</param>
+ /// <param name="isOld">Whether this is an archived file.</param>
+ public FileDataModel(string name, string description, string version, bool isDeleted, bool isHidden, bool isDefault, bool isOld)
+ {
+ this.Name = name;
+ this.Description = description;
+ this.Version = version;
+ this.IsDeleted = isDeleted;
+ this.IsHidden = isHidden;
+ this.IsDefault = isDefault;
+ this.IsOld = isOld;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs
index 6cae16d9..0654b576 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs
@@ -1,17 +1,33 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
{
/// <summary>Metadata about a mod from the ModDrop API.</summary>
public class ModDataModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The mod's unique ID on ModDrop.</summary>
public int ID { get; set; }
+ /// <summary>The mod name.</summary>
+ public string Title { get; set; }
+
/// <summary>The error code, if any.</summary>
public int? ErrorCode { get; set; }
- /// <summary>The mod name.</summary>
- public string Title { get; set; }
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The mod's unique ID on ModDrop.</param>
+ /// <param name="title">The mod name.</param>
+ /// <param name="errorCode">The error code, if any.</param>
+ public ModDataModel(int id, string title, int? errorCode)
+ {
+ this.ID = id;
+ this.Title = title;
+ this.ErrorCode = errorCode;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs
index 445e25cb..cb4be35c 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
@@ -7,7 +5,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
/// <summary>A list of mods from the ModDrop API.</summary>
public class ModListModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The mod data.</summary>
- public IDictionary<long, ModModel> Mods { get; set; }
+ public IDictionary<long, ModModel> Mods { get; } = new Dictionary<long, ModModel>();
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs
index 8869193e..60b818d6 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs
@@ -1,14 +1,28 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
{
/// <summary>An entry in a mod list from the ModDrop API.</summary>
public class ModModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The available file downloads.</summary>
- public FileDataModel[] Files { get; set; }
+ public FileDataModel[] Files { get; }
/// <summary>The mod metadata.</summary>
- public ModDataModel Mod { get; set; }
+ public ModDataModel Mod { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="files">The available file downloads.</param>
+ /// <param name="mod">The mod metadata.</param>
+ public ModModel(FileDataModel[] files, ModDataModel mod)
+ {
+ this.Files = files;
+ this.Mod = mod;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/DisabledNexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/DisabledNexusClient.cs
new file mode 100644
index 00000000..6edd5f64
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/DisabledNexusClient.cs
@@ -0,0 +1,31 @@
+using System.Threading.Tasks;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
+
+namespace StardewModdingAPI.Web.Framework.Clients.Nexus
+{
+ /// <summary>A client for the Nexus website which does nothing, used for local development.</summary>
+ internal class DisabledNexusClient : INexusClient
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <inheritdoc />
+ public ModSiteKey SiteKey => ModSiteKey.Nexus;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get update check info about a mod.</summary>
+ /// <param name="id">The mod ID.</param>
+ public Task<IModPage?> GetModData(string id)
+ {
+ return Task.FromResult<IModPage?>(
+ new GenericModPage(ModSiteKey.Nexus, id).SetError(RemoteModStatus.TemporaryError, "The Nexus client is currently disabled due to the configuration.")
+ );
+ }
+
+ /// <inheritdoc />
+ public void Dispose() { }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs
index dd0bb94f..23b25f95 100644
--- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -61,7 +59,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- public async Task<IModPage> GetModData(string id)
+ public async Task<IModPage?> GetModData(string id)
{
IModPage page = new GenericModPage(this.SiteKey, id);
@@ -72,7 +70,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
// adult content are hidden for anonymous users, so fall back to the API in that case.
// Note that the API has very restrictive rate limits which means we can't just use it
// for all cases.
- NexusMod mod = await this.GetModFromWebsiteAsync(parsedId);
+ NexusMod? mod = await this.GetModFromWebsiteAsync(parsedId);
if (mod?.Status == NexusModStatus.AdultContentForbidden)
mod = await this.GetModFromApiAsync(parsedId);
@@ -81,16 +79,16 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
return page.SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID.");
// return info
- page.SetInfo(name: mod.Name, url: mod.Url, version: mod.Version, downloads: mod.Downloads);
+ page.SetInfo(name: mod.Name ?? parsedId.ToString(), url: mod.Url ?? this.GetModUrl(parsedId), version: mod.Version, downloads: mod.Downloads);
if (mod.Status != NexusModStatus.Ok)
- page.SetError(RemoteModStatus.TemporaryError, mod.Error);
+ page.SetError(RemoteModStatus.TemporaryError, mod.Error!);
return page;
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.WebClient?.Dispose();
+ this.WebClient.Dispose();
}
@@ -100,7 +98,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
/// <summary>Get metadata about a mod by scraping the Nexus website.</summary>
/// <param name="id">The Nexus mod ID.</param>
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
- private async Task<NexusMod> GetModFromWebsiteAsync(uint id)
+ private async Task<NexusMod?> GetModFromWebsiteAsync(uint id)
{
// fetch HTML
string html;
@@ -116,35 +114,38 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
}
// parse HTML
- var doc = new HtmlDocument();
+ HtmlDocument doc = new();
doc.LoadHtml(html);
// handle Nexus error message
- HtmlNode node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]");
+ HtmlNode? node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]");
if (node != null)
{
string[] errorParts = node.InnerText.Trim().Split(new[] { '\n' }, 2, System.StringSplitOptions.RemoveEmptyEntries);
string errorCode = errorParts[0];
- string errorText = errorParts.Length > 1 ? errorParts[1] : null;
+ string? errorText = errorParts.Length > 1 ? errorParts[1] : null;
switch (errorCode.Trim().ToLower())
{
case "not found":
return null;
default:
- return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = this.GetWebStatus(errorCode) };
+ return new NexusMod(
+ status: this.GetWebStatus(errorCode),
+ error: $"Nexus error: {errorCode} ({errorText})."
+ );
}
}
// extract mod info
string url = this.GetModUrl(id);
- string name = doc.DocumentNode.SelectSingleNode("//div[@id='pagetitle']//h1")?.InnerText.Trim();
- string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim();
- SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion);
+ string? name = doc.DocumentNode.SelectSingleNode("//div[@id='pagetitle']//h1")?.InnerText.Trim();
+ string? version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim();
+ SemanticVersion.TryParse(version, out ISemanticVersion? parsedVersion);
// extract files
var downloads = new List<IModDownload>();
- foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]"))
+ foreach (HtmlNode fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]"))
{
string sectionName = fileSection.Descendants("h2").First().InnerText;
if (sectionName != "Main files" && sectionName != "Optional files")
@@ -154,7 +155,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
{
string fileName = container.GetDataAttribute("name").Value;
string fileVersion = container.GetDataAttribute("version").Value;
- string description = container.SelectSingleNode("following-sibling::*[1][self::dd]//div").InnerText?.Trim(); // get text of next <dd> tag; derived from https://stackoverflow.com/a/25535623/262123
+ string? description = container.SelectSingleNode("following-sibling::*[1][self::dd]//div").InnerText?.Trim(); // get text of next <dd> tag; derived from https://stackoverflow.com/a/25535623/262123
downloads.Add(
new GenericModDownload(fileName, description, fileVersion)
@@ -163,13 +164,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
}
// yield info
- return new NexusMod
- {
- Name = name,
- Version = parsedVersion?.ToString() ?? version,
- Url = url,
- Downloads = downloads.ToArray()
- };
+ return new NexusMod(
+ name: name ?? id.ToString(),
+ version: parsedVersion?.ToString() ?? version,
+ url: url,
+ downloads: downloads.ToArray()
+ );
}
/// <summary>Get metadata about a mod from the Nexus API.</summary>
@@ -182,22 +182,21 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
ModFileList files = await this.ApiClient.ModFiles.GetModFiles("stardewvalley", (int)id, FileCategory.Main, FileCategory.Optional);
// yield info
- return new NexusMod
- {
- Name = mod.Name,
- Version = SemanticVersion.TryParse(mod.Version, out ISemanticVersion version) ? version?.ToString() : mod.Version,
- Url = this.GetModUrl(id),
- Downloads = files.Files
+ return new NexusMod(
+ name: mod.Name,
+ version: SemanticVersion.TryParse(mod.Version, out ISemanticVersion? version) ? version.ToString() : mod.Version,
+ url: this.GetModUrl(id),
+ downloads: files.Files
.Select(file => (IModDownload)new GenericModDownload(file.Name, file.Description, file.FileVersion))
.ToArray()
- };
+ );
}
/// <summary>Get the full mod page URL for a given ID.</summary>
/// <param name="id">The mod ID.</param>
private string GetModUrl(uint id)
{
- UriBuilder builder = new(this.WebClient.BaseClient.BaseAddress);
+ UriBuilder builder = new(this.WebClient.BaseClient.BaseAddress!);
builder.Path += string.Format(this.WebModUrlFormat, id);
return builder.Uri.ToString();
}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs
index 358c4633..3155cfda 100644
--- a/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using System;
using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels
@@ -11,25 +10,53 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels
** Accessors
*********/
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string? Name { get; }
/// <summary>The mod's semantic version number.</summary>
- public string Version { get; set; }
+ public string? Version { get; }
/// <summary>The mod's web URL.</summary>
[JsonProperty("mod_page_uri")]
- public string Url { get; set; }
+ public string? Url { get; }
/// <summary>The mod's publication status.</summary>
[JsonIgnore]
- public NexusModStatus Status { get; set; } = NexusModStatus.Ok;
+ public NexusModStatus Status { get; }
/// <summary>The files available to download.</summary>
[JsonIgnore]
- public IModDownload[] Downloads { get; set; }
+ public IModDownload[] Downloads { get; }
/// <summary>A custom user-friendly error which indicates why fetching the mod info failed (if applicable).</summary>
[JsonIgnore]
- public string Error { get; set; }
+ public string? Error { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The mod name</param>
+ /// <param name="version">The mod's semantic version number.</param>
+ /// <param name="url">The mod's web URL.</param>
+ /// <param name="downloads">The files available to download.</param>
+ public NexusMod(string name, string? version, string url, IModDownload[] downloads)
+ {
+ this.Name = name;
+ this.Version = version;
+ this.Url = url;
+ this.Status = NexusModStatus.Ok;
+ this.Downloads = downloads;
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="status">The mod's publication status.</param>
+ /// <param name="error">A custom user-friendly error which indicates why fetching the mod info failed (if applicable).</param>
+ public NexusMod(NexusModStatus status, string error)
+ {
+ this.Status = status;
+ this.Error = error;
+ this.Downloads = Array.Empty<IModDownload>();
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs
index 03c78e01..431fed7b 100644
--- a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Threading.Tasks;
diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs
index 2d48a7ae..7f40e713 100644
--- a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs
+++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs
@@ -1,17 +1,35 @@
-#nullable disable
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
{
/// <summary>The response for a get-paste request.</summary>
internal class PasteInfo
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>Whether the log was successfully fetched.</summary>
- public bool Success { get; set; }
+ [MemberNotNullWhen(true, nameof(PasteInfo.Content))]
+ [MemberNotNullWhen(false, nameof(PasteInfo.Error))]
+ public bool Success => this.Error == null || this.Content != null;
/// <summary>The fetched paste content (if <see cref="Success"/> is <c>true</c>).</summary>
- public string Content { get; set; }
+ public string? Content { get; internal set; }
- /// <summary>The error message if saving failed.</summary>
- public string Error { get; set; }
+ /// <summary>The error message (if <see cref="Success"/> is <c>false</c>).</summary>
+ public string? Error { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="content">The fetched paste content.</param>
+ /// <param name="error">The error message, if it failed.</param>
+ public PasteInfo(string? content, string? error)
+ {
+ this.Content = content;
+ this.Error = error;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs
index d0cdf374..0e00f071 100644
--- a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Net;
using System.Threading.Tasks;
@@ -35,24 +33,24 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
try
{
// get from API
- string content = await this.Client
+ string? content = await this.Client
.GetAsync($"raw/{id}")
.AsString();
// handle Pastebin errors
if (string.IsNullOrWhiteSpace(content))
- return new PasteInfo { Error = "Received an empty response from Pastebin." };
+ return new PasteInfo(null, "Received an empty response from Pastebin.");
if (content.StartsWith("<!DOCTYPE"))
- return new PasteInfo { Error = $"Received a captcha challenge from Pastebin. Please visit https://pastebin.com/{id} in a new window to solve it." };
- return new PasteInfo { Success = true, Content = content };
+ return new PasteInfo(null, $"Received a captcha challenge from Pastebin. Please visit https://pastebin.com/{id} in a new window to solve it.");
+ return new PasteInfo(content, null);
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
- return new PasteInfo { Error = "There's no log with that ID." };
+ return new PasteInfo(null, "There's no log with that ID.");
}
catch (Exception ex)
{
- return new PasteInfo { Error = $"Pastebin error: {ex}" };
+ return new PasteInfo(null, $"Pastebin error: {ex}");
}
}
diff --git a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
index 843b7735..e7a2df13 100644
--- a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
+++ b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;
using System.Text;
@@ -53,8 +52,12 @@ namespace StardewModdingAPI.Web.Framework.Compression
/// <summary>Decompress a string.</summary>
/// <param name="rawText">The compressed text.</param>
/// <remarks>Derived from <a href="https://stackoverflow.com/a/17993002/262123"/>.</remarks>
- public string DecompressString(string rawText)
+ [return: NotNullIfNotNull("rawText")]
+ public string? DecompressString(string? rawText)
{
+ if (rawText is null)
+ return rawText;
+
// get raw bytes
byte[] zipBuffer;
try
diff --git a/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs b/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs
index e1ec9b67..ef2d5696 100644
--- a/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs
+++ b/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs
@@ -1,4 +1,4 @@
-#nullable disable
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Web.Framework.Compression
{
@@ -14,6 +14,7 @@ namespace StardewModdingAPI.Web.Framework.Compression
/// <summary>Decompress a string.</summary>
/// <param name="rawText">The compressed text.</param>
- string DecompressString(string rawText);
+ [return: NotNullIfNotNull("rawText")]
+ string? DecompressString(string? rawText);
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
index 3730a9db..b582b2b0 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.Framework.ConfigModels
{
/// <summary>The config settings for the API clients.</summary>
@@ -12,17 +10,17 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
** Generic
****/
/// <summary>The user agent for API clients, where {0} is the SMAPI version.</summary>
- public string UserAgent { get; set; }
+ public string UserAgent { get; set; } = null!;
/****
** Azure
****/
/// <summary>The connection string for the Azure Blob storage account.</summary>
- public string AzureBlobConnectionString { get; set; }
+ public string? AzureBlobConnectionString { get; set; }
/// <summary>The Azure Blob container in which to store temporary uploaded logs.</summary>
- public string AzureBlobTempContainer { get; set; }
+ public string AzureBlobTempContainer { get; set; } = null!;
/// <summary>The number of days since the blob's last-modified date when it will be deleted.</summary>
public int AzureBlobTempExpiryDays { get; set; }
@@ -32,65 +30,65 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
** Chucklefish
****/
/// <summary>The base URL for the Chucklefish mod site.</summary>
- public string ChucklefishBaseUrl { get; set; }
+ public string ChucklefishBaseUrl { get; set; } = null!;
/// <summary>The URL for a mod page on the Chucklefish mod site excluding the <see cref="GitHubBaseUrl"/>, where {0} is the mod ID.</summary>
- public string ChucklefishModPageUrlFormat { get; set; }
+ public string ChucklefishModPageUrlFormat { get; set; } = null!;
/****
** CurseForge
****/
/// <summary>The base URL for the CurseForge API.</summary>
- public string CurseForgeBaseUrl { get; set; }
+ public string CurseForgeBaseUrl { get; set; } = null!;
/****
** GitHub
****/
/// <summary>The base URL for the GitHub API.</summary>
- public string GitHubBaseUrl { get; set; }
+ public string GitHubBaseUrl { get; set; } = null!;
/// <summary>The Accept header value expected by the GitHub API.</summary>
- public string GitHubAcceptHeader { get; set; }
+ public string GitHubAcceptHeader { get; set; } = null!;
/// <summary>The username with which to authenticate to the GitHub API (if any).</summary>
- public string GitHubUsername { get; set; }
+ public string? GitHubUsername { get; set; }
/// <summary>The password with which to authenticate to the GitHub API (if any).</summary>
- public string GitHubPassword { get; set; }
+ public string? GitHubPassword { get; set; }
/****
** ModDrop
****/
/// <summary>The base URL for the ModDrop API.</summary>
- public string ModDropApiUrl { get; set; }
+ public string ModDropApiUrl { get; set; } = null!;
/// <summary>The URL for a ModDrop mod page for the user, where {0} is the mod ID.</summary>
- public string ModDropModPageUrl { get; set; }
+ public string ModDropModPageUrl { get; set; } = null!;
/****
** Nexus Mods
****/
/// <summary>The base URL for the Nexus Mods API.</summary>
- public string NexusBaseUrl { get; set; }
+ public string NexusBaseUrl { get; set; } = null!;
/// <summary>The URL for a Nexus mod page for the user, excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
- public string NexusModUrlFormat { get; set; }
+ public string NexusModUrlFormat { get; set; } = null!;
/// <summary>The URL for a Nexus mod page to scrape for versions, excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
- public string NexusModScrapeUrlFormat { get; set; }
+ public string NexusModScrapeUrlFormat { get; set; } = null!;
/// <summary>The Nexus API authentication key.</summary>
- public string NexusApiKey { get; set; }
+ public string? NexusApiKey { get; set; }
/****
** Pastebin
****/
/// <summary>The base URL for the Pastebin API.</summary>
- public string PastebinBaseUrl { get; set; }
+ public string PastebinBaseUrl { get; set; } = null!;
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs
index 682c97e6..e46ecf2b 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs
@@ -1,17 +1,15 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.Framework.ConfigModels
{
/// <summary>Override update-check metadata for a mod.</summary>
internal class ModOverrideConfig
{
/// <summary>The unique ID from the mod's manifest.</summary>
- public string ID { get; set; }
+ public string ID { get; set; } = null!;
/// <summary>Whether to allow non-standard versions.</summary>
public bool AllowNonStandardVersions { get; set; }
/// <summary>The mod page URL to use regardless of which site has the update, or <c>null</c> to use the site URL.</summary>
- public string SetUrl { get; set; }
+ public string? SetUrl { get; set; }
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
index e525e09a..c3b136e8 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
@@ -1,4 +1,4 @@
-#nullable disable
+using System;
namespace StardewModdingAPI.Web.Framework.ConfigModels
{
@@ -8,16 +8,16 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/*********
** Accessors
*********/
- /// <summary>The number of minutes successful update checks should be cached before refetching them.</summary>
+ /// <summary>The number of minutes successful update checks should be cached before re-fetching them.</summary>
public int SuccessCacheMinutes { get; set; }
- /// <summary>The number of minutes failed update checks should be cached before refetching them.</summary>
+ /// <summary>The number of minutes failed update checks should be cached before re-fetching them.</summary>
public int ErrorCacheMinutes { get; set; }
/// <summary>Update-check metadata to override.</summary>
- public ModOverrideConfig[] ModOverrides { get; set; }
+ public ModOverrideConfig[] ModOverrides { get; set; } = Array.Empty<ModOverrideConfig>();
/// <summary>The update-check config for SMAPI's own update checks.</summary>
- public SmapiInfoConfig SmapiInfo { get; set; }
+ public SmapiInfoConfig SmapiInfo { get; set; } = null!;
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
index ef6c2659..62685e47 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.Framework.ConfigModels
{
/// <summary>The site config settings.</summary>
@@ -9,9 +7,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
** Accessors
*********/
/// <summary>A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format.</summary>
- public string OtherBlurb { get; set; }
+ public string? OtherBlurb { get; set; }
/// <summary>A list of supports to credit on the main page, in Markdown format.</summary>
- public string SupporterList { get; set; }
+ public string? SupporterList { get; set; }
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs
index dbf58817..a95e0048 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs
@@ -1,4 +1,4 @@
-#nullable disable
+using System;
namespace StardewModdingAPI.Web.Framework.ConfigModels
{
@@ -6,12 +6,12 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
internal class SmapiInfoConfig
{
/// <summary>The mod ID used for SMAPI update checks.</summary>
- public string ID { get; set; }
+ public string ID { get; set; } = null!;
/// <summary>The default update key used for SMAPI update checks.</summary>
- public string DefaultUpdateKey { get; set; }
+ public string DefaultUpdateKey { get; set; } = null!;
/// <summary>The update keys to add for SMAPI update checks when the player has a beta version installed.</summary>
- public string[] AddBetaUpdateKeys { get; set; }
+ public string[] AddBetaUpdateKeys { get; set; } = Array.Empty<string>();
}
}
diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs
index a72c12c1..62a23155 100644
--- a/src/SMAPI.Web/Framework/Extensions.cs
+++ b/src/SMAPI.Web/Framework/Extensions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Html;
@@ -28,7 +26,7 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="values">An object that contains route values.</param>
/// <param name="absoluteUrl">Get an absolute URL instead of a server-relative path/</param>
/// <returns>The generated URL.</returns>
- public static string PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object values = null, bool absoluteUrl = false)
+ public static string? PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object? values = null, bool absoluteUrl = false)
{
// get route values
RouteValueDictionary valuesDict = new(values);
@@ -39,7 +37,7 @@ namespace StardewModdingAPI.Web.Framework
}
// get relative URL
- string url = helper.Action(action, controller, valuesDict);
+ string? url = helper.Action(action, controller, valuesDict);
if (url == null && action.EndsWith("Async"))
url = helper.Action(action[..^"Async".Length], controller, valuesDict);
@@ -59,7 +57,7 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="value">The value to serialize.</param>
/// <returns>The serialized JSON.</returns>
/// <remarks>This bypasses unnecessary validation (e.g. not allowing null values) in <see cref="IJsonHelper.Serialize"/>.</remarks>
- public static IHtmlContent ForJson(this RazorPageBase page, object value)
+ public static IHtmlContent ForJson(this RazorPageBase page, object? value)
{
string json = JsonConvert.SerializeObject(value);
return new HtmlString(json);
diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs
index b8d1f62c..fe171785 100644
--- a/src/SMAPI.Web/Framework/IModDownload.cs
+++ b/src/SMAPI.Web/Framework/IModDownload.cs
@@ -1,17 +1,18 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.Framework
{
/// <summary>Generic metadata about a file download on a mod page.</summary>
internal interface IModDownload
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The download's display name.</summary>
string Name { get; }
/// <summary>The download's description.</summary>
- string Description { get; }
+ string? Description { get; }
/// <summary>The download's file version.</summary>
- string Version { get; }
+ string? Version { get; }
}
}
diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs
index 68220b49..4d0a8d61 100644
--- a/src/SMAPI.Web/Framework/IModPage.cs
+++ b/src/SMAPI.Web/Framework/IModPage.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
namespace StardewModdingAPI.Web.Framework
@@ -18,13 +17,13 @@ namespace StardewModdingAPI.Web.Framework
string Id { get; }
/// <summary>The mod name.</summary>
- string Name { get; }
+ string? Name { get; }
/// <summary>The mod's semantic version number.</summary>
- string Version { get; }
+ string? Version { get; }
/// <summary>The mod's web URL.</summary>
- string Url { get; }
+ string? Url { get; }
/// <summary>The mod downloads.</summary>
IModDownload[] Downloads { get; }
@@ -33,7 +32,12 @@ namespace StardewModdingAPI.Web.Framework
RemoteModStatus Status { get; }
/// <summary>A user-friendly error which indicates why fetching the mod info failed (if applicable).</summary>
- string Error { get; }
+ string? Error { get; }
+
+ /// <summary>Whether the mod data is valid.</summary>
+ [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))]
+ [MemberNotNullWhen(false, nameof(IModPage.Error))]
+ bool IsValid { get; }
/*********
@@ -44,7 +48,7 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="version">The mod's semantic version number.</param>
/// <param name="url">The mod's web URL.</param>
/// <param name="downloads">The mod downloads.</param>
- IModPage SetInfo(string name, string version, string url, IEnumerable<IModDownload> downloads);
+ IModPage SetInfo(string name, string? version, string url, IEnumerable<IModDownload> downloads);
/// <summary>Set a mod fetch error.</summary>
/// <param name="status">The mod availability status on the remote site.</param>
diff --git a/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs b/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs
index 98738a82..2c24c610 100644
--- a/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs
+++ b/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
diff --git a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs
index 8db43dca..3c1405eb 100644
--- a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs
+++ b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Hangfire.Dashboard;
namespace StardewModdingAPI.Web.Framework
diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs
index 021d14fb..e70b60bf 100644
--- a/src/SMAPI.Web/Framework/ModInfoModel.cs
+++ b/src/SMAPI.Web/Framework/ModInfoModel.cs
@@ -1,4 +1,5 @@
-#nullable disable
+using System.Diagnostics.CodeAnalysis;
+using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework
{
@@ -9,22 +10,22 @@ namespace StardewModdingAPI.Web.Framework
** Accessors
*********/
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string? Name { get; private set; }
+
+ /// <summary>The mod's web URL.</summary>
+ public string? Url { get; private set; }
/// <summary>The mod's latest version.</summary>
- public ISemanticVersion Version { get; set; }
+ public ISemanticVersion? Version { get; private set; }
/// <summary>The mod's latest optional or prerelease version, if newer than <see cref="Version"/>.</summary>
- public ISemanticVersion PreviewVersion { get; set; }
-
- /// <summary>The mod's web URL.</summary>
- public string Url { get; set; }
+ public ISemanticVersion? PreviewVersion { get; private set; }
/// <summary>The mod availability status on the remote site.</summary>
- public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok;
+ public RemoteModStatus Status { get; private set; }
/// <summary>The error message indicating why the mod is invalid (if applicable).</summary>
- public string Error { get; set; }
+ public string? Error { get; private set; }
/*********
@@ -35,19 +36,24 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Construct an instance.</summary>
/// <param name="name">The mod name.</param>
+ /// <param name="url">The mod's web URL.</param>
/// <param name="version">The semantic version for the mod's latest release.</param>
/// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
- /// <param name="url">The mod's web URL.</param>
- public ModInfoModel(string name, ISemanticVersion version, string url, ISemanticVersion previewVersion = null)
+ /// <param name="status">The mod availability status on the remote site.</param>
+ /// <param name="error">The error message indicating why the mod is invalid (if applicable).</param>
+ [JsonConstructor]
+ public ModInfoModel(string name, string url, ISemanticVersion? version, ISemanticVersion? previewVersion = null, RemoteModStatus status = RemoteModStatus.Ok, string? error = null)
{
this
.SetBasicInfo(name, url)
- .SetVersions(version, previewVersion);
+ .SetVersions(version!, previewVersion)
+ .SetError(status, error!);
}
/// <summary>Set the basic mod info.</summary>
/// <param name="name">The mod name.</param>
/// <param name="url">The mod's web URL.</param>
+ [MemberNotNull(nameof(ModInfoModel.Name), nameof(ModInfoModel.Url))]
public ModInfoModel SetBasicInfo(string name, string url)
{
this.Name = name;
@@ -59,7 +65,8 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Set the mod version info.</summary>
/// <param name="version">The semantic version for the mod's latest release.</param>
/// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
- public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion previewVersion = null)
+ [MemberNotNull(nameof(ModInfoModel.Version))]
+ public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null)
{
this.Version = version;
this.PreviewVersion = previewVersion;
diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs
index 2d6755d8..674b9ffc 100644
--- a/src/SMAPI.Web/Framework/ModSiteManager.cs
+++ b/src/SMAPI.Web/Framework/ModSiteManager.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@@ -36,12 +35,15 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="updateKey">The namespaced update key.</param>
public async Task<IModPage> GetModPageAsync(UpdateKey updateKey)
{
+ if (!updateKey.LooksValid)
+ return new GenericModPage(updateKey.Site, updateKey.ID!).SetError(RemoteModStatus.DoesNotExist, $"Invalid update key '{updateKey}'.");
+
// get site
- if (!this.ModSites.TryGetValue(updateKey.Site, out IModSiteClient client))
+ if (!this.ModSites.TryGetValue(updateKey.Site, out IModSiteClient? client))
return new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Site}'. Expected one of [{string.Join(", ", this.ModSites.Keys)}].");
// fetch mod
- IModPage mod;
+ IModPage? mod;
try
{
mod = await client.GetModData(updateKey.ID);
@@ -60,39 +62,42 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param>
/// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param>
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
- public ModInfoModel GetPageVersions(IModPage page, string subkey, bool allowNonStandardVersions, ChangeDescriptor mapRemoteVersions)
+ public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions)
{
// get base model
- ModInfoModel model = new ModInfoModel()
- .SetBasicInfo(page.Name, page.Url)
- .SetError(page.Status, page.Error);
- if (page.Status != RemoteModStatus.Ok)
+ ModInfoModel model = new();
+ if (page.IsValid)
+ model.SetBasicInfo(page.Name, page.Url);
+ else
+ {
+ model.SetError(page.Status, page.Error);
return model;
+ }
// fetch versions
- bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion mainVersion, out ISemanticVersion previewVersion);
+ bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion);
if (!hasVersions && subkey != null)
hasVersions = this.TryGetLatestVersions(page, null, allowNonStandardVersions, mapRemoteVersions, out mainVersion, out previewVersion);
if (!hasVersions)
return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions.");
// return info
- return model.SetVersions(mainVersion, previewVersion);
+ return model.SetVersions(mainVersion!, previewVersion);
}
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The version to parse.</param>
/// <param name="map">Changes to apply to the raw version, if any.</param>
/// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
- public ISemanticVersion GetMappedVersion(string version, ChangeDescriptor map, bool allowNonStandard)
+ public ISemanticVersion? GetMappedVersion(string? version, ChangeDescriptor? map, bool allowNonStandard)
{
// try mapped version
- string rawNewVersion = this.GetRawMappedVersion(version, map, allowNonStandard);
- if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion parsedNew))
+ string? rawNewVersion = this.GetRawMappedVersion(version, map);
+ if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion? parsedNew))
return parsedNew;
// return original version
- return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsedOld)
+ return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion? parsedOld)
? parsedOld
: null;
}
@@ -108,31 +113,31 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param>
/// <param name="main">The main mod version.</param>
/// <param name="preview">The latest prerelease version, if newer than <paramref name="main"/>.</param>
- private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, ChangeDescriptor mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview)
+ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview)
{
main = null;
preview = null;
// parse all versions from the mod page
- IEnumerable<(string name, string description, ISemanticVersion version)> GetAllVersions()
+ IEnumerable<(string? name, string? description, ISemanticVersion? version)> GetAllVersions()
{
if (mod != null)
{
- ISemanticVersion ParseAndMapVersion(string raw)
+ ISemanticVersion? ParseAndMapVersion(string? raw)
{
raw = this.NormalizeVersion(raw);
return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions);
}
// get mod version
- ISemanticVersion modVersion = ParseAndMapVersion(mod.Version);
+ ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version);
if (modVersion != null)
yield return (name: null, description: null, version: ParseAndMapVersion(mod.Version));
// get file versions
foreach (IModDownload download in mod.Downloads)
{
- ISemanticVersion cur = ParseAndMapVersion(download.Version);
+ ISemanticVersion? cur = ParseAndMapVersion(download.Version);
if (cur != null)
yield return (download.Name, download.Description, cur);
}
@@ -143,15 +148,15 @@ namespace StardewModdingAPI.Web.Framework
.ToArray();
// get main + preview versions
- void TryGetVersions(out ISemanticVersion mainVersion, out ISemanticVersion previewVersion, Func<(string name, string description, ISemanticVersion version), bool> filter = null)
+ void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, Func<(string? name, string? description, ISemanticVersion? version), bool>? filter = null)
{
mainVersion = null;
previewVersion = null;
// get latest main + preview version
- foreach (var entry in versions)
+ foreach ((string? name, string? description, ISemanticVersion? version) entry in versions)
{
- if (filter?.Invoke(entry) == false)
+ if (entry.version is null || filter?.Invoke(entry) == false)
continue;
if (entry.version.IsPrerelease())
@@ -160,7 +165,7 @@ namespace StardewModdingAPI.Web.Framework
mainVersion ??= entry.version;
if (mainVersion != null)
- break; // any other values will be older
+ break; // any others will be older since entries are sorted by version
}
// normalize values
@@ -183,8 +188,7 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The version to map.</param>
/// <param name="map">Changes to apply to the raw version, if any.</param>
- /// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
- private string GetRawMappedVersion(string version, ChangeDescriptor map, bool allowNonStandard)
+ private string? GetRawMappedVersion(string? version, ChangeDescriptor? map)
{
if (version == null || map?.HasChanges != true)
return version;
@@ -197,7 +201,7 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Normalize a version string.</summary>
/// <param name="version">The version to normalize.</param>
- private string NormalizeVersion(string version)
+ private string? NormalizeVersion(string? version)
{
if (string.IsNullOrWhiteSpace(version))
return null;
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs
index fe601524..7b8f0ec9 100644
--- a/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Net;
using Microsoft.AspNetCore.Rewrite;
@@ -13,7 +11,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
** Fields
*********/
/// <summary>Maps a lowercase hostname to the resulting redirect URL.</summary>
- private readonly Func<string, string> Map;
+ private readonly Func<string, string?> Map;
/*********
@@ -22,7 +20,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <summary>Construct an instance.</summary>
/// <param name="statusCode">The status code to use for redirects.</param>
/// <param name="map">Hostnames mapped to the resulting redirect URL.</param>
- public RedirectHostsToUrlsRule(HttpStatusCode statusCode, Func<string, string> map)
+ public RedirectHostsToUrlsRule(HttpStatusCode statusCode, Func<string, string?> map)
{
this.StatusCode = statusCode;
this.Map = map ?? throw new ArgumentNullException(nameof(map));
@@ -35,10 +33,10 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <summary>Get the new redirect URL.</summary>
/// <param name="context">The rewrite context.</param>
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
- protected override string GetNewUrl(RewriteContext context)
+ protected override string? GetNewUrl(RewriteContext context)
{
// get requested host
- string host = context.HttpContext.Request.Host.Host;
+ string? host = context.HttpContext.Request.Host.Host;
// get new host
host = this.Map(host);
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs
index 81a265c9..b46e8f69 100644
--- a/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Net;
using Microsoft.AspNetCore.Http;
@@ -24,7 +22,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <param name="context">The rewrite context.</param>
public void ApplyRule(RewriteContext context)
{
- string newUrl = this.GetNewUrl(context);
+ string? newUrl = this.GetNewUrl(context);
if (newUrl == null)
return;
@@ -41,7 +39,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <summary>Get the new redirect URL.</summary>
/// <param name="context">The rewrite context.</param>
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
- protected abstract string GetNewUrl(RewriteContext context);
+ protected abstract string? GetNewUrl(RewriteContext context);
/// <summary>Get the full request URL.</summary>
/// <param name="request">The request.</param>
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs
index cb3e53ef..e691ffba 100644
--- a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using System.Net;
@@ -39,9 +37,9 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <summary>Get the new redirect URL.</summary>
/// <param name="context">The rewrite context.</param>
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
- protected override string GetNewUrl(RewriteContext context)
+ protected override string? GetNewUrl(RewriteContext context)
{
- string path = context.HttpContext.Request.Path.Value;
+ string? path = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrWhiteSpace(path))
{
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs
index dd7c836f..01807608 100644
--- a/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Net;
using Microsoft.AspNetCore.Http;
@@ -22,7 +20,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
*********/
/// <summary>Construct an instance.</summary>
/// <param name="except">Matches requests which should be ignored.</param>
- public RedirectToHttpsRule(Func<HttpRequest, bool> except = null)
+ public RedirectToHttpsRule(Func<HttpRequest, bool>? except = null)
{
this.Except = except ?? (_ => false);
this.StatusCode = HttpStatusCode.RedirectKeepVerb;
@@ -35,7 +33,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <summary>Get the new redirect URL.</summary>
/// <param name="context">The rewrite context.</param>
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
- protected override string GetNewUrl(RewriteContext context)
+ protected override string? GetNewUrl(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
if (request.IsHttps || this.Except(request))
diff --git a/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs b/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs
index 2eca4845..dfc1fb47 100644
--- a/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs
+++ b/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Threading.Tasks;
namespace StardewModdingAPI.Web.Framework.Storage
diff --git a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
index 0177e602..effbbc9f 100644
--- a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
+++ b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -65,11 +63,11 @@ namespace StardewModdingAPI.Web.Framework.Storage
BlobClient blob = this.GetAzureBlobClient(id);
await blob.UploadAsync(stream);
- return new UploadResult(true, id, null);
+ return new UploadResult(id, null);
}
catch (Exception ex)
{
- return new UploadResult(false, null, ex.Message);
+ return new UploadResult(null, ex.Message);
}
}
@@ -77,10 +75,10 @@ namespace StardewModdingAPI.Web.Framework.Storage
else
{
string path = this.GetDevFilePath(id);
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, content);
- return new UploadResult(true, id, null);
+ return new UploadResult(id, null);
}
}
@@ -110,21 +108,15 @@ namespace StardewModdingAPI.Web.Framework.Storage
string content = this.GzipHelper.DecompressString(reader.ReadToEnd());
// build model
- return new StoredFileInfo
- {
- Success = true,
- Content = content,
- Expiry = expiry.UtcDateTime
- };
+ return new StoredFileInfo(content, expiry);
}
catch (RequestFailedException ex)
{
- return new StoredFileInfo
- {
- Error = ex.ErrorCode == "BlobNotFound"
+ return new StoredFileInfo(
+ error: ex.ErrorCode == "BlobNotFound"
? "There's no file with that ID."
: $"Could not fetch that file from storage ({ex.ErrorCode}: {ex.Message})."
- };
+ );
}
}
@@ -137,10 +129,7 @@ namespace StardewModdingAPI.Web.Framework.Storage
file.Delete();
if (!file.Exists)
{
- return new StoredFileInfo
- {
- Error = "There's no file with that ID."
- };
+ return new StoredFileInfo(error: "There's no file with that ID.");
}
// renew
@@ -151,13 +140,11 @@ namespace StardewModdingAPI.Web.Framework.Storage
}
// build model
- return new StoredFileInfo
- {
- Success = true,
- Content = File.ReadAllText(file.FullName),
- Expiry = DateTime.UtcNow.AddDays(this.ExpiryDays),
- Warning = "This file was saved temporarily to the local computer. This should only happen in a local development environment."
- };
+ return new StoredFileInfo(
+ content: File.ReadAllText(file.FullName),
+ expiry: DateTime.UtcNow.AddDays(this.ExpiryDays),
+ warning: "This file was saved temporarily to the local computer. This should only happen in a local development environment."
+ );
}
}
@@ -166,12 +153,7 @@ namespace StardewModdingAPI.Web.Framework.Storage
{
PasteInfo response = await this.Pastebin.GetAsync(id);
response.Content = this.GzipHelper.DecompressString(response.Content);
- return new StoredFileInfo
- {
- Success = response.Success,
- Content = response.Content,
- Error = response.Error
- };
+ return new StoredFileInfo(response.Content, null, error: response.Error);
}
}
@@ -179,8 +161,8 @@ namespace StardewModdingAPI.Web.Framework.Storage
/// <param name="id">The file ID.</param>
private BlobClient GetAzureBlobClient(string id)
{
- var azure = new BlobServiceClient(this.ClientsConfig.AzureBlobConnectionString);
- var container = azure.GetBlobContainerClient(this.ClientsConfig.AzureBlobTempContainer);
+ BlobServiceClient azure = new(this.ClientsConfig.AzureBlobConnectionString);
+ BlobContainerClient container = azure.GetBlobContainerClient(this.ClientsConfig.AzureBlobTempContainer);
return container.GetBlobClient($"uploads/{id}");
}
diff --git a/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs b/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs
index cd941c94..bbbcf2a9 100644
--- a/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs
+++ b/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs
@@ -1,25 +1,52 @@
-#nullable disable
-
using System;
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Web.Framework.Storage
{
/// <summary>The response for a get-file request.</summary>
internal class StoredFileInfo
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>Whether the file was successfully fetched.</summary>
- public bool Success { get; set; }
+ [MemberNotNullWhen(true, nameof(StoredFileInfo.Content))]
+ public bool Success => this.Content != null && this.Error == null;
/// <summary>The fetched file content (if <see cref="Success"/> is <c>true</c>).</summary>
- public string Content { get; set; }
+ public string? Content { get; }
/// <summary>When the file will no longer be available.</summary>
- public DateTime? Expiry { get; set; }
+ public DateTimeOffset? Expiry { get; }
/// <summary>The error message if saving succeeded, but a non-blocking issue was encountered.</summary>
- public string Warning { get; set; }
+ public string? Warning { get; }
/// <summary>The error message if saving failed.</summary>
- public string Error { get; set; }
+ public string? Error { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="content">The fetched file content (if <see cref="Success"/> is <c>true</c>).</param>
+ /// <param name="expiry">When the file will no longer be available.</param>
+ /// <param name="warning">The error message if saving succeeded, but a non-blocking issue was encountered.</param>
+ /// <param name="error">The error message if saving failed.</param>
+ public StoredFileInfo(string? content, DateTimeOffset? expiry, string? warning = null, string? error = null)
+ {
+ this.Content = content;
+ this.Expiry = expiry;
+ this.Warning = warning;
+ this.Error = error;
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="error">The error message if saving failed.</param>
+ public StoredFileInfo(string error)
+ {
+ this.Error = error;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Storage/UploadResult.cs b/src/SMAPI.Web/Framework/Storage/UploadResult.cs
index b1eedd59..92993d42 100644
--- a/src/SMAPI.Web/Framework/Storage/UploadResult.cs
+++ b/src/SMAPI.Web/Framework/Storage/UploadResult.cs
@@ -1,4 +1,4 @@
-#nullable disable
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Web.Framework.Storage
{
@@ -9,25 +9,25 @@ namespace StardewModdingAPI.Web.Framework.Storage
** Accessors
*********/
/// <summary>Whether the file upload succeeded.</summary>
- public bool Succeeded { get; }
+ [MemberNotNullWhen(true, nameof(UploadResult.ID))]
+ [MemberNotNullWhen(false, nameof(UploadResult.UploadError))]
+ public bool Succeeded => this.ID != null && this.UploadError == null;
/// <summary>The file ID, if applicable.</summary>
- public string ID { get; }
+ public string? ID { get; }
/// <summary>The upload error, if any.</summary>
- public string UploadError { get; }
+ public string? UploadError { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="succeeded">Whether the file upload succeeded.</param>
/// <param name="id">The file ID, if applicable.</param>
/// <param name="uploadError">The upload error, if any.</param>
- public UploadResult(bool succeeded, string id, string uploadError)
+ public UploadResult(string? id, string? uploadError)
{
- this.Succeeded = succeeded;
this.ID = id;
this.UploadError = uploadError;
}
diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs
index f230a95b..1b1abd81 100644
--- a/src/SMAPI.Web/Framework/VersionConstraint.cs
+++ b/src/SMAPI.Web/Framework/VersionConstraint.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
@@ -20,7 +18,7 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="values">A dictionary that contains the parameters for the URL.</param>
/// <param name="routeDirection">An object that indicates whether the constraint check is being performed when an incoming request is being handled or when a URL is being generated.</param>
/// <returns><c>true</c> if the URL parameter contains a valid value; otherwise, <c>false</c>.</returns>
- public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
+ public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeKey == null)
throw new ArgumentNullException(nameof(routeKey));
@@ -28,7 +26,7 @@ namespace StardewModdingAPI.Web.Framework
throw new ArgumentNullException(nameof(values));
return
- values.TryGetValue(routeKey, out object routeValue)
+ values.TryGetValue(routeKey, out object? routeValue)
&& routeValue is string routeStr
&& SemanticVersion.TryParse(routeStr, allowNonStandard: true, out _);
}
diff --git a/src/SMAPI.Web/Program.cs b/src/SMAPI.Web/Program.cs
index 5134791a..1fdd3185 100644
--- a/src/SMAPI.Web/Program.cs
+++ b/src/SMAPI.Web/Program.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 0199938d..2693aa90 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Net;
using Hangfire;
@@ -102,7 +100,7 @@ namespace StardewModdingAPI.Web
// init API clients
{
ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get<ApiClientsConfig>();
- string version = this.GetType().Assembly.GetName().Version.ToString(3);
+ string version = this.GetType().Assembly.GetName().Version!.ToString(3);
string userAgent = string.Format(api.UserAgent, version);
services.AddSingleton<IChucklefishClient>(new ChucklefishClient(
@@ -130,14 +128,21 @@ namespace StardewModdingAPI.Web
modUrlFormat: api.ModDropModPageUrl
));
- services.AddSingleton<INexusClient>(new NexusClient(
- webUserAgent: userAgent,
- webBaseUrl: api.NexusBaseUrl,
- webModUrlFormat: api.NexusModUrlFormat,
- webModScrapeUrlFormat: api.NexusModScrapeUrlFormat,
- apiAppVersion: version,
- apiKey: api.NexusApiKey
- ));
+ if (!string.IsNullOrWhiteSpace(api.NexusApiKey))
+ {
+ services.AddSingleton<INexusClient>(new NexusClient(
+ webUserAgent: userAgent,
+ webBaseUrl: api.NexusBaseUrl,
+ webModUrlFormat: api.NexusModUrlFormat,
+ webModScrapeUrlFormat: api.NexusModScrapeUrlFormat,
+ apiAppVersion: version,
+ apiKey: api.NexusApiKey
+ ));
+ }
+ else
+ {
+ services.AddSingleton<INexusClient>(new DisabledNexusClient());
+ }
services.AddSingleton<IPastebinClient>(new PastebinClient(
baseUrl: api.PastebinBaseUrl,
diff --git a/src/SMAPI.Web/ViewModels/IndexModel.cs b/src/SMAPI.Web/ViewModels/IndexModel.cs
index 2283acd9..098f18cc 100644
--- a/src/SMAPI.Web/ViewModels/IndexModel.cs
+++ b/src/SMAPI.Web/ViewModels/IndexModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.ViewModels
{
/// <summary>The view model for the index page.</summary>
@@ -9,26 +7,23 @@ namespace StardewModdingAPI.Web.ViewModels
** Accessors
*********/
/// <summary>The latest stable SMAPI version.</summary>
- public IndexVersionModel StableVersion { get; set; }
+ public IndexVersionModel StableVersion { get; }
/// <summary>A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format.</summary>
- public string OtherBlurb { get; set; }
+ public string? OtherBlurb { get; }
/// <summary>A list of supports to credit on the main page, in Markdown format.</summary>
- public string SupporterList { get; set; }
+ public string? SupporterList { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public IndexModel() { }
-
- /// <summary>Construct an instance.</summary>
/// <param name="stableVersion">The latest stable SMAPI version.</param>
/// <param name="otherBlurb">A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format.</param>
/// <param name="supporterList">A list of supports to credit on the main page, in Markdown format.</param>
- internal IndexModel(IndexVersionModel stableVersion, string otherBlurb, string supporterList)
+ internal IndexModel(IndexVersionModel stableVersion, string? otherBlurb, string? supporterList)
{
this.StableVersion = stableVersion;
this.OtherBlurb = otherBlurb;
diff --git a/src/SMAPI.Web/ViewModels/IndexVersionModel.cs b/src/SMAPI.Web/ViewModels/IndexVersionModel.cs
index 1f5d4ec0..a76a5924 100644
--- a/src/SMAPI.Web/ViewModels/IndexVersionModel.cs
+++ b/src/SMAPI.Web/ViewModels/IndexVersionModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.ViewModels
{
/// <summary>The fields for a SMAPI version.</summary>
@@ -9,30 +7,27 @@ namespace StardewModdingAPI.Web.ViewModels
** Accessors
*********/
/// <summary>The release version.</summary>
- public string Version { get; set; }
+ public string Version { get; }
/// <summary>The Markdown description for the release.</summary>
- public string Description { get; set; }
+ public string Description { get; }
/// <summary>The main download URL.</summary>
- public string DownloadUrl { get; set; }
+ public string DownloadUrl { get; }
/// <summary>The for-developers download URL (not applicable for prerelease versions).</summary>
- public string DevDownloadUrl { get; set; }
+ public string? DevDownloadUrl { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public IndexVersionModel() { }
-
- /// <summary>Construct an instance.</summary>
/// <param name="version">The release number.</param>
/// <param name="description">The Markdown description for the release.</param>
/// <param name="downloadUrl">The main download URL.</param>
/// <param name="devDownloadUrl">The for-developers download URL (not applicable for prerelease versions).</param>
- internal IndexVersionModel(string version, string description, string downloadUrl, string devDownloadUrl)
+ internal IndexVersionModel(string version, string description, string downloadUrl, string? devDownloadUrl)
{
this.Version = version;
this.Description = description;
diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs
index 3c63b730..4d37d449 100644
--- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs
+++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Newtonsoft.Json.Schema;
namespace StardewModdingAPI.Web.ViewModels.JsonValidator
@@ -11,30 +9,27 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
** Accessors
*********/
/// <summary>The line number on which the error occurred.</summary>
- public int Line { get; set; }
+ public int Line { get; }
/// <summary>The field path in the JSON file where the error occurred.</summary>
- public string Path { get; set; }
+ public string? Path { get; }
/// <summary>A human-readable description of the error.</summary>
- public string Message { get; set; }
+ public string Message { get; }
/// <summary>The schema error type.</summary>
- public ErrorType SchemaErrorType { get; set; }
+ public ErrorType SchemaErrorType { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public JsonValidatorErrorModel() { }
-
- /// <summary>Construct an instance.</summary>
/// <param name="line">The line number on which the error occurred.</param>
/// <param name="path">The field path in the JSON file where the error occurred.</param>
/// <param name="message">A human-readable description of the error.</param>
/// <param name="schemaErrorType">The schema error type.</param>
- public JsonValidatorErrorModel(int line, string path, string message, ErrorType schemaErrorType)
+ public JsonValidatorErrorModel(int line, string? path, string message, ErrorType schemaErrorType)
{
this.Line = line;
this.Path = path;
diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs
index 2543807f..85c2f44d 100644
--- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs
+++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -13,51 +11,48 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
** Accessors
*********/
/// <summary>Whether to show the edit view.</summary>
- public bool IsEditView { get; set; }
+ public bool IsEditView { get; }
/// <summary>The paste ID.</summary>
- public string PasteID { get; set; }
+ public string? PasteID { get; }
/// <summary>The schema name with which the JSON was validated.</summary>
- public string SchemaName { get; set; }
+ public string? SchemaName { get; }
/// <summary>The supported JSON schemas (names indexed by ID).</summary>
- public readonly IDictionary<string, string> SchemaFormats;
+ public IDictionary<string, string> SchemaFormats { get; }
/// <summary>The validated content.</summary>
- public string Content { get; set; }
+ public string? Content { get; set; }
/// <summary>The schema validation errors, if any.</summary>
public JsonValidatorErrorModel[] Errors { get; set; } = Array.Empty<JsonValidatorErrorModel>();
/// <summary>A non-blocking warning while uploading the file.</summary>
- public string UploadWarning { get; set; }
+ public string? UploadWarning { get; set; }
/// <summary>When the uploaded file will no longer be available.</summary>
- public DateTime? Expiry { get; set; }
+ public DateTimeOffset? Expiry { get; set; }
/// <summary>An error which occurred while uploading the JSON.</summary>
- public string UploadError { get; set; }
+ public string? UploadError { get; set; }
/// <summary>An error which occurred while parsing the JSON.</summary>
- public string ParseError { get; set; }
+ public string? ParseError { get; set; }
/// <summary>A web URL to the user-facing format documentation.</summary>
- public string FormatUrl { get; set; }
+ public string? FormatUrl { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public JsonValidatorModel() { }
-
- /// <summary>Construct an instance.</summary>
/// <param name="pasteID">The stored file ID.</param>
/// <param name="schemaName">The schema name with which the JSON was validated.</param>
/// <param name="schemaFormats">The supported JSON schemas (names indexed by ID).</param>
/// <param name="isEditView">Whether to show the edit view.</param>
- public JsonValidatorModel(string pasteID, string schemaName, IDictionary<string, string> schemaFormats, bool isEditView)
+ public JsonValidatorModel(string? pasteID, string? schemaName, IDictionary<string, string> schemaFormats, bool isEditView)
{
this.PasteID = pasteID;
this.SchemaName = schemaName;
@@ -69,7 +64,7 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
/// <param name="content">The validated content.</param>
/// <param name="expiry">When the uploaded file will no longer be available.</param>
/// <param name="uploadWarning">A non-blocking warning while uploading the log.</param>
- public JsonValidatorModel SetContent(string content, DateTime? expiry, string uploadWarning = null)
+ public JsonValidatorModel SetContent(string content, DateTimeOffset? expiry, string? uploadWarning = null)
{
this.Content = content;
this.Expiry = expiry;
diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs
index 43114d94..3edb58db 100644
--- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs
+++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.ViewModels.JsonValidator
{
/// <summary>The view model for a JSON validation request.</summary>
@@ -9,9 +7,22 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
** Accessors
*********/
/// <summary>The schema name with which to validate the JSON.</summary>
- public string SchemaName { get; set; }
+ public string SchemaName { get; }
/// <summary>The raw content to validate.</summary>
- public string Content { get; set; }
+ public string Content { get; }
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="schemaName">The schema name with which to validate the JSON.</param>
+ /// <param name="content">The raw content to validate.</param>
+ public JsonValidatorRequestModel(string schemaName, string content)
+ {
+ this.SchemaName = schemaName;
+ this.Content = content;
+ }
}
}
diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs
index d7e4d810..c39a9b0a 100644
--- a/src/SMAPI.Web/ViewModels/LogParserModel.cs
+++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs
@@ -44,7 +44,7 @@ namespace StardewModdingAPI.Web.ViewModels
public string? ParseError => this.ParsedLog?.Error;
/// <summary>When the uploaded file will no longer be available.</summary>
- public DateTime? Expiry { get; set; }
+ public DateTimeOffset? Expiry { get; set; }
/// <summary>Whether parsed log data is available.</summary>
[MemberNotNullWhen(true, nameof(LogParserModel.PasteID), nameof(LogParserModel.ParsedLog))]
diff --git a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs
index 2af30cc3..36ea891d 100644
--- a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs
+++ b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs
@@ -1,5 +1,4 @@
-#nullable disable
-
+using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
namespace StardewModdingAPI.Web.ViewModels
@@ -11,22 +10,36 @@ namespace StardewModdingAPI.Web.ViewModels
** Accessors
*********/
/// <summary>The compatibility status, as a string like <c>"Broken"</c>.</summary>
- public string Status { get; set; }
+ public string Status { get; }
/// <summary>The human-readable summary, as an HTML block.</summary>
- public string Summary { get; set; }
+ public string? Summary { get; }
/// <summary>The game or SMAPI version which broke this mod (if applicable).</summary>
- public string BrokeIn { get; set; }
+ public string? BrokeIn { get; }
/// <summary>A link to the unofficial version which fixes compatibility, if any.</summary>
- public ModLinkModel UnofficialVersion { get; set; }
+ public ModLinkModel? UnofficialVersion { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
+ /// <param name="status">The compatibility status, as a string like <c>"Broken"</c>.</param>
+ /// <param name="summary">The human-readable summary, as an HTML block.</param>
+ /// <param name="brokeIn">The game or SMAPI version which broke this mod (if applicable).</param>
+ /// <param name="unofficialVersion">A link to the unofficial version which fixes compatibility, if any.</param>
+ [JsonConstructor]
+ public ModCompatibilityModel(string status, string? summary, string? brokeIn, ModLinkModel? unofficialVersion)
+ {
+ this.Status = status;
+ this.Summary = summary;
+ this.BrokeIn = brokeIn;
+ this.UnofficialVersion = unofficialVersion;
+ }
+
+ /// <summary>Construct an instance.</summary>
/// <param name="info">The mod metadata.</param>
public ModCompatibilityModel(WikiCompatibilityInfo info)
{
@@ -36,7 +49,7 @@ namespace StardewModdingAPI.Web.ViewModels
this.Summary = info.Summary;
this.BrokeIn = info.BrokeIn;
if (info.UnofficialVersion != null)
- this.UnofficialVersion = new ModLinkModel(info.UnofficialUrl, info.UnofficialVersion.ToString());
+ this.UnofficialVersion = new ModLinkModel(info.UnofficialUrl!, info.UnofficialVersion.ToString());
}
}
}
diff --git a/src/SMAPI.Web/ViewModels/ModLinkModel.cs b/src/SMAPI.Web/ViewModels/ModLinkModel.cs
index 3039702e..96f14d48 100644
--- a/src/SMAPI.Web/ViewModels/ModLinkModel.cs
+++ b/src/SMAPI.Web/ViewModels/ModLinkModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Web.ViewModels
{
/// <summary>Metadata about a link.</summary>
@@ -9,10 +7,10 @@ namespace StardewModdingAPI.Web.ViewModels
** Accessors
*********/
/// <summary>The URL of the linked page.</summary>
- public string Url { get; set; }
+ public string Url { get; }
/// <summary>The suggested link text.</summary>
- public string Text { get; set; }
+ public string Text { get; }
/*********
diff --git a/src/SMAPI.Web/ViewModels/ModListModel.cs b/src/SMAPI.Web/ViewModels/ModListModel.cs
index f0cf0c3a..be9f973a 100644
--- a/src/SMAPI.Web/ViewModels/ModListModel.cs
+++ b/src/SMAPI.Web/ViewModels/ModListModel.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -13,37 +11,34 @@ namespace StardewModdingAPI.Web.ViewModels
** Accessors
*********/
/// <summary>The current stable version of the game.</summary>
- public string StableVersion { get; set; }
+ public string? StableVersion { get; }
/// <summary>The current beta version of the game (if any).</summary>
- public string BetaVersion { get; set; }
+ public string? BetaVersion { get; }
/// <summary>The mods to display.</summary>
- public ModModel[] Mods { get; set; }
+ public ModModel[] Mods { get; }
/// <summary>When the data was last updated.</summary>
- public DateTimeOffset LastUpdated { get; set; }
+ public DateTimeOffset LastUpdated { get; }
/// <summary>Whether the data hasn't been updated in a while.</summary>
- public bool IsStale { get; set; }
+ public bool IsStale { get; }
/// <summary>Whether the mod metadata is available.</summary>
- public bool HasData => this.Mods?.Any() == true;
+ public bool HasData => this.Mods.Any();
/*********
** Public methods
*********/
- /// <summary>Construct an empty instance.</summary>
- public ModListModel() { }
-
/// <summary>Construct an instance.</summary>
/// <param name="stableVersion">The current stable version of the game.</param>
/// <param name="betaVersion">The current beta version of the game (if any).</param>
/// <param name="mods">The mods to display.</param>
/// <param name="lastUpdated">When the data was last updated.</param>
/// <param name="isStale">Whether the data hasn't been updated in a while.</param>
- public ModListModel(string stableVersion, string betaVersion, IEnumerable<ModModel> mods, DateTimeOffset lastUpdated, bool isStale)
+ public ModListModel(string? stableVersion, string? betaVersion, IEnumerable<ModModel> mods, DateTimeOffset lastUpdated, bool isStale)
{
this.StableVersion = stableVersion;
this.BetaVersion = betaVersion;
diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs
index d0d7373b..929bf682 100644
--- a/src/SMAPI.Web/ViewModels/ModModel.cs
+++ b/src/SMAPI.Web/ViewModels/ModModel.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
+using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
namespace StardewModdingAPI.Web.ViewModels
@@ -13,43 +12,43 @@ namespace StardewModdingAPI.Web.ViewModels
** Accessors
*********/
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string? Name { get; }
/// <summary>The mod's alternative names, if any.</summary>
- public string AlternateNames { get; set; }
+ public string AlternateNames { get; }
/// <summary>The mod author's name.</summary>
- public string Author { get; set; }
+ public string? Author { get; }
/// <summary>The mod author's alternative names, if any.</summary>
- public string AlternateAuthors { get; set; }
+ public string AlternateAuthors { get; }
/// <summary>The GitHub repo, if any.</summary>
- public string GitHubRepo { get; set; }
+ public string? GitHubRepo { get; }
/// <summary>The URL to the mod's source code, if any.</summary>
- public string SourceUrl { get; set; }
+ public string? SourceUrl { get; }
/// <summary>The compatibility status for the stable version of the game.</summary>
- public ModCompatibilityModel Compatibility { get; set; }
+ public ModCompatibilityModel Compatibility { get; }
/// <summary>The compatibility status for the beta version of the game.</summary>
- public ModCompatibilityModel BetaCompatibility { get; set; }
+ public ModCompatibilityModel? BetaCompatibility { get; }
/// <summary>Links to the available mod pages.</summary>
- public ModLinkModel[] ModPages { get; set; }
+ public ModLinkModel[] ModPages { get; }
/// <summary>The human-readable warnings for players about this mod.</summary>
- public string[] Warnings { get; set; }
+ public string[] Warnings { get; }
/// <summary>The URL of the pull request which submits changes for an unofficial update to the author, if any.</summary>
- public string PullRequestUrl { get; set; }
+ public string? PullRequestUrl { get; }
- /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary>
- public string DevNote { get; set; }
+ /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests.</summary>
+ public string? DevNote { get; }
/// <summary>A unique identifier for the mod that can be used in an anchor URL.</summary>
- public string Slug { get; set; }
+ public string? Slug { get; }
/// <summary>The sites where the mod can be downloaded.</summary>
public string[] ModPageSites => this.ModPages.Select(p => p.Text).ToArray();
@@ -59,6 +58,38 @@ namespace StardewModdingAPI.Web.ViewModels
** Public methods
*********/
/// <summary>Construct an instance.</summary>
+ /// <param name="name">The mod name.</param>
+ /// <param name="alternateNames">The mod's alternative names, if any.</param>
+ /// <param name="author">The mod author's name.</param>
+ /// <param name="alternateAuthors">The mod author's alternative names, if any.</param>
+ /// <param name="gitHubRepo">The GitHub repo, if any.</param>
+ /// <param name="sourceUrl">The URL to the mod's source code, if any.</param>
+ /// <param name="compatibility">The compatibility status for the stable version of the game.</param>
+ /// <param name="betaCompatibility">The compatibility status for the beta version of the game.</param>
+ /// <param name="modPages">Links to the available mod pages.</param>
+ /// <param name="warnings">The human-readable warnings for players about this mod.</param>
+ /// <param name="pullRequestUrl">The URL of the pull request which submits changes for an unofficial update to the author, if any.</param>
+ /// <param name="devNote">Special notes intended for developers who maintain unofficial updates or submit pull requests.</param>
+ /// <param name="slug">A unique identifier for the mod that can be used in an anchor URL.</param>
+ [JsonConstructor]
+ public ModModel(string? name, string alternateNames, string author, string alternateAuthors, string gitHubRepo, string sourceUrl, ModCompatibilityModel compatibility, ModCompatibilityModel betaCompatibility, ModLinkModel[] modPages, string[] warnings, string pullRequestUrl, string devNote, string slug)
+ {
+ this.Name = name;
+ this.AlternateNames = alternateNames;
+ this.Author = author;
+ this.AlternateAuthors = alternateAuthors;
+ this.GitHubRepo = gitHubRepo;
+ this.SourceUrl = sourceUrl;
+ this.Compatibility = compatibility;
+ this.BetaCompatibility = betaCompatibility;
+ this.ModPages = modPages;
+ this.Warnings = warnings;
+ this.PullRequestUrl = pullRequestUrl;
+ this.DevNote = devNote;
+ this.Slug = slug;
+ }
+
+ /// <summary>Construct an instance.</summary>
/// <param name="entry">The mod metadata.</param>
public ModModel(WikiModEntry entry)
{
@@ -84,7 +115,7 @@ namespace StardewModdingAPI.Web.ViewModels
*********/
/// <summary>Get the web URL for the mod's source code repository, if any.</summary>
/// <param name="entry">The mod metadata.</param>
- private string GetSourceUrl(WikiModEntry entry)
+ private string? GetSourceUrl(WikiModEntry entry)
{
if (!string.IsNullOrWhiteSpace(entry.GitHubRepo))
return $"https://github.com/{entry.GitHubRepo}";
diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml
index 9841ca42..acb8df78 100644
--- a/src/SMAPI.Web/Views/Index/Index.cshtml
+++ b/src/SMAPI.Web/Views/Index/Index.cshtml
@@ -1,7 +1,3 @@
-@{
- #nullable disable
-}
-
@using Microsoft.Extensions.Options
@using StardewModdingAPI.Web.Framework
@using StardewModdingAPI.Web.Framework.ConfigModels
@@ -28,7 +24,7 @@
<div id="call-to-action">
<div class="cta-dropdown">
- <a href="@Model.StableVersion.DownloadUrl" class="main-cta download">Download SMAPI @Model.StableVersion.Version</a><br />
+ <a href="@Model!.StableVersion.DownloadUrl" class="main-cta download">Download SMAPI @Model.StableVersion.Version</a><br />
<div class="dropdown-content">
<a href="https://www.nexusmods.com/stardewvalley/mods/2400"><img src="Content/images/nexus-icon.png" /> Download from Nexus</a>
<a href="@Model.StableVersion.DownloadUrl"><img src="Content/images/direct-download-icon.png" /> Direct download</a>
diff --git a/src/SMAPI.Web/Views/Index/Privacy.cshtml b/src/SMAPI.Web/Views/Index/Privacy.cshtml
index 1dc327d7..fd78f908 100644
--- a/src/SMAPI.Web/Views/Index/Privacy.cshtml
+++ b/src/SMAPI.Web/Views/Index/Privacy.cshtml
@@ -1,7 +1,3 @@
-@{
- #nullable disable
-}
-
@using Microsoft.Extensions.Options
@using StardewModdingAPI.Web.Framework
@using StardewModdingAPI.Web.Framework.ConfigModels
diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml
index 5e38e4dc..f5ec0f7a 100644
--- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml
+++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml
@@ -1,7 +1,3 @@
-@{
- #nullable disable
-}
-
@using Humanizer
@using StardewModdingAPI.Web.Framework
@using StardewModdingAPI.Web.ViewModels.JsonValidator
@@ -9,10 +5,10 @@
@{
// get view data
- string curPageUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName, id = Model.PasteID }, absoluteUrl: true);
- string newUploadUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName });
- string schemaDisplayName = null;
- bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName?.ToLower() != "none";
+ string curPageUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model!.SchemaName, id = Model.PasteID }, absoluteUrl: true)!;
+ string newUploadUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName })!;
+ string? schemaDisplayName = null;
+ bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName.ToLower() != "none";
// build title
ViewData["Title"] = "JSON validator";
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index dbaa14e0..dfb603f2 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -23,7 +23,7 @@
.GetValues<LogSection>()
.ToDictionary(section => (int)section, section => section.ToString());
- string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true);
+ string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true)!;
ISet<int> screenIds = new HashSet<int>(log?.Messages.Select(p => p.ScreenId) ?? Array.Empty<int>());
}
diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml
index 4b6400ad..78eabad7 100644
--- a/src/SMAPI.Web/Views/Mods/Index.cshtml
+++ b/src/SMAPI.Web/Views/Mods/Index.cshtml
@@ -1,7 +1,3 @@
-@{
- #nullable disable
-}
-
@using Humanizer
@using Humanizer.Localisation
@using StardewModdingAPI.Web.Framework
@@ -10,7 +6,7 @@
@{
ViewData["Title"] = "Mod compatibility";
- TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated;
+ TimeSpan staleAge = DateTimeOffset.UtcNow - Model!.LastUpdated;
bool hasBeta = Model.BetaVersion != null;
string betaLabel = $"SDV {Model.BetaVersion} only";
diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml
index 248cc7ef..1e82ab5f 100644
--- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml
+++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml
@@ -1,7 +1,3 @@
-@{
- #nullable disable
-}
-
@using Microsoft.Extensions.Options
@using StardewModdingAPI.Web.Framework
@using StardewModdingAPI.Web.Framework.ConfigModels
diff --git a/src/SMAPI.Web/Views/_ViewStart.cshtml b/src/SMAPI.Web/Views/_ViewStart.cshtml
index 0dbac246..820a2f6e 100644
--- a/src/SMAPI.Web/Views/_ViewStart.cshtml
+++ b/src/SMAPI.Web/Views/_ViewStart.cshtml
@@ -1,7 +1,3 @@
@{
- #nullable disable
-}
-
-@{
Layout = "_Layout";
}
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index 0265a928..1231f824 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -17,7 +17,8 @@
"Site": {
"BetaEnabled": false,
- "OtherBlurb": null
+ "OtherBlurb": null,
+ "SupporterList": null
},
"ApiClients": {
diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings
index 5cb13525..ad546665 100644
--- a/src/SMAPI.sln.DotSettings
+++ b/src/SMAPI.sln.DotSettings
@@ -25,8 +25,12 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=analytics/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=backports/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=bigcraftable/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=bigcraftables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Chucklefish/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=clickable/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=Comparers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=craftable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=craftables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=crossplatform/@EntryIndexedValue">True</s:Boolean>
@@ -35,6 +39,8 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=debounced/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=decoratable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=devs/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=disambiguator/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=equippable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=fallbacks/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=filenames/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gamepad/@EntryIndexedValue">True</s:Boolean>
@@ -44,6 +50,8 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Junimo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Keybind/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=keybinds/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=Lidgren/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=minigames/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=modder/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=modders/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mongo/@EntryIndexedValue">True</s:Boolean>
@@ -56,8 +64,11 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=premultiplied/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=premultiply/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Prenormalize/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=Preprocesses/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=prerelease/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pufferchick/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=rasterizer/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=reimplements/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=rewriter/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=rewriters/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=SMAPI/@EntryIndexedValue">True</s:Boolean>
@@ -66,15 +77,20 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=spritesheet/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=stackable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Stardew/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=subchain/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=subdomain/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=subkey/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=synchronised/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=tbin/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=textbox/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=thumbstick/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tilesheet/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tilesheets/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tilesheet_0027s/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=transpiler/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unloadable/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=unlocalized/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=versioning/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=virally/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=Xbox/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary> \ No newline at end of file
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 2d9ab666..d40b97f4 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -33,7 +31,7 @@ namespace StardewModdingAPI
** Accessors
*********/
/// <summary>The path to the game folder.</summary>
- public static string GamePath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ public static string GamePath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
/// <summary>The absolute path to the folder containing SMAPI's internal files.</summary>
public static readonly string InternalFilesPath = Path.Combine(EarlyConstants.GamePath, "smapi-internal");
@@ -69,8 +67,8 @@ namespace StardewModdingAPI
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.6");
- /// <summary>The maximum supported version of Stardew Valley.</summary>
- public static ISemanticVersion MaximumGameVersion { get; } = null;
+ /// <summary>The maximum supported version of Stardew Valley, if any.</summary>
+ public static ISemanticVersion? MaximumGameVersion { get; } = null;
/// <summary>The target game platform.</summary>
public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform;
@@ -85,7 +83,7 @@ namespace StardewModdingAPI
get
{
SCore.DeprecationManager.Warn(
- source: SCore.DeprecationManager.GetSourceNameFromStack(),
+ source: SCore.DeprecationManager.GetModFromStack(),
nounPhrase: $"{nameof(Constants)}.{nameof(Constants.ExecutionPath)}",
version: "3.14.0",
severity: DeprecationLevel.Notice
@@ -111,10 +109,10 @@ namespace StardewModdingAPI
public static string SavesPath { get; } = Path.Combine(Constants.DataPath, "Saves");
/// <summary>The name of the current save folder (if save info is available, regardless of whether the save file exists yet).</summary>
- public static string SaveFolderName => Constants.GetSaveFolderName();
+ public static string? SaveFolderName => Constants.GetSaveFolderName();
/// <summary>The absolute path to the current save folder (if save info is available and the save file exists).</summary>
- public static string CurrentSavePath => Constants.GetSaveFolderPathIfExists();
+ public static string? CurrentSavePath => Constants.GetSaveFolderPathIfExists();
/****
** Internal
@@ -164,7 +162,7 @@ namespace StardewModdingAPI
internal static string DefaultModsPath { get; } = Path.Combine(Constants.GamePath, "Mods");
/// <summary>The actual full path to search for mods.</summary>
- internal static string ModsPath { get; set; }
+ internal static string ModsPath { get; set; } = null!; // initialized early during SMAPI startup
/// <summary>The game's current semantic version.</summary>
internal static ISemanticVersion GameVersion { get; } = new GameVersion(Game1.version);
@@ -179,7 +177,7 @@ namespace StardewModdingAPI
/// <summary>Get the SMAPI version to recommend for an older game version, if any.</summary>
/// <param name="version">The game version to search.</param>
/// <returns>Returns the compatible SMAPI version, or <c>null</c> if none was found.</returns>
- internal static ISemanticVersion GetCompatibleApiVersion(ISemanticVersion version)
+ internal static ISemanticVersion? GetCompatibleApiVersion(ISemanticVersion version)
{
// This covers all officially supported public game updates. It might seem like version
// ranges would be better, but the given SMAPI versions may not be compatible with
@@ -337,22 +335,22 @@ namespace StardewModdingAPI
}
/// <summary>Get the name of the save folder, if any.</summary>
- private static string GetSaveFolderName()
+ private static string? GetSaveFolderName()
{
return Constants.GetSaveFolder()?.Name;
}
/// <summary>Get the absolute path to the current save folder, if any.</summary>
- private static string GetSaveFolderPathIfExists()
+ private static string? GetSaveFolderPathIfExists()
{
- DirectoryInfo saveFolder = Constants.GetSaveFolder();
+ DirectoryInfo? saveFolder = Constants.GetSaveFolder();
return saveFolder?.Exists == true
? saveFolder.FullName
: null;
}
/// <summary>Get the current save folder, if any.</summary>
- private static DirectoryInfo GetSaveFolder()
+ private static DirectoryInfo? GetSaveFolder()
{
// save not available
if (Context.LoadStage == LoadStage.None)
@@ -365,7 +363,7 @@ namespace StardewModdingAPI
: Game1.uniqueIDForThisGame;
// get best match (accounting for rare case where folder name isn't sanitized)
- DirectoryInfo folder = null;
+ DirectoryInfo? folder = null;
foreach (string saveName in new[] { rawSaveName, new string(rawSaveName.Where(char.IsLetterOrDigit).ToArray()) })
{
try
diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs
index e906375b..aa4ecf35 100644
--- a/src/SMAPI/Context.cs
+++ b/src/SMAPI/Context.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/Events/AssetReadyEventArgs.cs b/src/SMAPI/Events/AssetReadyEventArgs.cs
index 19e5a9df..2c308f18 100644
--- a/src/SMAPI/Events/AssetReadyEventArgs.cs
+++ b/src/SMAPI/Events/AssetReadyEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs
index 3c51c95d..3bcf83b9 100644
--- a/src/SMAPI/Events/AssetRequestedEventArgs.cs
+++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Graphics;
@@ -19,7 +17,7 @@ namespace StardewModdingAPI.Events
private readonly IModMetadata Mod;
/// <summary>Get the mod metadata for a content pack, if it's a valid content pack for the mod.</summary>
- private readonly Func<IModMetadata, string, string, IModMetadata> GetOnBehalfOf;
+ private readonly Func<IModMetadata, string?, string, IModMetadata?> GetOnBehalfOf;
/*********
@@ -51,7 +49,7 @@ namespace StardewModdingAPI.Events
/// <param name="dataType">The requested data type.</param>
/// <param name="nameWithoutLocale">The <paramref name="name"/> with any locale codes stripped.</param>
/// <param name="getOnBehalfOf">Get the mod metadata for a content pack, if it's a valid content pack for the mod.</param>
- internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name, IAssetName nameWithoutLocale, Type dataType, Func<IModMetadata, string, string, IModMetadata> getOnBehalfOf)
+ internal AssetRequestedEventArgs(IModMetadata mod, IAssetName name, IAssetName nameWithoutLocale, Type dataType, Func<IModMetadata, string?, string, IModMetadata?> getOnBehalfOf)
{
this.Mod = mod;
this.Name = name;
@@ -71,7 +69,7 @@ namespace StardewModdingAPI.Events
/// <item>Each asset can logically only have one initial instance. If multiple loads apply at the same time, SMAPI will use the <paramref name="priority"/> parameter to decide what happens. If you're making changes to the existing asset instead of replacing it, you should use <see cref="Edit"/> instead to avoid those limitations and improve mod compatibility.</item>
/// </list>
/// </remarks>
- public void LoadFrom(Func<object> load, AssetLoadPriority priority, string onBehalfOf = null)
+ public void LoadFrom(Func<object> load, AssetLoadPriority priority, string? onBehalfOf = null)
{
this.LoadOperations.Add(
new AssetLoadOperation(
@@ -95,13 +93,15 @@ namespace StardewModdingAPI.Events
/// </list>
/// </remarks>
public void LoadFromModFile<TAsset>(string relativePath, AssetLoadPriority priority)
+ where TAsset : notnull
{
this.LoadOperations.Add(
new AssetLoadOperation(
mod: this.Mod,
priority: priority,
onBehalfOf: null,
- _ => this.Mod.Mod.Helper.ModContent.Load<TAsset>(relativePath))
+ _ => this.Mod.Mod!.Helper.ModContent.Load<TAsset>(relativePath)
+ )
);
}
@@ -116,7 +116,7 @@ namespace StardewModdingAPI.Events
/// <item>You can apply any number of edits to the asset. Each edit will be applied on top of the previous one (i.e. it'll see the merged asset from all previous edits as its input).</item>
/// </list>
/// </remarks>
- public void Edit(Action<IAssetData> apply, AssetEditPriority priority = AssetEditPriority.Default, string onBehalfOf = null)
+ public void Edit(Action<IAssetData> apply, AssetEditPriority priority = AssetEditPriority.Default, string? onBehalfOf = null)
{
this.EditOperations.Add(
new AssetEditOperation(
diff --git a/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs b/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs
index bd0df598..614cdf49 100644
--- a/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs
+++ b/src/SMAPI/Events/AssetsInvalidatedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
diff --git a/src/SMAPI/Events/BuildingListChangedEventArgs.cs b/src/SMAPI/Events/BuildingListChangedEventArgs.cs
index ba9574cc..74f37710 100644
--- a/src/SMAPI/Events/BuildingListChangedEventArgs.cs
+++ b/src/SMAPI/Events/BuildingListChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI/Events/ButtonPressedEventArgs.cs b/src/SMAPI/Events/ButtonPressedEventArgs.cs
index 94684513..1b30fd23 100644
--- a/src/SMAPI/Events/ButtonPressedEventArgs.cs
+++ b/src/SMAPI/Events/ButtonPressedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Framework.Input;
diff --git a/src/SMAPI/Events/ButtonReleasedEventArgs.cs b/src/SMAPI/Events/ButtonReleasedEventArgs.cs
index 6ff3727d..40ec1cc1 100644
--- a/src/SMAPI/Events/ButtonReleasedEventArgs.cs
+++ b/src/SMAPI/Events/ButtonReleasedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Framework.Input;
diff --git a/src/SMAPI/Events/ButtonsChangedEventArgs.cs b/src/SMAPI/Events/ButtonsChangedEventArgs.cs
index c63d34e6..a5e87735 100644
--- a/src/SMAPI/Events/ButtonsChangedEventArgs.cs
+++ b/src/SMAPI/Events/ButtonsChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI/Events/ChangeType.cs b/src/SMAPI/Events/ChangeType.cs
deleted file mode 100644
index 0fc717df..00000000
--- a/src/SMAPI/Events/ChangeType.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace StardewModdingAPI.Events
-{
- /// <summary>Indicates how an inventory item changed.</summary>
- public enum ChangeType
- {
- /// <summary>The entire stack was removed.</summary>
- Removed,
-
- /// <summary>The entire stack was added.</summary>
- Added,
-
- /// <summary>The stack size changed.</summary>
- StackChange
- }
-}
diff --git a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs
index bc8ac0c0..4b4c4210 100644
--- a/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs
+++ b/src/SMAPI/Events/ChestInventoryChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using StardewValley;
diff --git a/src/SMAPI/Events/CursorMovedEventArgs.cs b/src/SMAPI/Events/CursorMovedEventArgs.cs
index f3e7513b..43ff90ce 100644
--- a/src/SMAPI/Events/CursorMovedEventArgs.cs
+++ b/src/SMAPI/Events/CursorMovedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/DebrisListChangedEventArgs.cs b/src/SMAPI/Events/DebrisListChangedEventArgs.cs
index 56b1f30a..61b7590a 100644
--- a/src/SMAPI/Events/DebrisListChangedEventArgs.cs
+++ b/src/SMAPI/Events/DebrisListChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI/Events/FurnitureListChangedEventArgs.cs b/src/SMAPI/Events/FurnitureListChangedEventArgs.cs
index cda1b6cc..683f4620 100644
--- a/src/SMAPI/Events/FurnitureListChangedEventArgs.cs
+++ b/src/SMAPI/Events/FurnitureListChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI/Events/IContentEvents.cs b/src/SMAPI/Events/IContentEvents.cs
index 109f9753..d537db70 100644
--- a/src/SMAPI/Events/IContentEvents.cs
+++ b/src/SMAPI/Events/IContentEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/IDisplayEvents.cs b/src/SMAPI/Events/IDisplayEvents.cs
index b8b89120..dbf8d90f 100644
--- a/src/SMAPI/Events/IDisplayEvents.cs
+++ b/src/SMAPI/Events/IDisplayEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewValley;
diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs
index 52bac3f8..6855737b 100644
--- a/src/SMAPI/Events/IGameLoopEvents.cs
+++ b/src/SMAPI/Events/IGameLoopEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/IInputEvents.cs b/src/SMAPI/Events/IInputEvents.cs
index 01ceb224..081c40c0 100644
--- a/src/SMAPI/Events/IInputEvents.cs
+++ b/src/SMAPI/Events/IInputEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs
index a1aacbce..2603961b 100644
--- a/src/SMAPI/Events/IModEvents.cs
+++ b/src/SMAPI/Events/IModEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI.Events
{
/// <summary>Manages access to events raised by SMAPI.</summary>
diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs
index c50eaf04..af9b5f17 100644
--- a/src/SMAPI/Events/IMultiplayerEvents.cs
+++ b/src/SMAPI/Events/IMultiplayerEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/IPlayerEvents.cs b/src/SMAPI/Events/IPlayerEvents.cs
index 9d18bfad..81e17b1a 100644
--- a/src/SMAPI/Events/IPlayerEvents.cs
+++ b/src/SMAPI/Events/IPlayerEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/ISpecialisedEvents.cs b/src/SMAPI/Events/ISpecialisedEvents.cs
index 0ec5bf54..bf70956d 100644
--- a/src/SMAPI/Events/ISpecialisedEvents.cs
+++ b/src/SMAPI/Events/ISpecialisedEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/IWorldEvents.cs b/src/SMAPI/Events/IWorldEvents.cs
index 785dfa8f..c023e1f0 100644
--- a/src/SMAPI/Events/IWorldEvents.cs
+++ b/src/SMAPI/Events/IWorldEvents.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/InventoryChangedEventArgs.cs b/src/SMAPI/Events/InventoryChangedEventArgs.cs
index 58c0ff8f..40cd4128 100644
--- a/src/SMAPI/Events/InventoryChangedEventArgs.cs
+++ b/src/SMAPI/Events/InventoryChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using StardewValley;
diff --git a/src/SMAPI/Events/ItemStackSizeChange.cs b/src/SMAPI/Events/ItemStackSizeChange.cs
index 5d0986aa..35369be2 100644
--- a/src/SMAPI/Events/ItemStackSizeChange.cs
+++ b/src/SMAPI/Events/ItemStackSizeChange.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using StardewValley;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs
index aedb0e46..59d79f0f 100644
--- a/src/SMAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs
+++ b/src/SMAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI/Events/LevelChangedEventArgs.cs b/src/SMAPI/Events/LevelChangedEventArgs.cs
index 3beb9fd5..c7303603 100644
--- a/src/SMAPI/Events/LevelChangedEventArgs.cs
+++ b/src/SMAPI/Events/LevelChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Enums;
using StardewValley;
diff --git a/src/SMAPI/Events/LocaleChangedEventArgs.cs b/src/SMAPI/Events/LocaleChangedEventArgs.cs
index 015e7ec8..09d3f6e5 100644
--- a/src/SMAPI/Events/LocaleChangedEventArgs.cs
+++ b/src/SMAPI/Events/LocaleChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode;
diff --git a/src/SMAPI/Events/LocationListChangedEventArgs.cs b/src/SMAPI/Events/LocationListChangedEventArgs.cs
index 055463dd..1ebb3e2d 100644
--- a/src/SMAPI/Events/LocationListChangedEventArgs.cs
+++ b/src/SMAPI/Events/LocationListChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI/Events/MenuChangedEventArgs.cs b/src/SMAPI/Events/MenuChangedEventArgs.cs
index 362accec..c37fd216 100644
--- a/src/SMAPI/Events/MenuChangedEventArgs.cs
+++ b/src/SMAPI/Events/MenuChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewValley.Menus;
@@ -11,20 +9,20 @@ namespace StardewModdingAPI.Events
/*********
** Accessors
*********/
- /// <summary>The previous menu.</summary>
- public IClickableMenu OldMenu { get; }
+ /// <summary>The previous menu, if any.</summary>
+ public IClickableMenu? OldMenu { get; }
- /// <summary>The current menu.</summary>
- public IClickableMenu NewMenu { get; }
+ /// <summary>The current menu, if any.</summary>
+ public IClickableMenu? NewMenu { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="oldMenu">The previous menu.</param>
- /// <param name="newMenu">The current menu.</param>
- internal MenuChangedEventArgs(IClickableMenu oldMenu, IClickableMenu newMenu)
+ /// <param name="oldMenu">The previous menu, if any.</param>
+ /// <param name="newMenu">The current menu, if any.</param>
+ internal MenuChangedEventArgs(IClickableMenu? oldMenu, IClickableMenu? newMenu)
{
this.OldMenu = oldMenu;
this.NewMenu = newMenu;
diff --git a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs
index 671bdf38..84a27d18 100644
--- a/src/SMAPI/Events/ModMessageReceivedEventArgs.cs
+++ b/src/SMAPI/Events/ModMessageReceivedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Framework.Networking;
using StardewModdingAPI.Toolkit.Serialization;
@@ -47,8 +45,10 @@ namespace StardewModdingAPI.Events
/// <summary>Read the message data into the given model type.</summary>
/// <typeparam name="TModel">The message model type.</typeparam>
public TModel ReadAs<TModel>()
+ where TModel : notnull
{
- return this.Message.Data.ToObject<TModel>(this.JsonHelper.GetSerializer());
+ return this.Message.Data.ToObject<TModel>(this.JsonHelper.GetSerializer())
+ ?? throw new InvalidOperationException($"Can't read empty mod message data as a {typeof(TModel).FullName} value.");
}
}
}
diff --git a/src/SMAPI/Events/NpcListChangedEventArgs.cs b/src/SMAPI/Events/NpcListChangedEventArgs.cs
index fb6dc1c5..3a37f1e7 100644
--- a/src/SMAPI/Events/NpcListChangedEventArgs.cs
+++ b/src/SMAPI/Events/NpcListChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI/Events/ObjectListChangedEventArgs.cs b/src/SMAPI/Events/ObjectListChangedEventArgs.cs
index b1a636aa..b21d2867 100644
--- a/src/SMAPI/Events/ObjectListChangedEventArgs.cs
+++ b/src/SMAPI/Events/ObjectListChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI/Events/PeerConnectedEventArgs.cs b/src/SMAPI/Events/PeerConnectedEventArgs.cs
index 3d11a3b5..bfaa2bd3 100644
--- a/src/SMAPI/Events/PeerConnectedEventArgs.cs
+++ b/src/SMAPI/Events/PeerConnectedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/PeerContextReceivedEventArgs.cs b/src/SMAPI/Events/PeerContextReceivedEventArgs.cs
index 35a4b20d..151a295c 100644
--- a/src/SMAPI/Events/PeerContextReceivedEventArgs.cs
+++ b/src/SMAPI/Events/PeerContextReceivedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/PeerDisconnectedEventArgs.cs b/src/SMAPI/Events/PeerDisconnectedEventArgs.cs
index 0675b8fe..8517988a 100644
--- a/src/SMAPI/Events/PeerDisconnectedEventArgs.cs
+++ b/src/SMAPI/Events/PeerDisconnectedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI.Events
diff --git a/src/SMAPI/Events/RenderedActiveMenuEventArgs.cs b/src/SMAPI/Events/RenderedActiveMenuEventArgs.cs
index 3da0b4b4..efd4163b 100644
--- a/src/SMAPI/Events/RenderedActiveMenuEventArgs.cs
+++ b/src/SMAPI/Events/RenderedActiveMenuEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
diff --git a/src/SMAPI/Events/RenderedEventArgs.cs b/src/SMAPI/Events/RenderedEventArgs.cs
index e8beaaac..d6341b19 100644
--- a/src/SMAPI/Events/RenderedEventArgs.cs
+++ b/src/SMAPI/Events/RenderedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
diff --git a/src/SMAPI/Events/RenderedHudEventArgs.cs b/src/SMAPI/Events/RenderedHudEventArgs.cs
index b25ecd4c..46e89013 100644
--- a/src/SMAPI/Events/RenderedHudEventArgs.cs
+++ b/src/SMAPI/Events/RenderedHudEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
diff --git a/src/SMAPI/Events/RenderedWorldEventArgs.cs b/src/SMAPI/Events/RenderedWorldEventArgs.cs
index a99d6ab3..56145381 100644
--- a/src/SMAPI/Events/RenderedWorldEventArgs.cs
+++ b/src/SMAPI/Events/RenderedWorldEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
diff --git a/src/SMAPI/Events/RenderingActiveMenuEventArgs.cs b/src/SMAPI/Events/RenderingActiveMenuEventArgs.cs
index 3e3f3258..103f56df 100644
--- a/src/SMAPI/Events/RenderingActiveMenuEventArgs.cs
+++ b/src/SMAPI/Events/RenderingActiveMenuEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
diff --git a/src/SMAPI/Events/RenderingEventArgs.cs b/src/SMAPI/Events/RenderingEventArgs.cs
index 8f6b3557..5acbef09 100644
--- a/src/SMAPI/Events/RenderingEventArgs.cs
+++ b/src/SMAPI/Events/RenderingEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
diff --git a/src/SMAPI/Events/RenderingHudEventArgs.cs b/src/SMAPI/Events/RenderingHudEventArgs.cs
index 87269b90..84c96ecd 100644
--- a/src/SMAPI/Events/RenderingHudEventArgs.cs
+++ b/src/SMAPI/Events/RenderingHudEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
diff --git a/src/SMAPI/Events/RenderingWorldEventArgs.cs b/src/SMAPI/Events/RenderingWorldEventArgs.cs
index 2fc9964f..d0d44789 100644
--- a/src/SMAPI/Events/RenderingWorldEventArgs.cs
+++ b/src/SMAPI/Events/RenderingWorldEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
diff --git a/src/SMAPI/Events/TerrainFeatureListChangedEventArgs.cs b/src/SMAPI/Events/TerrainFeatureListChangedEventArgs.cs
index 77a73102..cdf1e6dc 100644
--- a/src/SMAPI/Events/TerrainFeatureListChangedEventArgs.cs
+++ b/src/SMAPI/Events/TerrainFeatureListChangedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/src/SMAPI/Events/WarpedEventArgs.cs b/src/SMAPI/Events/WarpedEventArgs.cs
index 92a8ea77..9afe4a4e 100644
--- a/src/SMAPI/Events/WarpedEventArgs.cs
+++ b/src/SMAPI/Events/WarpedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewValley;
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;
diff --git a/src/SMAPI/GamePlatform.cs b/src/SMAPI/GamePlatform.cs
index 8013faa9..cce5ed8d 100644
--- a/src/SMAPI/GamePlatform.cs
+++ b/src/SMAPI/GamePlatform.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI
diff --git a/src/SMAPI/IAssetData.cs b/src/SMAPI/IAssetData.cs
index f07340e4..1ec09d87 100644
--- a/src/SMAPI/IAssetData.cs
+++ b/src/SMAPI/IAssetData.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI
@@ -7,6 +5,7 @@ namespace StardewModdingAPI
/// <summary>Generic metadata and methods for a content asset being loaded.</summary>
/// <typeparam name="TValue">The expected data type.</typeparam>
public interface IAssetData<TValue> : IAssetInfo
+ where TValue : notnull
{
/*********
** Accessors
diff --git a/src/SMAPI/IAssetDataForDictionary.cs b/src/SMAPI/IAssetDataForDictionary.cs
index 82ba25cb..1136316f 100644
--- a/src/SMAPI/IAssetDataForDictionary.cs
+++ b/src/SMAPI/IAssetDataForDictionary.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
namespace StardewModdingAPI
diff --git a/src/SMAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs
index 388caa68..6f8a4719 100644
--- a/src/SMAPI/IAssetDataForImage.cs
+++ b/src/SMAPI/IAssetDataForImage.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
@@ -23,8 +21,8 @@ namespace StardewModdingAPI
void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace);
/// <summary>Extend the image if needed to fit the given size. Note that this is an expensive operation, creates a new texture instance, and that extending a spritesheet horizontally may cause game errors or bugs.</summary>
- /// <param name="minWidth">The minimum texture width.</param>
- /// <param name="minHeight">The minimum texture height.</param>
+ /// <param name="minWidth">The minimum texture width in pixels.</param>
+ /// <param name="minHeight">The minimum texture height in pixels.</param>
/// <returns>Whether the texture was resized.</returns>
bool ExtendImage(int minWidth, int minHeight);
}
diff --git a/src/SMAPI/IAssetDataForMap.cs b/src/SMAPI/IAssetDataForMap.cs
index 89ee28f2..19bdf3b2 100644
--- a/src/SMAPI/IAssetDataForMap.cs
+++ b/src/SMAPI/IAssetDataForMap.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using Microsoft.Xna.Framework;
using xTile;
@@ -17,5 +15,12 @@ namespace StardewModdingAPI
/// <param name="targetArea">The tile area within the target map to overwrite, or <c>null</c> to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map.</param>
/// <param name="patchMode">Indicates how the map should be patched.</param>
void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMapMode patchMode = PatchMapMode.Overlay);
+
+ /// <summary>Extend the map if needed to fit the given size. Note that this is an expensive operation and resizes the map in-place.</summary>
+ /// <param name="map">The map to resize.</param>
+ /// <param name="minWidth">The minimum map width in tiles.</param>
+ /// <param name="minHeight">The minimum map height in tiles.</param>
+ /// <returns>Whether the map was resized.</returns>
+ bool ExtendMap(Map map, int minWidth, int minHeight);
}
}
diff --git a/src/SMAPI/IAssetEditor.cs b/src/SMAPI/IAssetEditor.cs
index f3d91bd0..9f22ed83 100644
--- a/src/SMAPI/IAssetEditor.cs
+++ b/src/SMAPI/IAssetEditor.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs
index 5b4ac479..4d651a72 100644
--- a/src/SMAPI/IAssetInfo.cs
+++ b/src/SMAPI/IAssetInfo.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI
@@ -11,7 +9,7 @@ namespace StardewModdingAPI
** Accessors
*********/
/// <summary>The content's locale code, if the content is localized.</summary>
- string Locale { get; }
+ string? Locale { get; }
/// <summary>The asset name being read.</summary>
/// <remarks>NOTE: when reading this field from an <see cref="IAssetLoader"/> or <see cref="IAssetEditor"/> implementation, it's always equivalent to <see cref="NameWithoutLocale"/> for backwards compatibility.</remarks>
diff --git a/src/SMAPI/IAssetLoader.cs b/src/SMAPI/IAssetLoader.cs
index 0d52a481..96b98793 100644
--- a/src/SMAPI/IAssetLoader.cs
+++ b/src/SMAPI/IAssetLoader.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/IAssetName.cs b/src/SMAPI/IAssetName.cs
index 22f5c6b7..71b6ed6b 100644
--- a/src/SMAPI/IAssetName.cs
+++ b/src/SMAPI/IAssetName.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewValley;
@@ -18,7 +16,7 @@ namespace StardewModdingAPI
string BaseName { get; }
/// <summary>The locale code specified in the <see cref="Name"/>, if it's a valid code recognized by the game content.</summary>
- string LocaleCode { get; }
+ string? LocaleCode { get; }
/// <summary>The language code matching the <see cref="LocaleCode"/>, if applicable.</summary>
LocalizedContentManager.LanguageCode? LanguageCode { get; }
@@ -30,23 +28,23 @@ namespace StardewModdingAPI
/// <summary>Get whether the given asset name is equivalent, ignoring capitalization and formatting.</summary>
/// <param name="assetName">The asset name to compare this instance to.</param>
/// <param name="useBaseName">Whether to compare the given name with the <see cref="BaseName"/> (if true) or <see cref="Name"/> (if false). This has no effect on any locale included in the given <paramref name="assetName"/>.</param>
- bool IsEquivalentTo(string assetName, bool useBaseName = false);
+ bool IsEquivalentTo(string? assetName, bool useBaseName = false);
/// <summary>Get whether the given asset name is equivalent, ignoring capitalization and formatting.</summary>
/// <param name="assetName">The asset name to compare this instance to.</param>
/// <param name="useBaseName">Whether to compare the given name with the <see cref="BaseName"/> (if true) or <see cref="Name"/> (if false).</param>
- bool IsEquivalentTo(IAssetName assetName, bool useBaseName = false);
+ bool IsEquivalentTo(IAssetName? assetName, bool useBaseName = false);
/// <summary>Get whether the asset name starts with the given value, ignoring capitalization and formatting. This can be used with a trailing slash to test for an asset folder, like <c>Data/</c>.</summary>
/// <param name="prefix">The prefix to match.</param>
/// <param name="allowPartialWord">Whether to match if the prefix occurs mid-word, so <c>Data/AchievementsToIgnore</c> matches prefix <c>Data/Achievements</c>. If this is false, the prefix only matches if the asset name starts with the prefix followed by a non-alphanumeric character (including <c>.</c>, <c>/</c>, or <c>\\</c>) or the end of string.</param>
/// <param name="allowSubfolder">Whether to match the prefix if there's a subfolder path after it, so <c>Data/Achievements/Example</c> matches prefix <c>Data/Achievements</c>. If this is false, the prefix only matches if the asset name has no <c>/</c> or <c>\\</c> characters after the prefix.</param>
- bool StartsWith(string prefix, bool allowPartialWord = true, bool allowSubfolder = true);
+ bool StartsWith(string? prefix, bool allowPartialWord = true, bool allowSubfolder = true);
/// <summary>Get whether the asset is directly within the given asset path.</summary>
/// <remarks>For example, <c>Characters/Dialogue/Abigail</c> is directly under <c>Characters/Dialogue</c> but not <c>Characters</c> or <c>Characters/Dialogue/Ab</c>. To allow sub-paths, use <see cref="StartsWith"/> instead.</remarks>
/// <param name="assetFolder">The asset path to check. This doesn't need a trailing slash.</param>
- bool IsDirectlyUnderPath(string assetFolder);
+ bool IsDirectlyUnderPath(string? assetFolder);
/// <summary>Get an asset name representing the <see cref="BaseName"/> without locale.</summary>
internal IAssetName GetBaseAssetName();
diff --git a/src/SMAPI/ICommandHelper.cs b/src/SMAPI/ICommandHelper.cs
index a0c524d6..9f1c345c 100644
--- a/src/SMAPI/ICommandHelper.cs
+++ b/src/SMAPI/ICommandHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI
diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs
index 0ad209ab..2cd0c1fc 100644
--- a/src/SMAPI/IContentHelper.cs
+++ b/src/SMAPI/IContentHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
@@ -42,12 +40,14 @@ namespace StardewModdingAPI
/// <param name="source">Where to search for a matching content asset.</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
- T Load<T>(string key, ContentSource source = ContentSource.ModFolder);
+ T Load<T>(string key, ContentSource source = ContentSource.ModFolder)
+ where T : notnull;
/// <summary>Normalize an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like <see cref="string.StartsWith(string)"/> on generated asset names, and isn't necessary when passing asset names into other content helper methods.</summary>
/// <param name="assetName">The asset key.</param>
+ /// <exception cref="ContentLoadException">The asset key is empty or contains invalid characters.</exception>
[Pure]
- string NormalizeAssetName(string assetName);
+ string NormalizeAssetName(string? assetName);
/// <summary>Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists.</summary>
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
@@ -64,7 +64,8 @@ namespace StardewModdingAPI
/// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary>
/// <typeparam name="T">The asset type to remove from the cache.</typeparam>
/// <returns>Returns whether any assets were invalidated.</returns>
- bool InvalidateCache<T>();
+ bool InvalidateCache<T>()
+ where T : notnull;
/// <summary>Remove matching assets from the content cache so they're reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary>
/// <param name="predicate">A predicate matching the assets to invalidate.</param>
@@ -75,6 +76,7 @@ namespace StardewModdingAPI
/// <typeparam name="T">The data type.</typeparam>
/// <param name="data">The asset data.</param>
/// <param name="assetName">The asset name. This is only used for tracking purposes and has no effect on the patch helper.</param>
- IAssetData GetPatchHelper<T>(T data, string assetName = null);
+ IAssetData GetPatchHelper<T>(T data, string? assetName = null)
+ where T : notnull;
}
}
diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs
index f853e2b4..1215fe0b 100644
--- a/src/SMAPI/IContentPack.cs
+++ b/src/SMAPI/IContentPack.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
@@ -38,14 +36,16 @@ namespace StardewModdingAPI
/// <param name="path">The relative file path within the content pack (case-insensitive).</param>
/// <returns>Returns the deserialized model, or <c>null</c> if the file doesn't exist or is empty.</returns>
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
- TModel ReadJsonFile<TModel>(string path) where TModel : class;
+ TModel? ReadJsonFile<TModel>(string path)
+ where TModel : class;
/// <summary>Save data to a JSON file in the content pack's folder.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="path">The relative file path within the content pack (case-insensitive).</param>
/// <param name="data">The arbitrary data to save.</param>
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
- void WriteJsonFile<TModel>(string path, TModel data) where TModel : class;
+ void WriteJsonFile<TModel>(string path, TModel data)
+ where TModel : class;
/// <summary>Load content from the content pack folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
/// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
@@ -53,7 +53,8 @@ namespace StardewModdingAPI
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
[Obsolete($"Use {nameof(IContentPack.ModContent)}.{nameof(IModContentHelper.Load)} instead. This method will be removed in SMAPI 4.0.0.")]
- T LoadAsset<T>(string key);
+ T LoadAsset<T>(string key)
+ where T : notnull;
/// <summary>Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists.</summary>
/// <param name="key">The relative file path within the content pack (case-insensitive).</param>
diff --git a/src/SMAPI/IContentPackHelper.cs b/src/SMAPI/IContentPackHelper.cs
index 5464df22..c48a4f86 100644
--- a/src/SMAPI/IContentPackHelper.cs
+++ b/src/SMAPI/IContentPackHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
namespace StardewModdingAPI
diff --git a/src/SMAPI/ICursorPosition.cs b/src/SMAPI/ICursorPosition.cs
index da6cbb62..99c1b84d 100644
--- a/src/SMAPI/ICursorPosition.cs
+++ b/src/SMAPI/ICursorPosition.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework;
using StardewValley;
diff --git a/src/SMAPI/IDataHelper.cs b/src/SMAPI/IDataHelper.cs
index 4c96367b..7ddf851e 100644
--- a/src/SMAPI/IDataHelper.cs
+++ b/src/SMAPI/IDataHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI
@@ -18,14 +16,16 @@ namespace StardewModdingAPI
/// <param name="path">The file path relative to the mod folder.</param>
/// <returns>Returns the deserialized model, or <c>null</c> if the file doesn't exist or is empty.</returns>
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
- TModel ReadJsonFile<TModel>(string path) where TModel : class;
+ TModel? ReadJsonFile<TModel>(string path)
+ where TModel : class;
/// <summary>Save data to a JSON file in the mod's folder.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="path">The file path relative to the mod folder.</param>
/// <param name="data">The arbitrary data to save, or <c>null</c> to delete the file.</param>
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
- void WriteJsonFile<TModel>(string path, TModel data) where TModel : class;
+ void WriteJsonFile<TModel>(string path, TModel? data)
+ where TModel : class;
/****
** Save file
@@ -35,14 +35,16 @@ namespace StardewModdingAPI
/// <param name="key">The unique key identifying the data.</param>
/// <returns>Returns the parsed data, or <c>null</c> if the entry doesn't exist or is empty.</returns>
/// <exception cref="InvalidOperationException">The player hasn't loaded a save file yet or isn't the main player.</exception>
- TModel ReadSaveData<TModel>(string key) where TModel : class;
+ TModel? ReadSaveData<TModel>(string key)
+ where TModel : class;
/// <summary>Save arbitrary data to the current save slot. This is only possible if a save has been loaded, and the data will be lost if the player exits without saving the current day.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="key">The unique key identifying the data.</param>
/// <param name="data">The arbitrary data to save, or <c>null</c> to remove the entry.</param>
/// <exception cref="InvalidOperationException">The player hasn't loaded a save file yet or isn't the main player.</exception>
- void WriteSaveData<TModel>(string key, TModel data) where TModel : class;
+ void WriteSaveData<TModel>(string key, TModel? data)
+ where TModel : class;
/****
@@ -52,12 +54,14 @@ namespace StardewModdingAPI
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="key">The unique key identifying the data.</param>
/// <returns>Returns the parsed data, or <c>null</c> if the entry doesn't exist or is empty.</returns>
- TModel ReadGlobalData<TModel>(string key) where TModel : class;
+ TModel? ReadGlobalData<TModel>(string key)
+ where TModel : class;
/// <summary>Save arbitrary data to the local computer, synchronised by GOG/Steam if applicable.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="key">The unique key identifying the data.</param>
/// <param name="data">The arbitrary data to save, or <c>null</c> to delete the file.</param>
- void WriteGlobalData<TModel>(string key, TModel data) where TModel : class;
+ void WriteGlobalData<TModel>(string key, TModel? data)
+ where TModel : class;
}
}
diff --git a/src/SMAPI/IGameContentHelper.cs b/src/SMAPI/IGameContentHelper.cs
index 4b967993..d40d0c82 100644
--- a/src/SMAPI/IGameContentHelper.cs
+++ b/src/SMAPI/IGameContentHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
@@ -35,14 +33,16 @@ namespace StardewModdingAPI
/// <param name="assetName">The asset name to load.</param>
/// <exception cref="ArgumentException">The <paramref name="assetName"/> is empty or contains invalid characters.</exception>
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
- T Load<T>(string assetName);
+ T Load<T>(string assetName)
+ where T : notnull;
/// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
/// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, dictionaries, and lists; other types may be supported by the game's content pipeline.</typeparam>
/// <param name="assetName">The asset name to load.</param>
/// <exception cref="ArgumentException">The <paramref name="assetName"/> is empty or contains invalid characters.</exception>
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
- T Load<T>(IAssetName assetName);
+ T Load<T>(IAssetName assetName)
+ where T : notnull;
/// <summary>Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary>
/// <param name="assetName">The asset key to invalidate in the content folder.</param>
@@ -59,7 +59,8 @@ namespace StardewModdingAPI
/// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary>
/// <typeparam name="T">The asset type to remove from the cache.</typeparam>
/// <returns>Returns whether any assets were invalidated.</returns>
- bool InvalidateCache<T>();
+ bool InvalidateCache<T>()
+ where T : notnull;
/// <summary>Remove matching assets from the content cache so they're reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary>
/// <param name="predicate">A predicate matching the assets to invalidate.</param>
@@ -70,6 +71,7 @@ namespace StardewModdingAPI
/// <typeparam name="T">The data type.</typeparam>
/// <param name="data">The asset data.</param>
/// <param name="assetName">The asset name. This is only used for tracking purposes and has no effect on the patch helper.</param>
- IAssetData GetPatchHelper<T>(T data, string assetName = null);
+ IAssetData GetPatchHelper<T>(T data, string? assetName = null)
+ where T : notnull;
}
}
diff --git a/src/SMAPI/IInputHelper.cs b/src/SMAPI/IInputHelper.cs
index b7ed0838..2b907b0d 100644
--- a/src/SMAPI/IInputHelper.cs
+++ b/src/SMAPI/IInputHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using StardewModdingAPI.Utilities;
namespace StardewModdingAPI
diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs
index 0de4961e..b81ba0e3 100644
--- a/src/SMAPI/IMod.cs
+++ b/src/SMAPI/IMod.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI
{
/// <summary>The implementation for a Stardew Valley mod.</summary>
@@ -26,6 +24,6 @@ namespace StardewModdingAPI
void Entry(IModHelper helper);
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary>
- object GetApi();
+ object? GetApi();
}
}
diff --git a/src/SMAPI/IModContentHelper.cs b/src/SMAPI/IModContentHelper.cs
index 815d6848..f1f6ce94 100644
--- a/src/SMAPI/IModContentHelper.cs
+++ b/src/SMAPI/IModContentHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
@@ -18,7 +16,8 @@ namespace StardewModdingAPI
/// <param name="relativePath">The local path to a content file relative to the mod folder.</param>
/// <exception cref="ArgumentException">The <paramref name="relativePath"/> is empty or contains invalid characters.</exception>
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
- T Load<T>(string relativePath);
+ T Load<T>(string relativePath)
+ where T : notnull;
/// <summary>Get the internal asset name which allows loading a mod file through any of the game's content managers. This can be used when passing asset names directly to the game (e.g. for map tilesheets), but should be avoided if you can use <see cref="Load{T}"/> instead. This does not validate whether the asset exists.</summary>
/// <param name="relativePath">The local path to a content file relative to the mod folder.</param>
@@ -29,6 +28,7 @@ namespace StardewModdingAPI
/// <typeparam name="T">The data type.</typeparam>
/// <param name="data">The asset data.</param>
/// <param name="relativePath">The local path to the content file being edited relative to the mod folder. This is only used for tracking purposes and has no effect on the patch helper.</param>
- IAssetData GetPatchHelper<T>(T data, string relativePath = null);
+ IAssetData GetPatchHelper<T>(T data, string? relativePath = null)
+ where T : notnull;
}
}
diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs
index 5e4246aa..15e4ed8d 100644
--- a/src/SMAPI/IModHelper.cs
+++ b/src/SMAPI/IModHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using StardewModdingAPI.Events;
diff --git a/src/SMAPI/IModInfo.cs b/src/SMAPI/IModInfo.cs
index 2788e4fc..3c85d454 100644
--- a/src/SMAPI/IModInfo.cs
+++ b/src/SMAPI/IModInfo.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI
{
/// <summary>Metadata for a loaded mod.</summary>
diff --git a/src/SMAPI/IModLinked.cs b/src/SMAPI/IModLinked.cs
index cf08c9c5..172ee30c 100644
--- a/src/SMAPI/IModLinked.cs
+++ b/src/SMAPI/IModLinked.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI
{
/// <summary>An instance linked to a mod.</summary>
diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs
index 9cab08a1..cf60bc29 100644
--- a/src/SMAPI/IModRegistry.cs
+++ b/src/SMAPI/IModRegistry.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
namespace StardewModdingAPI
@@ -13,7 +11,7 @@ namespace StardewModdingAPI
/// <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>
- IModInfo Get(string uniqueID);
+ IModInfo? Get(string uniqueID);
/// <summary>Get whether a mod has been loaded.</summary>
/// <param name="uniqueID">The mod's unique ID.</param>
@@ -21,11 +19,12 @@ namespace StardewModdingAPI
/// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary>
/// <param name="uniqueID">The mod's unique ID.</param>
- object GetApi(string uniqueID);
+ object? GetApi(string uniqueID);
/// <summary>Get the API provided by a mod, mapped to a given interface which specifies the expected properties and methods. If the mod has no API or it's not compatible with the given interface, get <c>null</c>.</summary>
/// <typeparam name="TInterface">The interface which matches the properties and methods you intend to access.</typeparam>
/// <param name="uniqueID">The mod's unique ID.</param>
- TInterface GetApi<TInterface>(string uniqueID) where TInterface : class;
+ TInterface? GetApi<TInterface>(string uniqueID)
+ where TInterface : class;
}
}
diff --git a/src/SMAPI/IMonitor.cs b/src/SMAPI/IMonitor.cs
index 535f56e3..c400a211 100644
--- a/src/SMAPI/IMonitor.cs
+++ b/src/SMAPI/IMonitor.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI
{
/// <summary>Encapsulates monitoring and logging for a given module.</summary>
diff --git a/src/SMAPI/IMultiplayerHelper.cs b/src/SMAPI/IMultiplayerHelper.cs
index 77a0f3f4..bb851410 100644
--- a/src/SMAPI/IMultiplayerHelper.cs
+++ b/src/SMAPI/IMultiplayerHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using StardewValley;
@@ -18,7 +16,7 @@ namespace StardewModdingAPI
/// <summary>Get a connected player.</summary>
/// <param name="id">The player's unique ID.</param>
/// <returns>Returns the connected player, or <c>null</c> if no such player is connected.</returns>
- IMultiplayerPeer GetConnectedPlayer(long id);
+ IMultiplayerPeer? GetConnectedPlayer(long id);
/// <summary>Get all connected players.</summary>
IEnumerable<IMultiplayerPeer> GetConnectedPlayers();
@@ -30,6 +28,6 @@ namespace StardewModdingAPI
/// <param name="modIDs">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="playerIDs">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>
/// <exception cref="ArgumentNullException">The <paramref name="message"/> or <paramref name="messageType" /> is null.</exception>
- void SendMessage<TMessage>(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null);
+ void SendMessage<TMessage>(TMessage message, string messageType, string[]? modIDs = null, long[]? playerIDs = null);
}
}
diff --git a/src/SMAPI/IMultiplayerPeer.cs b/src/SMAPI/IMultiplayerPeer.cs
index e487f100..8b0062b5 100644
--- a/src/SMAPI/IMultiplayerPeer.cs
+++ b/src/SMAPI/IMultiplayerPeer.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI
{
@@ -20,6 +19,7 @@ namespace StardewModdingAPI
bool IsSplitScreen { get; }
/// <summary>Whether the player has SMAPI installed.</summary>
+ [MemberNotNullWhen(true, nameof(IMultiplayerPeer.Platform), nameof(IMultiplayerPeer.GameVersion), nameof(IMultiplayerPeer.ApiVersion), nameof(IMultiplayerPeer.Mods))]
bool HasSmapi { get; }
/// <summary>The player's screen ID, if applicable.</summary>
@@ -30,10 +30,10 @@ namespace StardewModdingAPI
GamePlatform? Platform { get; }
/// <summary>The installed version of Stardew Valley, if <see cref="HasSmapi"/> is true.</summary>
- ISemanticVersion GameVersion { get; }
+ ISemanticVersion? GameVersion { get; }
/// <summary>The installed version of SMAPI, if <see cref="HasSmapi"/> is true.</summary>
- ISemanticVersion ApiVersion { get; }
+ ISemanticVersion? ApiVersion { get; }
/// <summary>The installed mods, if <see cref="HasSmapi"/> is true.</summary>
IEnumerable<IMultiplayerPeerMod> Mods { get; }
@@ -45,6 +45,6 @@ namespace StardewModdingAPI
/// <summary>Get metadata for a mod installed by the player.</summary>
/// <param name="id">The unique mod ID.</param>
/// <returns>Returns the mod info, or <c>null</c> if the player doesn't have that mod.</returns>
- IMultiplayerPeerMod GetMod(string id);
+ IMultiplayerPeerMod? GetMod(string? id);
}
}
diff --git a/src/SMAPI/IMultiplayerPeerMod.cs b/src/SMAPI/IMultiplayerPeerMod.cs
index 81978bef..005408b1 100644
--- a/src/SMAPI/IMultiplayerPeerMod.cs
+++ b/src/SMAPI/IMultiplayerPeerMod.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace StardewModdingAPI
{
/// <summary>Metadata about a mod installed by a connected player.</summary>
diff --git a/src/SMAPI/IReflectedField.cs b/src/SMAPI/IReflectedField.cs
index 94dbe6a3..8b00909c 100644
--- a/src/SMAPI/IReflectedField.cs
+++ b/src/SMAPI/IReflectedField.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Reflection;
namespace StardewModdingAPI
diff --git a/src/SMAPI/IReflectedMethod.cs b/src/SMAPI/IReflectedMethod.cs
index 78e66cb1..04636b84 100644
--- a/src/SMAPI/IReflectedMethod.cs
+++ b/src/SMAPI/IReflectedMethod.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Reflection;
namespace StardewModdingAPI
@@ -20,10 +18,10 @@ namespace StardewModdingAPI
/// <summary>Invoke the method.</summary>
/// <typeparam name="TValue">The return type.</typeparam>
/// <param name="arguments">The method arguments to pass in.</param>
- TValue Invoke<TValue>(params object[] arguments);
+ TValue Invoke<TValue>(params object?[] arguments);
/// <summary>Invoke the method.</summary>
/// <param name="arguments">The method arguments to pass in.</param>
- void Invoke(params object[] arguments);
+ void Invoke(params object?[] arguments);
}
}
diff --git a/src/SMAPI/IReflectedProperty.cs b/src/SMAPI/IReflectedProperty.cs
index edbf0b21..73ad9f30 100644
--- a/src/SMAPI/IReflectedProperty.cs
+++ b/src/SMAPI/IReflectedProperty.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Reflection;
namespace StardewModdingAPI
diff --git a/src/SMAPI/IReflectionHelper.cs b/src/SMAPI/IReflectionHelper.cs
index bf7270cf..b8fb877f 100644
--- a/src/SMAPI/IReflectionHelper.cs
+++ b/src/SMAPI/IReflectionHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI
@@ -14,40 +12,52 @@ namespace StardewModdingAPI
/// <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>
+ /// <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 return value non-nullable.</strong></param>
+ /// <returns>Returns the method 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>
IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true);
/// <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 return value non-nullable.</strong></param>
+ /// <returns>Returns the method 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>
IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true);
/// <summary>Get an instance property.</summary>
/// <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 return value non-nullable.</strong></param>
+ /// <returns>Returns the method 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>
IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true);
/// <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 return value non-nullable.</strong></param>
+ /// <returns>Returns the method 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>
IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true);
/// <summary>Get an 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 return 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>
IReflectedMethod GetMethod(object obj, string name, bool required = true);
/// <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 return 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>
IReflectedMethod GetMethod(Type type, string name, bool required = true);
}
}
diff --git a/src/SMAPI/ITranslationHelper.cs b/src/SMAPI/ITranslationHelper.cs
index 3c297731..8be8d2c1 100644
--- a/src/SMAPI/ITranslationHelper.cs
+++ b/src/SMAPI/ITranslationHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using StardewValley;
@@ -11,7 +9,7 @@ namespace StardewModdingAPI
/*********
** Accessors
*********/
- /// <summary>The current locale.</summary>
+ /// <summary>The current locale code like <c>fr-FR</c>, or an empty string for English.</summary>
string Locale { get; }
/// <summary>The game's current language code.</summary>
@@ -31,7 +29,7 @@ namespace StardewModdingAPI
/// <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>
- Translation Get(string key, object tokens);
+ Translation Get(string key, object? tokens);
/// <summary>Get a translation in every locale for which it's defined.</summary>
/// <param name="key">The translation key.</param>
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index b7cec72c..5dee2c4d 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -712,7 +710,7 @@ namespace StardewModdingAPI.Metadata
bool isPaintMask = assetName.BaseName.EndsWith(paintMaskSuffix, StringComparison.OrdinalIgnoreCase);
// get building type
- string type = Path.GetFileName(assetName.BaseName)!;
+ string type = Path.GetFileName(assetName.BaseName);
if (isPaintMask)
type = type.Substring(0, type.Length - paintMaskSuffix.Length);
@@ -749,7 +747,7 @@ namespace StardewModdingAPI.Metadata
if (!ignoreWorld)
{
- foreach (var location in this.GetLocations())
+ foreach (GameLocation location in this.GetLocations())
{
foreach (MapSeat seat in location.mapSeats.Where(p => p != null))
{
@@ -783,7 +781,7 @@ namespace StardewModdingAPI.Metadata
// update sprites
Texture2D texture = content.Load<Texture2D>(assetName.BaseName);
- foreach (var entry in critters)
+ foreach (Critter entry in critters)
entry.sprite.spriteTexture = texture;
return critters.Length;
@@ -799,16 +797,16 @@ namespace StardewModdingAPI.Metadata
foreach (GameLocation location in this.GetLocations())
{
- IEnumerable<InteriorDoor> doors = location.interiorDoors?.Doors;
+ IEnumerable<InteriorDoor?>? doors = location.interiorDoors?.Doors;
if (doors == null)
continue;
- foreach (InteriorDoor door in doors)
+ foreach (InteriorDoor? door in doors)
{
if (door?.Sprite == null)
continue;
- string curKey = this.Reflection.GetField<string>(door.Sprite, "textureName").GetValue();
+ string? curKey = this.Reflection.GetField<string?>(door.Sprite, "textureName").GetValue();
if (this.IsSameBaseName(assetName, curKey))
door.Sprite.texture = texture.Value;
}
@@ -933,7 +931,7 @@ namespace StardewModdingAPI.Metadata
// warping onto the wrong tile (or even off-screen) if a patch changes the farmhouse
// map on location change.
if (playerPos.HasValue)
- Game1.player.Position = playerPos.Value;
+ Game1.player!.Position = playerPos.Value;
}
/// <summary>Reload the disposition data for matching NPCs.</summary>
@@ -1003,7 +1001,11 @@ namespace StardewModdingAPI.Metadata
{
GameLocation adventureGuild = Game1.getLocationFromName("AdventureGuild");
if (adventureGuild != null)
- characters.Add(new { Npc = this.Reflection.GetField<NPC>(adventureGuild, "Gil").GetValue(), AssetName = gilKey });
+ {
+ NPC? gil = this.Reflection.GetField<NPC?>(adventureGuild, "Gil").GetValue();
+ if (gil != null)
+ characters.Add(new { Npc = gil, AssetName = gilKey });
+ }
}
}
@@ -1029,7 +1031,7 @@ namespace StardewModdingAPI.Metadata
foreach (Farmer player in players)
{
- this.Reflection.GetField<Dictionary<string, Dictionary<int, List<int>>>>(typeof(FarmerRenderer), "_recolorOffsets").GetValue().Remove(player.getTexture());
+ this.Reflection.GetField<Dictionary<string, Dictionary<int, List<int>>>?>(typeof(FarmerRenderer), "_recolorOffsets").GetValue()?.Remove(player.getTexture());
player.FarmerRenderer.MarkSpriteDirty();
}
@@ -1047,13 +1049,18 @@ namespace StardewModdingAPI.Metadata
foreach (GameLocation location in this.GetLocations(buildingInteriors: false))
{
// get suspension bridges field
- var field = this.Reflection.GetField<IEnumerable<SuspensionBridge>>(location, nameof(IslandNorth.suspensionBridges), required: false);
+ var field = this.Reflection.GetField<IEnumerable<SuspensionBridge>?>(location, nameof(IslandNorth.suspensionBridges), required: false);
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- field is nullable when required: false
if (field == null || !typeof(IEnumerable<SuspensionBridge>).IsAssignableFrom(field.FieldInfo.FieldType))
continue;
// update textures
- foreach (SuspensionBridge bridge in field.GetValue())
- this.Reflection.GetField<Texture2D>(bridge, "_texture").SetValue(texture.Value);
+ IEnumerable<SuspensionBridge>? bridges = field.GetValue();
+ if (bridges != null)
+ {
+ foreach (SuspensionBridge bridge in bridges)
+ this.Reflection.GetField<Texture2D>(bridge, "_texture").SetValue(texture.Value);
+ }
}
return texture.IsValueCreated;
@@ -1131,7 +1138,7 @@ namespace StardewModdingAPI.Metadata
{
// reload schedule
this.Reflection.GetField<bool>(villager, "_hasLoadedMasterScheduleData").SetValue(false);
- this.Reflection.GetField<Dictionary<string, string>>(villager, "_masterScheduleData").SetValue(null);
+ this.Reflection.GetField<Dictionary<string, string>?>(villager, "_masterScheduleData").SetValue(null);
villager.Schedule = villager.getSchedule(Game1.dayOfMonth);
// switch to new schedule if needed
@@ -1227,7 +1234,7 @@ namespace StardewModdingAPI.Metadata
{
foreach (Building building in buildableLocation.buildings)
{
- GameLocation indoors = building.indoors.Value;
+ GameLocation? indoors = building.indoors.Value;
if (indoors != null)
yield return new LocationInfo(indoors, building);
}
@@ -1238,19 +1245,19 @@ namespace StardewModdingAPI.Metadata
/// <summary>Get whether two asset names are equivalent if you ignore the locale code.</summary>
/// <param name="left">The first value to compare.</param>
/// <param name="right">The second value to compare.</param>
- private bool IsSameBaseName(IAssetName left, string right)
+ private bool IsSameBaseName(IAssetName? left, string? right)
{
if (left is null || right is null)
return false;
- IAssetName parsedB = this.ParseAssetNameOrNull(right);
+ IAssetName? parsedB = this.ParseAssetNameOrNull(right);
return this.IsSameBaseName(left, parsedB);
}
/// <summary>Get whether two asset names are equivalent if you ignore the locale code.</summary>
/// <param name="left">The first value to compare.</param>
/// <param name="right">The second value to compare.</param>
- private bool IsSameBaseName(IAssetName left, IAssetName right)
+ private bool IsSameBaseName(IAssetName? left, IAssetName? right)
{
if (left is null || right is null)
return false;
@@ -1260,7 +1267,7 @@ namespace StardewModdingAPI.Metadata
/// <summary>Normalize an asset key to match the cache key and assert that it's valid, but don't raise an error for null or empty values.</summary>
/// <param name="path">The asset key to normalize.</param>
- private IAssetName ParseAssetNameOrNull(string path)
+ private IAssetName? ParseAssetNameOrNull(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return null;
@@ -1270,7 +1277,7 @@ namespace StardewModdingAPI.Metadata
/// <summary>Get the segments in a path (e.g. 'a/b' is 'a' and 'b').</summary>
/// <param name="path">The path to check.</param>
- private string[] GetSegments(string path)
+ private string[] GetSegments(string? path)
{
return path != null
? PathUtilities.GetSegments(path)
@@ -1280,7 +1287,7 @@ namespace StardewModdingAPI.Metadata
/// <summary>Load a texture, and dispose the old one if <see cref="AggressiveMemoryOptimizations"/> is enabled and it's different from the new instance.</summary>
/// <param name="oldTexture">The previous texture to dispose.</param>
/// <param name="key">The asset key to load.</param>
- private Texture2D LoadAndDisposeIfNeeded(Texture2D oldTexture, string key)
+ private Texture2D LoadAndDisposeIfNeeded(Texture2D? oldTexture, string key)
{
// if aggressive memory optimizations are enabled, load the asset from the disposable
// content manager and dispose the old instance if needed.
@@ -1322,7 +1329,7 @@ namespace StardewModdingAPI.Metadata
public GameLocation Location { get; }
/// <summary>The building which contains the location, if any.</summary>
- public Building ParentBuilding { get; }
+ public Building? ParentBuilding { get; }
/*********
@@ -1331,7 +1338,7 @@ namespace StardewModdingAPI.Metadata
/// <summary>Construct an instance.</summary>
/// <param name="location">The location instance.</param>
/// <param name="parentBuilding">The building which contains the location, if any.</param>
- public LocationInfo(GameLocation location, Building parentBuilding)
+ public LocationInfo(GameLocation location, Building? parentBuilding)
{
this.Location = location;
this.ParentBuilding = parentBuilding;
diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs
index 5617fd13..9b56f963 100644
--- a/src/SMAPI/Metadata/InstructionMetadata.cs
+++ b/src/SMAPI/Metadata/InstructionMetadata.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Events;
@@ -70,8 +68,8 @@ namespace StardewModdingAPI.Metadata
** detect code which may impact game stability
****/
yield return new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic);
- yield return new FieldFinder(typeof(SaveGame).FullName, new[] { nameof(SaveGame.serializer), nameof(SaveGame.farmerSerializer), nameof(SaveGame.locationSerializer) }, InstructionHandleResult.DetectedSaveSerializer);
- yield return new EventFinder(typeof(ISpecializedEvents).FullName, new[] { nameof(ISpecializedEvents.UnvalidatedUpdateTicked), nameof(ISpecializedEvents.UnvalidatedUpdateTicking) }, InstructionHandleResult.DetectedUnvalidatedUpdateTick);
+ yield return new FieldFinder(typeof(SaveGame).FullName!, new[] { nameof(SaveGame.serializer), nameof(SaveGame.farmerSerializer), nameof(SaveGame.locationSerializer) }, InstructionHandleResult.DetectedSaveSerializer);
+ yield return new EventFinder(typeof(ISpecializedEvents).FullName!, new[] { nameof(ISpecializedEvents.UnvalidatedUpdateTicked), nameof(ISpecializedEvents.UnvalidatedUpdateTicking) }, InstructionHandleResult.DetectedUnvalidatedUpdateTick);
/****
** detect paranoid issues
@@ -79,23 +77,23 @@ namespace StardewModdingAPI.Metadata
if (paranoidMode)
{
// filesystem access
- yield return new TypeFinder(typeof(System.Console).FullName, InstructionHandleResult.DetectedConsoleAccess);
+ yield return new TypeFinder(typeof(System.Console).FullName!, InstructionHandleResult.DetectedConsoleAccess);
yield return new TypeFinder(
new[]
{
- typeof(System.IO.File).FullName,
- typeof(System.IO.FileStream).FullName,
- typeof(System.IO.FileInfo).FullName,
- typeof(System.IO.Directory).FullName,
- typeof(System.IO.DirectoryInfo).FullName,
- typeof(System.IO.DriveInfo).FullName,
- typeof(System.IO.FileSystemWatcher).FullName
+ typeof(System.IO.File).FullName!,
+ typeof(System.IO.FileStream).FullName!,
+ typeof(System.IO.FileInfo).FullName!,
+ typeof(System.IO.Directory).FullName!,
+ typeof(System.IO.DirectoryInfo).FullName!,
+ typeof(System.IO.DriveInfo).FullName!,
+ typeof(System.IO.FileSystemWatcher).FullName!
},
InstructionHandleResult.DetectedFilesystemAccess
);
// shell access
- yield return new TypeFinder(typeof(System.Diagnostics.Process).FullName, InstructionHandleResult.DetectedShellAccess);
+ yield return new TypeFinder(typeof(System.Diagnostics.Process).FullName!, InstructionHandleResult.DetectedShellAccess);
}
}
}
diff --git a/src/SMAPI/Mod.cs b/src/SMAPI/Mod.cs
index 2b3750d5..f764752b 100644
--- a/src/SMAPI/Mod.cs
+++ b/src/SMAPI/Mod.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace StardewModdingAPI
@@ -11,13 +9,13 @@ namespace StardewModdingAPI
** Accessors
*********/
/// <inheritdoc />
- public IModHelper Helper { get; internal set; }
+ public IModHelper Helper { get; internal set; } = null!;
/// <inheritdoc />
- public IMonitor Monitor { get; internal set; }
+ public IMonitor Monitor { get; internal set; } = null!;
/// <inheritdoc />
- public IManifest ModManifest { get; internal set; }
+ public IManifest ModManifest { get; internal set; } = null!;
/*********
@@ -27,7 +25,7 @@ namespace StardewModdingAPI
public abstract void Entry(IModHelper helper);
/// <inheritdoc />
- public virtual object GetApi()
+ public virtual object? GetApi()
{
return null;
}
@@ -35,7 +33,7 @@ namespace StardewModdingAPI
/// <summary>Release or reset unmanaged resources.</summary>
public void Dispose()
{
- (this.Helper as IDisposable)?.Dispose(); // deliberate do this outside overridable dispose method so mods don't accidentally suppress it
+ (this.Helper as IDisposable)?.Dispose(); // deliberately do this outside overridable dispose method so mods don't accidentally suppress it
this.Dispose(true);
GC.SuppressFinalize(this);
}
@@ -51,6 +49,7 @@ namespace StardewModdingAPI
/// <summary>Destruct the instance.</summary>
~Mod()
{
+ (this.Helper as IDisposable)?.Dispose(); // deliberately do this outside overridable dispose method so mods don't accidentally suppress it
this.Dispose(false);
}
}
diff --git a/src/SMAPI/Patches/Game1Patcher.cs b/src/SMAPI/Patches/Game1Patcher.cs
index c5d98e9e..8f806790 100644
--- a/src/SMAPI/Patches/Game1Patcher.cs
+++ b/src/SMAPI/Patches/Game1Patcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using HarmonyLib;
@@ -22,10 +20,10 @@ namespace StardewModdingAPI.Patches
** Fields
*********/
/// <summary>Simplifies access to private code.</summary>
- private static Reflector Reflection;
+ private static Reflector Reflection = null!; // initialized in constructor
/// <summary>A callback to invoke when the load stage changes.</summary>
- private static Action<LoadStage> OnStageChanged;
+ private static Action<LoadStage> OnStageChanged = null!; // initialized in constructor
/// <summary>Whether the game is running running the code in <see cref="Game1.loadForNewGame"/>.</summary>
private static bool IsInLoadForNewGame;
diff --git a/src/SMAPI/Patches/TitleMenuPatcher.cs b/src/SMAPI/Patches/TitleMenuPatcher.cs
index 56e5597c..18f1a830 100644
--- a/src/SMAPI/Patches/TitleMenuPatcher.cs
+++ b/src/SMAPI/Patches/TitleMenuPatcher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics.CodeAnalysis;
using HarmonyLib;
@@ -19,7 +17,7 @@ namespace StardewModdingAPI.Patches
** Fields
*********/
/// <summary>A callback to invoke when the load stage changes.</summary>
- private static Action<LoadStage> OnStageChanged;
+ private static Action<LoadStage> OnStageChanged = null!; // initialized in constructor
/*********
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index b2e213fe..a6861bca 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -22,7 +20,7 @@ namespace StardewModdingAPI
private static readonly string DllSearchPath = EarlyConstants.InternalFilesPath;
/// <summary>The assembly paths in the search folders indexed by assembly name.</summary>
- private static Dictionary<string, string> AssemblyPathsByName;
+ private static Dictionary<string, string>? AssemblyPathsByName;
/*********
@@ -61,7 +59,7 @@ namespace StardewModdingAPI
/// <summary>Method called when assembly resolution fails, which may return a manually resolved assembly.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
- private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e)
+ private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs e)
{
// cache assembly paths by name
if (Program.AssemblyPathsByName == null)
@@ -74,7 +72,7 @@ namespace StardewModdingAPI
{
try
{
- string curName = AssemblyName.GetAssemblyName(dllPath).Name;
+ string? curName = AssemblyName.GetAssemblyName(dllPath).Name;
if (curName != null)
Program.AssemblyPathsByName[curName] = dllPath;
}
@@ -89,8 +87,8 @@ namespace StardewModdingAPI
// resolve
try
{
- string searchName = new AssemblyName(e.Name).Name;
- return searchName != null && Program.AssemblyPathsByName.TryGetValue(searchName, out string assemblyPath)
+ string? searchName = new AssemblyName(e.Name).Name;
+ return searchName != null && Program.AssemblyPathsByName.TryGetValue(searchName, out string? assemblyPath)
? Assembly.LoadFrom(assemblyPath)
: null;
}
@@ -129,7 +127,7 @@ namespace StardewModdingAPI
// min version
if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion))
{
- ISemanticVersion suggestedApiVersion = Constants.GetCompatibleApiVersion(Constants.GameVersion);
+ ISemanticVersion? suggestedApiVersion = Constants.GetCompatibleApiVersion(Constants.GameVersion);
Program.PrintErrorAndExit(suggestedApiVersion != null
? $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. You can install SMAPI {suggestedApiVersion} instead to fix this error, or update your game to the latest version."
: $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI."
@@ -152,7 +150,7 @@ namespace StardewModdingAPI
foreach (var type in new[] { typeof(IManifest), typeof(Manifest) })
{
AssemblyName assemblyName = type.Assembly.GetName();
- ISemanticVersion assemblyVersion = new SemanticVersion(assemblyName.Version);
+ ISemanticVersion assemblyVersion = new SemanticVersion(assemblyName.Version!);
if (!assemblyVersion.Equals(smapiVersion))
Program.PrintErrorAndExit($"Oops! The 'smapi-internal/{assemblyName.Name}.dll' file is version {assemblyVersion} instead of the required {Constants.ApiVersion}. SMAPI doesn't seem to be installed correctly.");
}
@@ -184,7 +182,7 @@ namespace StardewModdingAPI
bool? developerMode = null;
string modsPath;
{
- string rawModsPath = null;
+ string? rawModsPath = null;
// get mods path from command line args
int pathIndex = Array.LastIndexOf(args, "--mods-path") + 1;
@@ -202,7 +200,7 @@ namespace StardewModdingAPI
rawModsPath = Environment.GetEnvironmentVariable("SMAPI_MODS_PATH");
if (developerMode is null)
{
- string rawDeveloperMode = Environment.GetEnvironmentVariable("SMAPI_DEVELOPER_MODE");
+ string? rawDeveloperMode = Environment.GetEnvironmentVariable("SMAPI_DEVELOPER_MODE");
if (rawDeveloperMode != null)
developerMode = bool.Parse(rawDeveloperMode);
}
@@ -221,7 +219,7 @@ namespace StardewModdingAPI
/// <summary>Write an error directly to the console and exit.</summary>
/// <param name="message">The error message to display.</param>
/// <param name="technicalMessage">An additional message to log with technical details.</param>
- private static void PrintErrorAndExit(string message, string technicalMessage = null)
+ private static void PrintErrorAndExit(string message, string? technicalMessage = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs
index 7a6cdcdd..81e526e7 100644
--- a/src/SMAPI/SemanticVersion.cs
+++ b/src/SMAPI/SemanticVersion.cs
@@ -85,6 +85,9 @@ namespace StardewModdingAPI
}
/// <inheritdoc />
+#if NET5_0_OR_GREATER
+ [MemberNotNullWhen(true, nameof(SemanticVersion.PrereleaseTag))]
+#endif
public bool IsPrerelease()
{
return this.Version.IsPrerelease();
diff --git a/src/SMAPI/Translation.cs b/src/SMAPI/Translation.cs
index ef98a00f..eb608f15 100644
--- a/src/SMAPI/Translation.cs
+++ b/src/SMAPI/Translation.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections;
using System.Collections.Generic;
@@ -17,14 +15,14 @@ namespace StardewModdingAPI
/// <summary>The placeholder text when the translation is <c>null</c> or empty, where <c>{0}</c> is the translation key.</summary>
internal const string PlaceholderText = "(no translation:{0})";
- /// <summary>The locale for which the translation was fetched.</summary>
+ /// <summary>The locale for which the translation was fetched like <c>fr-FR</c>, or an empty string for English.</summary>
private readonly string Locale;
/// <summary>The underlying translation text.</summary>
- private readonly string Text;
+ private readonly string? Text;
/// <summary>The value to return if the translations is undefined.</summary>
- private readonly string Placeholder;
+ private readonly string? Placeholder;
/*********
@@ -41,12 +39,12 @@ namespace StardewModdingAPI
/// <param name="locale">The locale for which the translation was fetched.</param>
/// <param name="key">The translation key.</param>
/// <param name="text">The underlying translation text.</param>
- internal Translation(string locale, string key, string text)
+ internal Translation(string locale, string key, string? text)
: this(locale, key, text, string.Format(Translation.PlaceholderText, key)) { }
/// <summary>Replace the text if it's <c>null</c> or empty. If you set a <c>null</c> or empty value, the translation will show the fallback "no translation" placeholder (see <see cref="UsePlaceholder"/> if you want to disable that). Returns a new instance if changed.</summary>
/// <param name="default">The default value.</param>
- public Translation Default(string @default)
+ public Translation Default(string? @default)
{
return this.HasValue()
? this
@@ -54,7 +52,7 @@ namespace StardewModdingAPI
}
/// <summary>Whether to return a "no translation" placeholder if the translation is <c>null</c> or empty. Returns a new instance.</summary>
- /// <param name="use">Whether to return a placeholder.</param>
+ /// <param name="use">Whether to return a placeholder. <strong>Due to limitations with nullable reference types, setting this to <c>false</c> will still mark the text non-nullable.</strong></param>
public Translation UsePlaceholder(bool use)
{
return new Translation(this.Locale, this.Key, this.Text, use ? string.Format(Translation.PlaceholderText, this.Key) : null);
@@ -63,20 +61,20 @@ namespace StardewModdingAPI
/// <summary>Replace tokens in the text like <c>{{value}}</c> with the given values. Returns a new instance.</summary>
/// <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>
/// <exception cref="ArgumentNullException">The <paramref name="tokens"/> argument is <c>null</c>.</exception>
- public Translation Tokens(object tokens)
+ public Translation Tokens(object? tokens)
{
if (string.IsNullOrWhiteSpace(this.Text) || tokens == null)
return this;
// get dictionary of tokens
- IDictionary<string, string> tokenLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ Dictionary<string, string?> tokenLookup = new(StringComparer.OrdinalIgnoreCase);
{
// from dictionary
if (tokens is IDictionary inputLookup)
{
foreach (DictionaryEntry entry in inputLookup)
{
- string key = entry.Key.ToString()?.Trim();
+ string? key = entry.Key.ToString()?.Trim();
if (key != null)
tokenLookup[key] = entry.Value?.ToString();
}
@@ -97,8 +95,8 @@ namespace StardewModdingAPI
string text = Regex.Replace(this.Text, @"{{([ \w\.\-]+)}}", match =>
{
string key = match.Groups[1].Value.Trim();
- return tokenLookup.TryGetValue(key, out string value)
- ? value
+ return tokenLookup.TryGetValue(key, out string? value)
+ ? (value ?? "")
: match.Value;
});
return new Translation(this.Locale, this.Key, text);
@@ -111,18 +109,20 @@ namespace StardewModdingAPI
}
/// <summary>Get the translation text. Calling this method isn't strictly necessary, since you can assign a <see cref="Translation"/> value directly to a string.</summary>
+ /// <remarks><strong>Limitation with nullable reference types: if there's no text and you disabled the fallback via <see cref="UsePlaceholder"/>, this will return null but the return value will still be marked non-nullable.</strong></remarks>
public override string ToString()
{
return this.Placeholder != null && !this.HasValue()
? this.Placeholder
- : this.Text;
+ : this.Text!;
}
/// <summary>Get a string representation of the given translation.</summary>
/// <param name="translation">The translation key.</param>
+ /// <remarks><strong>Limitation with nullable reference types: if there's no text and you disabled the fallback via <see cref="UsePlaceholder"/>, this will return null but the return value will still be marked non-nullable.</strong></remarks>
public static implicit operator string(Translation translation)
{
- return translation?.ToString();
+ return translation.ToString();
}
@@ -134,7 +134,7 @@ namespace StardewModdingAPI
/// <param name="key">The translation key.</param>
/// <param name="text">The underlying translation text.</param>
/// <param name="placeholder">The value to return if the translations is undefined.</param>
- private Translation(string locale, string key, string text, string placeholder)
+ private Translation(string locale, string key, string? text, string? placeholder)
{
this.Locale = locale;
this.Key = key;
diff --git a/src/SMAPI/Utilities/CaseInsensitivePathCache.cs b/src/SMAPI/Utilities/CaseInsensitivePathCache.cs
index 4596fdce..2ac1b9f9 100644
--- a/src/SMAPI/Utilities/CaseInsensitivePathCache.cs
+++ b/src/SMAPI/Utilities/CaseInsensitivePathCache.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -81,7 +79,7 @@ namespace StardewModdingAPI.Utilities
return relativePath;
// already cached
- if (this.RelativePathCache.Value.TryGetValue(relativePath, out string resolved))
+ if (this.RelativePathCache.Value.TryGetValue(relativePath, out string? resolved))
return resolved;
// file exists but isn't cached for some reason
diff --git a/src/SMAPI/Utilities/Keybind.cs b/src/SMAPI/Utilities/Keybind.cs
index 7b1acf1d..3455ce77 100644
--- a/src/SMAPI/Utilities/Keybind.cs
+++ b/src/SMAPI/Utilities/Keybind.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Framework;
@@ -44,7 +43,7 @@ namespace StardewModdingAPI.Utilities
/// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
/// <param name="parsed">The parsed keybind, if valid.</param>
/// <param name="errors">The parse errors, if any.</param>
- public static bool TryParse(string input, out Keybind parsed, out string[] errors)
+ public static bool TryParse(string input, [NotNullWhen(true)] out Keybind? parsed, out string[] errors)
{
// empty input
if (string.IsNullOrWhiteSpace(input))
diff --git a/src/SMAPI/Utilities/KeybindList.cs b/src/SMAPI/Utilities/KeybindList.cs
index 7b2c396b..aa12a37a 100644
--- a/src/SMAPI/Utilities/KeybindList.cs
+++ b/src/SMAPI/Utilities/KeybindList.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Toolkit.Serialization;
@@ -42,7 +41,7 @@ namespace StardewModdingAPI.Utilities
/// <exception cref="FormatException">The <paramref name="input"/> format is invalid.</exception>
public static KeybindList Parse(string input)
{
- return KeybindList.TryParse(input, out KeybindList parsed, out string[] errors)
+ return KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors)
? parsed
: throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{input}'.\n{string.Join("\n", errors)}");
}
@@ -51,7 +50,7 @@ namespace StardewModdingAPI.Utilities
/// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
/// <param name="parsed">The parsed keybind list, if valid.</param>
/// <param name="errors">The errors that occurred while parsing the input, if any.</param>
- public static bool TryParse(string input, out KeybindList parsed, out string[] errors)
+ public static bool TryParse(string? input, [NotNullWhen(true)] out KeybindList? parsed, out string[] errors)
{
// empty input
if (string.IsNullOrWhiteSpace(input))
@@ -69,7 +68,7 @@ namespace StardewModdingAPI.Utilities
if (string.IsNullOrWhiteSpace(rawSet))
continue;
- if (!Keybind.TryParse(rawSet, out Keybind keybind, out string[] curErrors))
+ if (!Keybind.TryParse(rawSet, out Keybind? keybind, out string[] curErrors))
rawErrors.AddRange(curErrors);
else
keybinds.Add(keybind);
@@ -151,7 +150,7 @@ namespace StardewModdingAPI.Utilities
}
/// <summary>Get the keybind which is currently down, if any. If there are multiple keybinds down, the first one is returned.</summary>
- public Keybind GetKeybindCurrentlyDown()
+ public Keybind? GetKeybindCurrentlyDown()
{
return this.Keybinds.FirstOrDefault(p => p.GetState().IsDown());
}
diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs
index afe3ba91..6c2e436b 100644
--- a/src/SMAPI/Utilities/PerScreen.cs
+++ b/src/SMAPI/Utilities/PerScreen.cs
@@ -1,8 +1,7 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
+using StardewModdingAPI.Framework;
namespace StardewModdingAPI.Utilities
{
@@ -39,15 +38,14 @@ namespace StardewModdingAPI.Utilities
** Public methods
*********/
/// <summary>Construct an instance.</summary>
+ /// <remarks><strong>Limitation with nullable reference types:</strong> when the underlying type <typeparamref name="T"/> is nullable, this sets the default value to null regardless of whether you marked the type parameter nullable. To avoid that, set the default value with the 'createNewState' argument instead.</remarks>
public PerScreen()
- : this(null) { }
+ : this(null, nullExpected: true) { }
/// <summary>Construct an instance.</summary>
/// <param name="createNewState">Create the initial state for a screen.</param>
public PerScreen(Func<T> createNewState)
- {
- this.CreateNewState = createNewState ?? (() => default);
- }
+ : this(createNewState, nullExpected: false) { }
/// <summary>Get all active values by screen ID. This doesn't initialize the value for a screen ID if it's not created yet.</summary>
public IEnumerable<KeyValuePair<int, T>> GetActiveValues()
@@ -61,7 +59,7 @@ namespace StardewModdingAPI.Utilities
public T GetValueForScreen(int screenId)
{
this.RemoveDeadScreens();
- return this.States.TryGetValue(screenId, out T state)
+ return this.States.TryGetValue(screenId, out T? state)
? state
: this.States[screenId] = this.CreateNewState();
}
@@ -85,6 +83,30 @@ namespace StardewModdingAPI.Utilities
/*********
** Private methods
*********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="createNewState">Create the initial state for a screen.</param>
+ /// <param name="nullExpected">Whether a null <paramref name="createNewState"/> value is expected.</param>
+ /// <remarks>This constructor only exists to maintain backwards compatibility. In SMAPI 4.0.0, the overload that passes <c>nullExpected: false</c> should throw an exception instead.</remarks>
+ private PerScreen(Func<T>? createNewState, bool nullExpected)
+ {
+ if (createNewState is null)
+ {
+ createNewState = (() => default!);
+
+ if (!nullExpected)
+ {
+ SCore.DeprecationManager.Warn(
+ SCore.DeprecationManager.GetModFromStack(),
+ $"calling the {nameof(PerScreen<T>)} constructor with null",
+ "3.14.0",
+ DeprecationLevel.Notice
+ );
+ }
+ }
+
+ this.CreateNewState = createNewState;
+ }
+
/// <summary>Remove screens which are no longer active.</summary>
private void RemoveDeadScreens()
{
diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs
index b10bc3da..1d4e4489 100644
--- a/src/SMAPI/Utilities/SDate.cs
+++ b/src/SMAPI/Utilities/SDate.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Newtonsoft.Json;
using StardewModdingAPI.Framework;
@@ -24,7 +23,7 @@ namespace StardewModdingAPI.Utilities
private readonly int DaysInSeason = 28;
/// <summary>The core SMAPI translations.</summary>
- internal static Translator Translations;
+ internal static Translator? Translations;
/*********
@@ -94,7 +93,8 @@ namespace StardewModdingAPI.Utilities
/// <summary>Get a date from a game date instance.</summary>
/// <param name="date">The world date.</param>
- public static SDate From(WorldDate date)
+ [return: NotNullIfNotNull("date")]
+ public static SDate? From(WorldDate? date)
{
if (date == null)
return null;
@@ -170,14 +170,14 @@ namespace StardewModdingAPI.Utilities
****/
/// <summary>Get whether this instance is equal to another.</summary>
/// <param name="other">The other value to compare.</param>
- public bool Equals(SDate other)
+ public bool Equals(SDate? other)
{
return this == other;
}
/// <summary>Get whether this instance is equal to another.</summary>
/// <param name="obj">The other value to compare.</param>
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
return obj is SDate other && this == other;
}
@@ -195,7 +195,7 @@ namespace StardewModdingAPI.Utilities
/// <param name="date">The base date to compare.</param>
/// <param name="other">The other date to compare.</param>
/// <returns>The equality of the dates</returns>
- public static bool operator ==(SDate date, SDate other)
+ public static bool operator ==(SDate? date, SDate? other)
{
return date?.DaysSinceStart == other?.DaysSinceStart;
}
@@ -203,7 +203,7 @@ namespace StardewModdingAPI.Utilities
/// <summary>Get whether one date is not equal to another.</summary>
/// <param name="date">The base date to compare.</param>
/// <param name="other">The other date to compare.</param>
- public static bool operator !=(SDate date, SDate other)
+ public static bool operator !=(SDate? date, SDate? other)
{
return date?.DaysSinceStart != other?.DaysSinceStart;
}
@@ -211,7 +211,7 @@ namespace StardewModdingAPI.Utilities
/// <summary>Get whether one date is more than another.</summary>
/// <param name="date">The base date to compare.</param>
/// <param name="other">The other date to compare.</param>
- public static bool operator >(SDate date, SDate other)
+ public static bool operator >(SDate? date, SDate? other)
{
return date?.DaysSinceStart > other?.DaysSinceStart;
}
@@ -219,7 +219,7 @@ namespace StardewModdingAPI.Utilities
/// <summary>Get whether one date is more than or equal to another.</summary>
/// <param name="date">The base date to compare.</param>
/// <param name="other">The other date to compare.</param>
- public static bool operator >=(SDate date, SDate other)
+ public static bool operator >=(SDate? date, SDate? other)
{
return date?.DaysSinceStart >= other?.DaysSinceStart;
}
@@ -227,7 +227,7 @@ namespace StardewModdingAPI.Utilities
/// <summary>Get whether one date is less than or equal to another.</summary>
/// <param name="date">The base date to compare.</param>
/// <param name="other">The other date to compare.</param>
- public static bool operator <=(SDate date, SDate other)
+ public static bool operator <=(SDate? date, SDate? other)
{
return date?.DaysSinceStart <= other?.DaysSinceStart;
}
@@ -235,7 +235,7 @@ namespace StardewModdingAPI.Utilities
/// <summary>Get whether one date is less than another.</summary>
/// <param name="date">The base date to compare.</param>
/// <param name="other">The other date to compare.</param>
- public static bool operator <(SDate date, SDate other)
+ public static bool operator <(SDate? date, SDate? other)
{
return date?.DaysSinceStart < other?.DaysSinceStart;
}
@@ -250,9 +250,10 @@ namespace StardewModdingAPI.Utilities
/// <param name="year">The year.</param>
/// <param name="allowDayZero">Whether to allow 0 spring Y1 as a valid date.</param>
/// <exception cref="ArgumentException">One of the arguments has an invalid value (like day 35).</exception>
+ [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The nullability is validated in this constructor.")]
private SDate(int day, string season, int year, bool allowDayZero)
{
- season = season?.Trim().ToLowerInvariant();
+ season = season?.Trim().ToLowerInvariant()!; // null-checked below
// validate
if (season == null)
@@ -288,25 +289,17 @@ namespace StardewModdingAPI.Utilities
/// <param name="day">The day of month.</param>
private DayOfWeek GetDayOfWeek(int day)
{
- switch (day % 7)
+ return (day % 7) switch
{
- case 0:
- return DayOfWeek.Sunday;
- case 1:
- return DayOfWeek.Monday;
- case 2:
- return DayOfWeek.Tuesday;
- case 3:
- return DayOfWeek.Wednesday;
- case 4:
- return DayOfWeek.Thursday;
- case 5:
- return DayOfWeek.Friday;
- case 6:
- return DayOfWeek.Saturday;
- default:
- return 0;
- }
+ 0 => DayOfWeek.Sunday,
+ 1 => DayOfWeek.Monday,
+ 2 => DayOfWeek.Tuesday,
+ 3 => DayOfWeek.Wednesday,
+ 4 => DayOfWeek.Thursday,
+ 5 => DayOfWeek.Friday,
+ 6 => DayOfWeek.Saturday,
+ _ => 0
+ };
}
/// <summary>Get the number of days since the game began (starting at 1 for the first day of spring in Y1).</summary>