From 89f8be10b17bf0b2caa250bc0475b9a682f0fa5d Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 24 May 2019 03:23:57 -0400 Subject: [PATCH] Upgrade --- src/SMAPI.sln | 2 + src/SMAPI/Constants.cs | 2 +- .../Rewriters/TypeFieldToTypeFieldRewriter.cs | 78 + .../Framework/RewriteFacades/FarmerMethods.cs | 21 + .../RewriteFacades/FarmerRendererMethods.cs | 16 + .../Framework/RewriteFacades/Game1Methods.cs | 32 + .../RewriteFacades/HUDMessageMethods.cs | 17 + .../RewriteFacades/IClickableMenuMethods.cs | 33 + .../RewriteFacades/MapPageMethods.cs | 17 + .../RewriteFacades/TextBoxMethods.cs | 18 + src/SMAPI/StardewModdingAPI.csproj | 1 + src/SMAPI/StardewModdingAPI.sln | 26 + src/StardewModdingAPI/Constants.cs | 238 + src/StardewModdingAPI/ContentSource.cs | 12 + src/StardewModdingAPI/Context.cs | 47 + src/StardewModdingAPI/Enums/LoadStage.cs | 36 + src/StardewModdingAPI/Enums/SkillType.cs | 26 + .../Events/BuildingListChangedEventArgs.cs | 42 + .../Events/ButtonPressedEventArgs.cs | 60 + .../Events/ButtonReleasedEventArgs.cs | 60 + src/StardewModdingAPI/Events/ChangeType.cs | 15 + src/StardewModdingAPI/Events/ContentEvents.cs | 45 + src/StardewModdingAPI/Events/ControlEvents.cs | 123 + .../Events/CursorMovedEventArgs.cs | 30 + .../Events/DayEndingEventArgs.cs | 7 + .../Events/DayStartedEventArgs.cs | 7 + .../Events/DebrisListChangedEventArgs.cs | 41 + .../Events/EventArgsClickableMenuChanged.cs | 33 + .../Events/EventArgsClickableMenuClosed.cs | 28 + .../EventArgsControllerButtonPressed.cs | 34 + .../EventArgsControllerButtonReleased.cs | 34 + .../EventArgsControllerTriggerPressed.cs | 39 + .../EventArgsControllerTriggerReleased.cs | 39 + .../Events/EventArgsInput.cs | 64 + .../Events/EventArgsIntChanged.cs | 32 + .../Events/EventArgsInventoryChanged.cs | 43 + .../Events/EventArgsKeyPressed.cs | 28 + .../Events/EventArgsKeyboardStateChanged.cs | 33 + .../Events/EventArgsLevelUp.cs | 55 + .../EventArgsLocationBuildingsChanged.cs | 41 + .../Events/EventArgsLocationObjectsChanged.cs | 42 + .../Events/EventArgsLocationsChanged.cs | 35 + .../Events/EventArgsMineLevelChanged.cs | 32 + .../Events/EventArgsMouseStateChanged.cs | 44 + .../Events/EventArgsPlayerWarped.cs | 34 + .../Events/EventArgsValueChanged.cs | 33 + src/StardewModdingAPI/Events/GameEvents.cs | 122 + .../Events/GameLaunchedEventArgs.cs | 7 + .../Events/GraphicsEvents.cs | 120 + .../Events/IDisplayEvents.cs | 39 + .../Events/IGameLoopEvents.cs | 50 + src/StardewModdingAPI/Events/IInputEvents.cs | 20 + src/StardewModdingAPI/Events/IModEvents.cs | 27 + .../Events/IMultiplayerEvents.cs | 17 + src/StardewModdingAPI/Events/IPlayerEvents.cs | 17 + .../Events/ISpecialisedEvents.cs | 17 + src/StardewModdingAPI/Events/IWorldEvents.cs | 29 + src/StardewModdingAPI/Events/InputEvents.cs | 56 + .../Events/InventoryChangedEventArgs.cs | 59 + .../Events/ItemStackChange.cs | 20 + .../Events/ItemStackSizeChange.cs | 35 + ...LargeTerrainFeatureListChangedEventArgs.cs | 42 + .../Events/LevelChangedEventArgs.cs | 45 + .../Events/LoadStageChangedEventArgs.cs | 31 + .../Events/LocationEvents.cs | 67 + .../Events/LocationListChangedEventArgs.cs | 33 + .../Events/MenuChangedEventArgs.cs | 31 + src/StardewModdingAPI/Events/MenuEvents.cs | 56 + src/StardewModdingAPI/Events/MineEvents.cs | 45 + .../Events/ModMessageReceivedEventArgs.cs | 46 + .../Events/MouseWheelScrolledEventArgs.cs | 38 + .../Events/MultiplayerEvents.cs | 78 + .../Events/NpcListChangedEventArgs.cs | 41 + .../Events/ObjectListChangedEventArgs.cs | 43 + .../Events/OneSecondUpdateTickedEventArgs.cs | 26 + .../Events/OneSecondUpdateTickingEventArgs.cs | 26 + .../Events/PeerContextReceivedEventArgs.cs | 25 + .../Events/PeerDisconnectedEventArgs.cs | 25 + src/StardewModdingAPI/Events/PlayerEvents.cs | 68 + .../Events/RenderedActiveMenuEventArgs.cs | 16 + .../Events/RenderedEventArgs.cs | 16 + .../Events/RenderedHudEventArgs.cs | 16 + .../Events/RenderedWorldEventArgs.cs | 16 + .../Events/RenderingActiveMenuEventArgs.cs | 16 + .../Events/RenderingEventArgs.cs | 16 + .../Events/RenderingHudEventArgs.cs | 16 + .../Events/RenderingWorldEventArgs.cs | 16 + .../Events/ReturnedToTitleEventArgs.cs | 7 + .../Events/SaveCreatedEventArgs.cs | 7 + .../Events/SaveCreatingEventArgs.cs | 7 + src/StardewModdingAPI/Events/SaveEvents.cs | 100 + .../Events/SaveLoadedEventArgs.cs | 7 + .../Events/SavedEventArgs.cs | 7 + .../Events/SavingEventArgs.cs | 7 + .../Events/SpecialisedEvents.cs | 45 + .../TerrainFeatureListChangedEventArgs.cs | 43 + .../Events/TimeChangedEventArgs.cs | 30 + src/StardewModdingAPI/Events/TimeEvents.cs | 56 + .../UnvalidatedUpdateTickedEventArgs.cs | 29 + .../UnvalidatedUpdateTickingEventArgs.cs | 29 + .../Events/UpdateTickedEventArgs.cs | 29 + .../Events/UpdateTickingEventArgs.cs | 29 + .../Events/WarpedEventArgs.cs | 40 + .../Events/WindowResizedEventArgs.cs | 31 + src/StardewModdingAPI/Framework/Command.cs | 40 + .../Framework/CommandManager.cs | 153 + .../Framework/Content/AssetData.cs | 54 + .../Content/AssetDataForDictionary.cs | 54 + .../Framework/Content/AssetDataForImage.cs | 106 + .../Framework/Content/AssetDataForObject.cs | 54 + .../Framework/Content/AssetInfo.cs | 82 + .../Framework/Content/ContentCache.cs | 137 + .../Framework/ContentCoordinator.cs | 323 + .../ContentManagers/BaseContentManager.cs | 303 + .../ContentManagers/GameContentManager.cs | 279 + .../ContentManagers/IContentManager.cs | 86 + .../ContentManagers/ModContentManager.cs | 285 + .../Framework/ContentPack.cs | 99 + .../Framework/CursorPosition.cs | 47 + .../Framework/DeprecationLevel.cs | 15 + .../Framework/DeprecationManager.cs | 168 + .../Framework/DeprecationWarning.cs | 43 + .../Framework/Events/EventManager.cs | 491 ++ .../Framework/Events/ManagedEvent.cs | 161 + .../Framework/Events/ManagedEventBase.cs | 93 + .../Framework/Events/ModDisplayEvents.cs | 93 + .../Framework/Events/ModEvents.cs | 50 + .../Framework/Events/ModEventsBase.cs | 28 + .../Framework/Events/ModGameLoopEvents.cs | 121 + .../Framework/Events/ModInputEvents.cs | 50 + .../Framework/Events/ModMultiplayerEvents.cs | 43 + .../Framework/Events/ModPlayerEvents.cs | 43 + .../Framework/Events/ModSpecialisedEvents.cs | 43 + .../Framework/Events/ModWorldEvents.cs | 71 + .../SAssemblyLoadFailedException.cs | 16 + .../Exceptions/SContentLoadException.cs | 18 + .../Framework/GameVersion.cs | 69 + .../Framework/IModMetadata.cs | 106 + .../Framework/Input/GamePadStateBuilder.cs | 162 + .../Framework/Input/InputStatus.cs | 29 + .../Framework/Input/SInputState.cs | 386 ++ .../Framework/InternalExtensions.cs | 106 + .../Logging/ConsoleInterceptionManager.cs | 59 + .../Logging/InterceptingTextWriter.cs | 63 + .../Framework/Logging/LogFileManager.cs | 57 + .../Framework/ModHelpers/BaseHelper.cs | 23 + .../Framework/ModHelpers/CommandHelper.cs | 53 + .../Framework/ModHelpers/ContentHelper.cs | 381 ++ .../Framework/ModHelpers/ContentPackHelper.cs | 82 + .../Framework/ModHelpers/DataHelper.cs | 166 + .../Framework/ModHelpers/InputHelper.cs | 54 + .../Framework/ModHelpers/ModHelper.cs | 190 + .../Framework/ModHelpers/ModRegistryHelper.cs | 105 + .../Framework/ModHelpers/MultiplayerHelper.cs | 76 + .../Framework/ModHelpers/ReflectionHelper.cs | 156 + .../Framework/ModHelpers/TranslationHelper.cs | 140 + .../ModLoading/AssemblyDefinitionResolver.cs | 54 + .../ModLoading/AssemblyLoadStatus.cs | 15 + .../Framework/ModLoading/AssemblyLoader.cs | 407 ++ .../ModLoading/AssemblyParseResult.cs | 36 + .../ModLoading/Finders/EventFinder.cs | 82 + .../ModLoading/Finders/FieldFinder.cs | 82 + .../ModLoading/Finders/MethodFinder.cs | 82 + .../ModLoading/Finders/PropertyFinder.cs | 82 + ...ferenceToMemberWithUnexpectedTypeFinder.cs | 142 + .../Finders/ReferenceToMissingMemberFinder.cs | 112 + .../ModLoading/Finders/TypeFinder.cs | 139 + .../ModLoading/IInstructionHandler.cs | 34 + .../IncompatibleInstructionException.cs | 35 + .../ModLoading/InstructionHandleResult.cs | 35 + .../ModLoading/InvalidModStateException.cs | 14 + .../ModLoading/ModDependencyStatus.cs | 18 + .../Framework/ModLoading/ModMetadata.cs | 192 + .../Framework/ModLoading/ModMetadataStatus.cs | 12 + .../Framework/ModLoading/ModResolver.cs | 449 ++ .../ModLoading/PlatformAssemblyMap.cs | 65 + .../Framework/ModLoading/RewriteHelper.cs | 114 + .../Rewriters/FieldReplaceRewriter.cs | 50 + .../Rewriters/FieldToPropertyRewriter.cs | 58 + .../Rewriters/MethodParentRewriter.cs | 88 + .../StaticFieldToConstantRewriter.cs | 63 + .../TypeFieldToAnotherTypeFieldRewriter.cs | 77 + .../Rewriters/TypeReferenceRewriter.cs | 152 + .../ModLoading/TypeReferenceComparer.cs | 201 + .../Framework/ModRegistry.cs | 115 + .../Framework/Models/ModFolderExport.cs | 21 + .../Framework/Models/SConfig.cs | 46 + src/StardewModdingAPI/Framework/Monitor.cs | 163 + .../Framework/Networking/MessageType.cs | 26 + .../Framework/Networking/ModMessageModel.cs | 72 + .../Framework/Networking/MultiplayerPeer.cs | 84 + .../Networking/MultiplayerPeerMod.cs | 30 + .../Networking/RemoteContextModModel.cs | 15 + .../Networking/RemoteContextModel.cs | 24 + .../Framework/Networking/SGalaxyNetClient.cs | 52 + .../Framework/Networking/SGalaxyNetServer.cs | 75 + .../Framework/Networking/SLidgrenClient.cs | 50 + .../Framework/Networking/SLidgrenServer.cs | 65 + .../Framework/Patching/GamePatcher.cs | 48 + .../Framework/Patching/IHarmonyPatch.cs | 15 + .../Framework/Reflection/CacheEntry.cs | 30 + .../Reflection/InterfaceProxyBuilder.cs | 118 + .../Reflection/InterfaceProxyFactory.cs | 58 + .../Framework/Reflection/ReflectedField.cs | 93 + .../Framework/Reflection/ReflectedMethod.cs | 99 + .../Framework/Reflection/ReflectedProperty.cs | 105 + .../Framework/Reflection/Reflector.cs | 276 + .../Framework/RequestExitDelegate.cs | 7 + .../Framework/RewriteFacades/FarmerMethods.cs | 21 + .../RewriteFacades/FarmerRenderMethods.cs | 16 + .../Framework/RewriteFacades/Game1Methods.cs | 32 + .../RewriteFacades/HUDMessageMethods.cs | 16 + .../RewriteFacades/IClickableMenuMethods.cs | 33 + .../RewriteFacades/MapPageMethods.cs | 17 + .../RewriteFacades/SpriteBatchMethods.cs | 61 + .../RewriteFacades/TextBoxMethods.cs | 18 + src/StardewModdingAPI/Framework/SCore.cs | 1412 +++++ src/StardewModdingAPI/Framework/SGame.cs | 1791 ++++++ .../Framework/SGameConstructorHack.cs | 37 + src/StardewModdingAPI/Framework/SModHooks.cs | 34 + .../Framework/SMultiplayer.cs | 532 ++ .../Framework/Serialisation/ColorConverter.cs | 47 + .../Framework/Serialisation/PointConverter.cs | 43 + .../Serialisation/RectangleConverter.cs | 52 + src/StardewModdingAPI/Framework/Singleton.cs | 10 + .../Comparers/EquatableComparer.cs | 32 + .../Comparers/GenericEqualsComparer.cs | 31 + .../Comparers/ObjectReferenceComparer.cs | 29 + .../FieldWatchers/BaseDisposableWatcher.cs | 36 + .../FieldWatchers/ComparableListWatcher.cs | 83 + .../FieldWatchers/ComparableWatcher.cs | 63 + .../FieldWatchers/NetCollectionWatcher.cs | 94 + .../FieldWatchers/NetDictionaryWatcher.cs | 103 + .../FieldWatchers/NetValueWatcher.cs | 85 + .../ObservableCollectionWatcher.cs | 87 + .../FieldWatchers/WatcherFactory.cs | 87 + .../StateTracking/ICollectionWatcher.cs | 17 + .../StateTracking/IDictionaryWatcher.cs | 7 + .../Framework/StateTracking/IValueWatcher.cs | 15 + .../Framework/StateTracking/IWatcher.cs | 24 + .../StateTracking/LocationTracker.cs | 103 + .../Framework/StateTracking/PlayerTracker.cs | 171 + .../StateTracking/WorldLocationsTracker.cs | 243 + .../Framework/Utilities/ContextHash.cs | 61 + .../Framework/Utilities/Countdown.cs | 44 + .../Framework/WatcherCore.cs | 120 + src/StardewModdingAPI/GamePlatform.cs | 17 + src/StardewModdingAPI/IAssetData.cs | 47 + .../IAssetDataForDictionary.cs | 32 + src/StardewModdingAPI/IAssetDataForImage.cs | 23 + src/StardewModdingAPI/IAssetEditor.cs | 17 + src/StardewModdingAPI/IAssetInfo.cs | 28 + src/StardewModdingAPI/IAssetLoader.cs | 17 + src/StardewModdingAPI/ICommandHelper.cs | 26 + src/StardewModdingAPI/IContentHelper.cs | 68 + src/StardewModdingAPI/IContentPack.cs | 50 + src/StardewModdingAPI/IContentPackHelper.cs | 27 + src/StardewModdingAPI/ICursorPosition.cs | 21 + src/StardewModdingAPI/IDataHelper.cs | 61 + src/StardewModdingAPI/IInputHelper.cs | 21 + src/StardewModdingAPI/IMod.cs | 29 + src/StardewModdingAPI/IModHelper.cs | 98 + src/StardewModdingAPI/IModInfo.cs | 12 + src/StardewModdingAPI/IModLinked.cs | 12 + src/StardewModdingAPI/IModRegistry.cs | 29 + src/StardewModdingAPI/IMonitor.cs | 32 + src/StardewModdingAPI/IMultiplayerHelper.cs | 33 + src/StardewModdingAPI/IMultiplayerPeer.cs | 41 + src/StardewModdingAPI/IMultiplayerPeerMod.cs | 15 + src/StardewModdingAPI/IReflectedField.cs | 26 + src/StardewModdingAPI/IReflectedMethod.cs | 27 + src/StardewModdingAPI/IReflectedProperty.cs | 26 + src/StardewModdingAPI/IReflectionHelper.cs | 51 + src/StardewModdingAPI/ITranslationHelper.cs | 34 + src/StardewModdingAPI/LogLevel.cs | 26 + .../Metadata/CoreAssetPropagator.cs | 750 +++ .../Metadata/InstructionMetadata.cs | 104 + src/StardewModdingAPI/Mod.cs | 53 + src/StardewModdingAPI/PatchMode.cs | 12 + .../Patches/DialogueErrorPatch.cs | 100 + .../Patches/LoadForNewGamePatch.cs | 109 + .../Patches/ObjectErrorPatch.cs | 55 + src/StardewModdingAPI/Program.cs | 154 + .../Properties/AssemblyInfo.cs | 30 + .../Resources/AboutResources.txt | 44 + .../Resources/Resource.designer.cs | 5484 +++++++++++++++++ .../Resources/values/strings.xml | 4 + src/StardewModdingAPI/SButton.cs | 703 +++ src/StardewModdingAPI/SMainActivity.cs | 870 +++ src/StardewModdingAPI/SemanticVersion.cs | 170 + .../StardewModdingAPI.csproj | 414 ++ src/StardewModdingAPI/Translation.cs | 154 + src/StardewModdingAPI/Utilities/SDate.cs | 268 + src/StardewModdingAPI/icon.ico | Bin 0 -> 15086 bytes src/StardewModdingAPI/steam_appid.txt | 1 + 295 files changed, 30627 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/SMAPI/Framework/ModLoading/Rewriters/TypeFieldToTypeFieldRewriter.cs create mode 100644 src/SMAPI/SMAPI/Framework/RewriteFacades/FarmerMethods.cs create mode 100644 src/SMAPI/SMAPI/Framework/RewriteFacades/FarmerRendererMethods.cs create mode 100644 src/SMAPI/SMAPI/Framework/RewriteFacades/Game1Methods.cs create mode 100644 src/SMAPI/SMAPI/Framework/RewriteFacades/HUDMessageMethods.cs create mode 100644 src/SMAPI/SMAPI/Framework/RewriteFacades/IClickableMenuMethods.cs create mode 100644 src/SMAPI/SMAPI/Framework/RewriteFacades/MapPageMethods.cs create mode 100644 src/SMAPI/SMAPI/Framework/RewriteFacades/TextBoxMethods.cs create mode 100644 src/SMAPI/StardewModdingAPI.csproj create mode 100644 src/SMAPI/StardewModdingAPI.sln create mode 100644 src/StardewModdingAPI/Constants.cs create mode 100644 src/StardewModdingAPI/ContentSource.cs create mode 100644 src/StardewModdingAPI/Context.cs create mode 100644 src/StardewModdingAPI/Enums/LoadStage.cs create mode 100644 src/StardewModdingAPI/Enums/SkillType.cs create mode 100644 src/StardewModdingAPI/Events/BuildingListChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/ButtonPressedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/ButtonReleasedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/ChangeType.cs create mode 100644 src/StardewModdingAPI/Events/ContentEvents.cs create mode 100644 src/StardewModdingAPI/Events/ControlEvents.cs create mode 100644 src/StardewModdingAPI/Events/CursorMovedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/DayEndingEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/DayStartedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/DebrisListChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsInput.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsIntChanged.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsKeyPressed.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsLevelUp.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsLocationBuildingsChanged.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsLocationsChanged.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsPlayerWarped.cs create mode 100644 src/StardewModdingAPI/Events/EventArgsValueChanged.cs create mode 100644 src/StardewModdingAPI/Events/GameEvents.cs create mode 100644 src/StardewModdingAPI/Events/GameLaunchedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/GraphicsEvents.cs create mode 100644 src/StardewModdingAPI/Events/IDisplayEvents.cs create mode 100644 src/StardewModdingAPI/Events/IGameLoopEvents.cs create mode 100644 src/StardewModdingAPI/Events/IInputEvents.cs create mode 100644 src/StardewModdingAPI/Events/IModEvents.cs create mode 100644 src/StardewModdingAPI/Events/IMultiplayerEvents.cs create mode 100644 src/StardewModdingAPI/Events/IPlayerEvents.cs create mode 100644 src/StardewModdingAPI/Events/ISpecialisedEvents.cs create mode 100644 src/StardewModdingAPI/Events/IWorldEvents.cs create mode 100644 src/StardewModdingAPI/Events/InputEvents.cs create mode 100644 src/StardewModdingAPI/Events/InventoryChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/ItemStackChange.cs create mode 100644 src/StardewModdingAPI/Events/ItemStackSizeChange.cs create mode 100644 src/StardewModdingAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/LevelChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/LoadStageChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/LocationEvents.cs create mode 100644 src/StardewModdingAPI/Events/LocationListChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/MenuChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/MenuEvents.cs create mode 100644 src/StardewModdingAPI/Events/MineEvents.cs create mode 100644 src/StardewModdingAPI/Events/ModMessageReceivedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/MouseWheelScrolledEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/MultiplayerEvents.cs create mode 100644 src/StardewModdingAPI/Events/NpcListChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/ObjectListChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/OneSecondUpdateTickedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/OneSecondUpdateTickingEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/PeerContextReceivedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/PeerDisconnectedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/PlayerEvents.cs create mode 100644 src/StardewModdingAPI/Events/RenderedActiveMenuEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/RenderedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/RenderedHudEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/RenderedWorldEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/RenderingActiveMenuEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/RenderingEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/RenderingHudEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/RenderingWorldEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/ReturnedToTitleEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/SaveCreatedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/SaveCreatingEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/SaveEvents.cs create mode 100644 src/StardewModdingAPI/Events/SaveLoadedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/SavedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/SavingEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/SpecialisedEvents.cs create mode 100644 src/StardewModdingAPI/Events/TerrainFeatureListChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/TimeChangedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/TimeEvents.cs create mode 100644 src/StardewModdingAPI/Events/UnvalidatedUpdateTickedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/UnvalidatedUpdateTickingEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/UpdateTickedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/UpdateTickingEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/WarpedEventArgs.cs create mode 100644 src/StardewModdingAPI/Events/WindowResizedEventArgs.cs create mode 100644 src/StardewModdingAPI/Framework/Command.cs create mode 100644 src/StardewModdingAPI/Framework/CommandManager.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetData.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetInfo.cs create mode 100644 src/StardewModdingAPI/Framework/Content/ContentCache.cs create mode 100644 src/StardewModdingAPI/Framework/ContentCoordinator.cs create mode 100644 src/StardewModdingAPI/Framework/ContentManagers/BaseContentManager.cs create mode 100644 src/StardewModdingAPI/Framework/ContentManagers/GameContentManager.cs create mode 100644 src/StardewModdingAPI/Framework/ContentManagers/IContentManager.cs create mode 100644 src/StardewModdingAPI/Framework/ContentManagers/ModContentManager.cs create mode 100644 src/StardewModdingAPI/Framework/ContentPack.cs create mode 100644 src/StardewModdingAPI/Framework/CursorPosition.cs create mode 100644 src/StardewModdingAPI/Framework/DeprecationLevel.cs create mode 100644 src/StardewModdingAPI/Framework/DeprecationManager.cs create mode 100644 src/StardewModdingAPI/Framework/DeprecationWarning.cs create mode 100644 src/StardewModdingAPI/Framework/Events/EventManager.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ManagedEvent.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ManagedEventBase.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ModDisplayEvents.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ModEvents.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ModEventsBase.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ModGameLoopEvents.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ModInputEvents.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ModMultiplayerEvents.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ModPlayerEvents.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ModSpecialisedEvents.cs create mode 100644 src/StardewModdingAPI/Framework/Events/ModWorldEvents.cs create mode 100644 src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs create mode 100644 src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs create mode 100644 src/StardewModdingAPI/Framework/GameVersion.cs create mode 100644 src/StardewModdingAPI/Framework/IModMetadata.cs create mode 100644 src/StardewModdingAPI/Framework/Input/GamePadStateBuilder.cs create mode 100644 src/StardewModdingAPI/Framework/Input/InputStatus.cs create mode 100644 src/StardewModdingAPI/Framework/Input/SInputState.cs create mode 100644 src/StardewModdingAPI/Framework/InternalExtensions.cs create mode 100644 src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs create mode 100644 src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs create mode 100644 src/StardewModdingAPI/Framework/Logging/LogFileManager.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ContentPackHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/DataHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/InputHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/MultiplayerHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeFieldToAnotherTypeFieldRewriter.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/TypeReferenceComparer.cs create mode 100644 src/StardewModdingAPI/Framework/ModRegistry.cs create mode 100644 src/StardewModdingAPI/Framework/Models/ModFolderExport.cs create mode 100644 src/StardewModdingAPI/Framework/Models/SConfig.cs create mode 100644 src/StardewModdingAPI/Framework/Monitor.cs create mode 100644 src/StardewModdingAPI/Framework/Networking/MessageType.cs create mode 100644 src/StardewModdingAPI/Framework/Networking/ModMessageModel.cs create mode 100644 src/StardewModdingAPI/Framework/Networking/MultiplayerPeer.cs create mode 100644 src/StardewModdingAPI/Framework/Networking/MultiplayerPeerMod.cs create mode 100644 src/StardewModdingAPI/Framework/Networking/RemoteContextModModel.cs create mode 100644 src/StardewModdingAPI/Framework/Networking/RemoteContextModel.cs create mode 100644 src/StardewModdingAPI/Framework/Networking/SGalaxyNetClient.cs create mode 100644 src/StardewModdingAPI/Framework/Networking/SGalaxyNetServer.cs create mode 100644 src/StardewModdingAPI/Framework/Networking/SLidgrenClient.cs create mode 100644 src/StardewModdingAPI/Framework/Networking/SLidgrenServer.cs create mode 100644 src/StardewModdingAPI/Framework/Patching/GamePatcher.cs create mode 100644 src/StardewModdingAPI/Framework/Patching/IHarmonyPatch.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/InterfaceProxyBuilder.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/InterfaceProxyFactory.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/ReflectedField.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/ReflectedMethod.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/ReflectedProperty.cs create mode 100644 src/StardewModdingAPI/Framework/Reflection/Reflector.cs create mode 100644 src/StardewModdingAPI/Framework/RequestExitDelegate.cs create mode 100644 src/StardewModdingAPI/Framework/RewriteFacades/FarmerMethods.cs create mode 100644 src/StardewModdingAPI/Framework/RewriteFacades/FarmerRenderMethods.cs create mode 100644 src/StardewModdingAPI/Framework/RewriteFacades/Game1Methods.cs create mode 100644 src/StardewModdingAPI/Framework/RewriteFacades/HUDMessageMethods.cs create mode 100644 src/StardewModdingAPI/Framework/RewriteFacades/IClickableMenuMethods.cs create mode 100644 src/StardewModdingAPI/Framework/RewriteFacades/MapPageMethods.cs create mode 100644 src/StardewModdingAPI/Framework/RewriteFacades/SpriteBatchMethods.cs create mode 100644 src/StardewModdingAPI/Framework/RewriteFacades/TextBoxMethods.cs create mode 100644 src/StardewModdingAPI/Framework/SCore.cs create mode 100644 src/StardewModdingAPI/Framework/SGame.cs create mode 100644 src/StardewModdingAPI/Framework/SGameConstructorHack.cs create mode 100644 src/StardewModdingAPI/Framework/SModHooks.cs create mode 100644 src/StardewModdingAPI/Framework/SMultiplayer.cs create mode 100644 src/StardewModdingAPI/Framework/Serialisation/ColorConverter.cs create mode 100644 src/StardewModdingAPI/Framework/Serialisation/PointConverter.cs create mode 100644 src/StardewModdingAPI/Framework/Serialisation/RectangleConverter.cs create mode 100644 src/StardewModdingAPI/Framework/Singleton.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/Comparers/EquatableComparer.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/ICollectionWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/IDictionaryWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/IValueWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/IWatcher.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/LocationTracker.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/PlayerTracker.cs create mode 100644 src/StardewModdingAPI/Framework/StateTracking/WorldLocationsTracker.cs create mode 100644 src/StardewModdingAPI/Framework/Utilities/ContextHash.cs create mode 100644 src/StardewModdingAPI/Framework/Utilities/Countdown.cs create mode 100644 src/StardewModdingAPI/Framework/WatcherCore.cs create mode 100644 src/StardewModdingAPI/GamePlatform.cs create mode 100644 src/StardewModdingAPI/IAssetData.cs create mode 100644 src/StardewModdingAPI/IAssetDataForDictionary.cs create mode 100644 src/StardewModdingAPI/IAssetDataForImage.cs create mode 100644 src/StardewModdingAPI/IAssetEditor.cs create mode 100644 src/StardewModdingAPI/IAssetInfo.cs create mode 100644 src/StardewModdingAPI/IAssetLoader.cs create mode 100644 src/StardewModdingAPI/ICommandHelper.cs create mode 100644 src/StardewModdingAPI/IContentHelper.cs create mode 100644 src/StardewModdingAPI/IContentPack.cs create mode 100644 src/StardewModdingAPI/IContentPackHelper.cs create mode 100644 src/StardewModdingAPI/ICursorPosition.cs create mode 100644 src/StardewModdingAPI/IDataHelper.cs create mode 100644 src/StardewModdingAPI/IInputHelper.cs create mode 100644 src/StardewModdingAPI/IMod.cs create mode 100644 src/StardewModdingAPI/IModHelper.cs create mode 100644 src/StardewModdingAPI/IModInfo.cs create mode 100644 src/StardewModdingAPI/IModLinked.cs create mode 100644 src/StardewModdingAPI/IModRegistry.cs create mode 100644 src/StardewModdingAPI/IMonitor.cs create mode 100644 src/StardewModdingAPI/IMultiplayerHelper.cs create mode 100644 src/StardewModdingAPI/IMultiplayerPeer.cs create mode 100644 src/StardewModdingAPI/IMultiplayerPeerMod.cs create mode 100644 src/StardewModdingAPI/IReflectedField.cs create mode 100644 src/StardewModdingAPI/IReflectedMethod.cs create mode 100644 src/StardewModdingAPI/IReflectedProperty.cs create mode 100644 src/StardewModdingAPI/IReflectionHelper.cs create mode 100644 src/StardewModdingAPI/ITranslationHelper.cs create mode 100644 src/StardewModdingAPI/LogLevel.cs create mode 100644 src/StardewModdingAPI/Metadata/CoreAssetPropagator.cs create mode 100644 src/StardewModdingAPI/Metadata/InstructionMetadata.cs create mode 100644 src/StardewModdingAPI/Mod.cs create mode 100644 src/StardewModdingAPI/PatchMode.cs create mode 100644 src/StardewModdingAPI/Patches/DialogueErrorPatch.cs create mode 100644 src/StardewModdingAPI/Patches/LoadForNewGamePatch.cs create mode 100644 src/StardewModdingAPI/Patches/ObjectErrorPatch.cs create mode 100644 src/StardewModdingAPI/Program.cs create mode 100644 src/StardewModdingAPI/Properties/AssemblyInfo.cs create mode 100644 src/StardewModdingAPI/Resources/AboutResources.txt create mode 100644 src/StardewModdingAPI/Resources/Resource.designer.cs create mode 100644 src/StardewModdingAPI/Resources/values/strings.xml create mode 100644 src/StardewModdingAPI/SButton.cs create mode 100644 src/StardewModdingAPI/SMainActivity.cs create mode 100644 src/StardewModdingAPI/SemanticVersion.cs create mode 100644 src/StardewModdingAPI/StardewModdingAPI.csproj create mode 100644 src/StardewModdingAPI/Translation.cs create mode 100644 src/StardewModdingAPI/Utilities/SDate.cs create mode 100644 src/StardewModdingAPI/icon.ico create mode 100644 src/StardewModdingAPI/steam_appid.txt diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 1eee5088..9db20e45 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -57,6 +57,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMAPI.Mods.VirtualKeyboard" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Loader", "Loader\Loader.csproj", "{45D7D2FB-6B70-45D1-A595-6E289D6A3468}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "StardewModdingAPI\StardewModdingAPI.csproj", "{9898B56E-51EB-40CF-8B1F-ACEB4B6397A7}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index ea3c6b4a..9f8989a6 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -160,7 +160,7 @@ namespace StardewModdingAPI internal static string ModsPath { get; set; } /// The game's current semantic version. - internal static ISemanticVersion GameVersion { get; } = new GameVersion(Game1.version); + internal static ISemanticVersion GameVersion { get; } = new GameVersion("1.3.36"); /// The target game platform as a SMAPI toolkit constant. internal static Platform Platform { get; } = (Platform)Constants.TargetPlatform; diff --git a/src/SMAPI/SMAPI/Framework/ModLoading/Rewriters/TypeFieldToTypeFieldRewriter.cs b/src/SMAPI/SMAPI/Framework/ModLoading/Rewriters/TypeFieldToTypeFieldRewriter.cs new file mode 100644 index 00000000..c91ae93d --- /dev/null +++ b/src/SMAPI/SMAPI/Framework/ModLoading/Rewriters/TypeFieldToTypeFieldRewriter.cs @@ -0,0 +1,78 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Finders; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites field references into property references. + internal class TypeFieldToTypeFieldRewriter : FieldFinder + { + /********* + ** Fields + *********/ + /// The type whose field to which references should be rewritten. + private readonly Type Type; + + /// The type whose field to which references should be rewritten to. + private readonly Type ToType; + + /// The property name. + private readonly string PropertyName; + + private readonly IMonitor Monitor; + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type whose field to which references should be rewritten. + /// The field name to rewrite. + /// The property name (if different). + public TypeFieldToTypeFieldRewriter(Type type, Type toType, string fieldName, string propertyName, IMonitor monitor) + : base(type.FullName, fieldName, InstructionHandleResult.None) + { + this.Monitor = monitor; + this.Type = type; + this.ToType = toType; + this.PropertyName = propertyName; + } + + /// Construct an instance. + /// The type whose field to which references should be rewritten. + /// The field name to rewrite. + public TypeFieldToTypeFieldRewriter(Type type, Type toType, string fieldName, IMonitor monitor) + : this(type, toType, fieldName, fieldName, monitor) { } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + //Instruction: IL_0025: ldsfld StardewValley.GameLocation StardewValley.Game1::currentLocation + string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; + try + { + //MethodReference propertyRef = module.ImportReference(this.ToType.GetMethod($"{methodPrefix}_{this.PropertyName}")); + + MethodReference method = module.ImportReference(this.ToType.GetMethod($"{methodPrefix}_{this.PropertyName}")); + this.Monitor.Log("Method Ref: " + method.ToString()); + + cil.Replace(instruction, cil.Create(OpCodes.Call, method)); + } + catch (Exception e) + { + this.Monitor.Log(e.Message); + } + + + return InstructionHandleResult.Rewritten; + } + } +} diff --git a/src/SMAPI/SMAPI/Framework/RewriteFacades/FarmerMethods.cs b/src/SMAPI/SMAPI/Framework/RewriteFacades/FarmerMethods.cs new file mode 100644 index 00000000..bb10627c --- /dev/null +++ b/src/SMAPI/SMAPI/Framework/RewriteFacades/FarmerMethods.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.SMAPI.Framework.RewriteFacades +{ + class FarmerMethods : Farmer + { + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new bool couldInventoryAcceptThisItem(Item item) + { + return base.couldInventoryAcceptThisItem(item, true); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new bool addItemToInventoryBool(Item item) + { + return base.addItemToInventoryBool(item, false); + } + } +} diff --git a/src/SMAPI/SMAPI/Framework/RewriteFacades/FarmerRendererMethods.cs b/src/SMAPI/SMAPI/Framework/RewriteFacades/FarmerRendererMethods.cs new file mode 100644 index 00000000..2104f316 --- /dev/null +++ b/src/SMAPI/SMAPI/Framework/RewriteFacades/FarmerRendererMethods.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.SMAPI.Framework.RewriteFacades +{ + public class FarmerRendererMethods : FarmerRenderer + { + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void drawMiniPortrat(SpriteBatch b, Vector2 position, float layerDepth, float scale, int facingDirection, Farmer who) + { + base.drawMiniPortrat(b, position, layerDepth, scale, facingDirection, who); + } + } +} diff --git a/src/SMAPI/SMAPI/Framework/RewriteFacades/Game1Methods.cs b/src/SMAPI/SMAPI/Framework/RewriteFacades/Game1Methods.cs new file mode 100644 index 00000000..01a0e48f --- /dev/null +++ b/src/SMAPI/SMAPI/Framework/RewriteFacades/Game1Methods.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.SMAPI.Framework.RewriteFacades +{ + public class Game1Methods : Game1 + { + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new string parseText(string text, SpriteFont whichFont, int width) + { + return parseText(text, whichFont, width, 1); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void warpFarmer(LocationRequest locationRequest, int tileX, int tileY, int facingDirectionAfterWarp) + { + warpFarmer(locationRequest, tileX, tileY, facingDirectionAfterWarp, true, false); + } + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void warpFarmer(string locationName, int tileX, int tileY, bool flip) + { + warpFarmer(locationName, tileX, tileY, flip ? ((player.FacingDirection + 2) % 4) : player.FacingDirection); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void warpFarmer(string locationName, int tileX, int tileY, int facingDirectionAfterWarp) + { + warpFarmer(locationName, tileX, tileY, facingDirectionAfterWarp, false, true, false); + } + } +} diff --git a/src/SMAPI/SMAPI/Framework/RewriteFacades/HUDMessageMethods.cs b/src/SMAPI/SMAPI/Framework/RewriteFacades/HUDMessageMethods.cs new file mode 100644 index 00000000..60a6f3f3 --- /dev/null +++ b/src/SMAPI/SMAPI/Framework/RewriteFacades/HUDMessageMethods.cs @@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.SMAPI.Framework.RewriteFacades +{ + public class HUDMessageMethods : HUDMessage + { + public HUDMessageMethods(string message, int whatType) + : base(message, whatType, -1) + { + } + + } +} diff --git a/src/SMAPI/SMAPI/Framework/RewriteFacades/IClickableMenuMethods.cs b/src/SMAPI/SMAPI/Framework/RewriteFacades/IClickableMenuMethods.cs new file mode 100644 index 00000000..1c82e515 --- /dev/null +++ b/src/SMAPI/SMAPI/Framework/RewriteFacades/IClickableMenuMethods.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.SMAPI.Framework.RewriteFacades +{ + public class IClickableMenuMethods : IClickableMenu + { + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void drawHoverText(SpriteBatch b, string text, SpriteFont font, int xOffset = 0, int yOffset = 0, int moneyAmounttoDisplayAtBottom = -1, string boldTitleText = null, int healAmountToDisplay = -1, string[] buffIconsToDsiplay = null, Item hoveredItem = null, int currencySymbol = 0, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, int overideX = -1, int overrideY = -1, float alpha = 1, CraftingRecipe craftingIngrediants = null) + { + drawHoverText(b, text, font, xOffset, yOffset, moneyAmounttoDisplayAtBottom, boldTitleText, healAmountToDisplay, buffIconsToDsiplay, hoveredItem, currencySymbol, extraItemToShowIndex, extraItemToShowAmount, overideX, overrideY, alpha, craftingIngrediants, -1, 80, -1); + } + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void drawTextureBox(SpriteBatch b, Texture2D texture, Microsoft.Xna.Framework.Rectangle sourceRect, int x, int y, int width, int height, Color color) + { + drawTextureBox(b, texture, sourceRect, x, y, width, height, color, 1, true, false); + } + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void drawTextureBox(SpriteBatch b, Texture2D texture, Microsoft.Xna.Framework.Rectangle sourceRect, int x, int y, int width, int height, Color color, float scale) + { + drawTextureBox(b, texture, sourceRect, x, y, width, height, color, scale, true, false); + } + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void drawTextureBox(SpriteBatch b, Texture2D texture, Microsoft.Xna.Framework.Rectangle sourceRect, int x, int y, int width, int height, Color color, float scale, bool drawShadow) + { + drawTextureBox(b, texture, sourceRect, x, y, width, height, color, scale, drawShadow, false); + } + + } +} diff --git a/src/SMAPI/SMAPI/Framework/RewriteFacades/MapPageMethods.cs b/src/SMAPI/SMAPI/Framework/RewriteFacades/MapPageMethods.cs new file mode 100644 index 00000000..8c98d11d --- /dev/null +++ b/src/SMAPI/SMAPI/Framework/RewriteFacades/MapPageMethods.cs @@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.SMAPI.Framework.RewriteFacades +{ + public class MapPageMethods : MapPage + { + public MapPageMethods(int x, int y, int width, int height) + : base(x, y, width, height, 1f, 1f) + { + } + + } +} diff --git a/src/SMAPI/SMAPI/Framework/RewriteFacades/TextBoxMethods.cs b/src/SMAPI/SMAPI/Framework/RewriteFacades/TextBoxMethods.cs new file mode 100644 index 00000000..279e08b1 --- /dev/null +++ b/src/SMAPI/SMAPI/Framework/RewriteFacades/TextBoxMethods.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Menus; + +#pragma warning disable 1591 // missing documentation +namespace StardewModdingAPI.SMAPI.Framework.RewriteFacades +{ + public class TextBoxMethods : TextBox + { + public TextBoxMethods(Texture2D textboxTexture, Texture2D caretTexture, SpriteFont font, Color textColor) + : base(textboxTexture, caretTexture, font, textColor, true, false) + { + + } + } +} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/SMAPI/StardewModdingAPI.sln b/src/SMAPI/StardewModdingAPI.sln new file mode 100644 index 00000000..7e523334 --- /dev/null +++ b/src/SMAPI/StardewModdingAPI.sln @@ -0,0 +1,26 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.156 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}" +EndProject +Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\SMAPI.Internal\SMAPI.Internal.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 + EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.ActiveCfg = Debug|x86 + {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.ActiveCfg = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {82DD83B8-40C3-4BEB-82EA-84A61392732D} + EndGlobalSection +EndGlobal diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs new file mode 100644 index 00000000..fedc4ce4 --- /dev/null +++ b/src/StardewModdingAPI/Constants.cs @@ -0,0 +1,238 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using StardewModdingAPI.Enums; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Internal; +using StardewValley; + +namespace StardewModdingAPI +{ + /// Contains SMAPI's constants and assumptions. + public static class Constants + { + /********* + ** Accessors + *********/ + /**** + ** Public + ****/ + /// SMAPI's current semantic version. + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.11.2"); + + /// The minimum supported version of Stardew Valley. + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.36"); + + /// The maximum supported version of Stardew Valley. + public static ISemanticVersion MaximumGameVersion { get; } = new GameVersion("1.3.36"); + + /// The target game platform. + public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform; + + /// The path to the game folder. + public static string ExecutionPath { get; } = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "StardewValley/smapi-internal"); + + /// The directory path containing Stardew Valley's app data. + public static string DataPath { get; } = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "StardewValley"); + + /// The directory path in which error logs should be stored. + public static string LogDir { get; } = Path.Combine(Constants.DataPath, "ErrorLogs"); + + /// The directory path where all saves are stored. + public static string SavesPath { get; } = Path.Combine(Constants.DataPath, "Saves"); + + /// The name of the current save folder (if save info is available, regardless of whether the save file exists yet). + public static string SaveFolderName + { + get + { + return Constants.GetSaveFolderName() +#if SMAPI_3_0_STRICT + ; +#else + ?? ""; +#endif + } + } + + /// The absolute path to the current save folder (if save info is available and the save file exists). + public static string CurrentSavePath + { + get + { + return Constants.GetSaveFolderPathIfExists() +#if SMAPI_3_0_STRICT + ; +#else + ?? ""; +#endif + } + } + + /**** + ** Internal + ****/ + /// The URL of the SMAPI home page. + internal const string HomePageUrl = "https://smapi.io"; + + /// The absolute path to the folder containing SMAPI's internal files. + internal static readonly string InternalFilesPath = Program.DllSearchPath; + + /// The file path for the SMAPI configuration file. + internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.config.json"); + + /// The file path for the SMAPI metadata file. + internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.metadata.json"); + + /// The filename prefix used for all SMAPI logs. + internal static string LogNamePrefix { get; } = "SMAPI-"; + + /// The filename for SMAPI's main log, excluding the . + internal static string LogFilename { get; } = $"{Constants.LogNamePrefix}latest"; + + /// The filename extension for SMAPI log files. + internal static string LogExtension { get; } = "txt"; + + /// The file path for the log containing the previous fatal crash, if any. + internal static string FatalCrashLog => Path.Combine(Constants.LogDir, "SMAPI-crash.txt"); + + /// The file path which stores a fatal crash message for the next run. + internal static string FatalCrashMarker => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.crash.marker"); + + /// The file path which stores the detected update version for the next run. + internal static string UpdateMarker => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.update.marker"); + + /// The default full path to search for mods. + internal static string DefaultModsPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); + + /// The actual full path to search for mods. + internal static string ModsPath { get; set; } + + /// The game's current semantic version. + internal static ISemanticVersion GameVersion { get; } = new GameVersion("1.3.36"); + + /// The target game platform. + internal static Platform Platform { get; } = EnvironmentUtility.DetectPlatform(); + + /// The game's assembly name. + internal static string GameAssemblyName => Constants.Platform == Platform.Windows ? "Stardew Valley" : "StardewValley"; + + + /********* + ** Internal methods + *********/ + /// Get the SMAPI version to recommend for an older game version, if any. + /// The game version to search. + /// Returns the compatible SMAPI version, or null if none was found. + internal static ISemanticVersion GetCompatibleApiVersion(ISemanticVersion version) + { + switch (version.ToString()) + { + case "1.3.28": + return new SemanticVersion(2, 7, 0); + + case "1.2.30": + case "1.2.31": + case "1.2.32": + case "1.2.33": + return new SemanticVersion(2, 5, 5); + } + + return null; + } + + /// Get metadata for mapping assemblies to the current platform. + /// The target game platform. + internal static PlatformAssemblyMap GetAssemblyMap(Platform targetPlatform) + { + // get assembly changes needed for platform + string[] removeAssemblyReferences; + Assembly[] targetAssemblies; + switch (targetPlatform) + { + case Platform.Linux: + case Platform.Mac: + removeAssemblyReferences = new[] + { + "Netcode", + "Stardew Valley", + "Microsoft.Xna.Framework", + "Microsoft.Xna.Framework.Game", + "Microsoft.Xna.Framework.Graphics", + "Microsoft.Xna.Framework.Xact" + }; + targetAssemblies = new[] + { + typeof(StardewValley.Game1).Assembly, // note: includes Netcode types on Linux/Mac + typeof(Microsoft.Xna.Framework.Vector2).Assembly + }; + break; + + case Platform.Windows: + removeAssemblyReferences = new[] + { + "StardewValley", + "MonoGame.Framework" + }; + targetAssemblies = new[] + { + typeof(Netcode.NetBool).Assembly, + typeof(StardewValley.Game1).Assembly, + typeof(Microsoft.Xna.Framework.Vector2).Assembly, + typeof(Microsoft.Xna.Framework.Game).Assembly, + typeof(Microsoft.Xna.Framework.Graphics.SpriteBatch).Assembly + }; + break; + + default: + throw new InvalidOperationException($"Unknown target platform '{targetPlatform}'."); + } + + return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); + } + + + /********* + ** Private methods + *********/ + /// Get the name of the save folder, if any. + internal static string GetSaveFolderName() + { + // save not available + if (Context.LoadStage == LoadStage.None) + return null; + + // get basic info + string playerName; + ulong saveID; + if (Context.LoadStage == LoadStage.SaveParsed) + { + playerName = SaveGame.loaded.player.Name; + saveID = SaveGame.loaded.uniqueIDForThisGame; + } + else + { + playerName = Game1.player.Name; + saveID = Game1.uniqueIDForThisGame; + } + + // build folder name + return $"{new string(playerName.Where(char.IsLetterOrDigit).ToArray())}_{saveID}"; + } + + /// Get the path to the current save folder, if any. + internal static string GetSaveFolderPathIfExists() + { + string folderName = Constants.GetSaveFolderName(); + if (folderName == null) + return null; + + string path = Path.Combine(Constants.SavesPath, folderName); + return Directory.Exists(path) + ? path + : null; + } + } +} diff --git a/src/StardewModdingAPI/ContentSource.cs b/src/StardewModdingAPI/ContentSource.cs new file mode 100644 index 00000000..35c8bc21 --- /dev/null +++ b/src/StardewModdingAPI/ContentSource.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// Specifies a source containing content that can be loaded. + public enum ContentSource + { + /// Assets in the game's content manager (i.e. XNBs in the game's content folder). + GameContent, + + /// XNB files in the current mod's folder. + ModFolder + } +} diff --git a/src/StardewModdingAPI/Context.cs b/src/StardewModdingAPI/Context.cs new file mode 100644 index 00000000..1cdef7f1 --- /dev/null +++ b/src/StardewModdingAPI/Context.cs @@ -0,0 +1,47 @@ +using StardewModdingAPI.Enums; +using StardewModdingAPI.Events; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI +{ + /// Provides information about the current game state. + public static class Context + { + /********* + ** Accessors + *********/ + /**** + ** Public + ****/ + /// Whether the player has loaded a save and the world has finished initialising. + public static bool IsWorldReady { get; internal set; } + + /// Whether is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc). + public static bool IsPlayerFree => Context.IsWorldReady && Game1.currentLocation != null && Game1.activeClickableMenu == null && !Game1.dialogueUp && (!Game1.eventUp || Game1.isFestival()); + + /// Whether is true and the player is free to move (e.g. not using a tool). + public static bool CanPlayerMove => Context.IsPlayerFree && Game1.player.CanMove; + + /// Whether the game is currently running the draw loop. This isn't relevant to most mods, since you should use events to draw to the screen. + public static bool IsInDrawLoop { get; internal set; } + + /// Whether and the player loaded the save in multiplayer mode (regardless of whether any other players are connected). + public static bool IsMultiplayer => Context.IsWorldReady && Game1.multiplayerMode != Game1.singlePlayer; + + /// Whether and the current player is the main player. This is always true in single-player, and true when hosting in multiplayer. + public static bool IsMainPlayer => Context.IsWorldReady && Game1.IsMasterGame; + + /**** + ** Internal + ****/ + /// Whether a player save has been loaded. + internal static bool IsSaveLoaded => Game1.hasLoadedGame && !(Game1.activeClickableMenu is TitleMenu); + + /// Whether the game is currently writing to the save file. + internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something + + /// The current stage in the game's loading process. + internal static LoadStage LoadStage { get; set; } + } +} diff --git a/src/StardewModdingAPI/Enums/LoadStage.cs b/src/StardewModdingAPI/Enums/LoadStage.cs new file mode 100644 index 00000000..6ff7de4f --- /dev/null +++ b/src/StardewModdingAPI/Enums/LoadStage.cs @@ -0,0 +1,36 @@ +namespace StardewModdingAPI.Enums +{ + /// A low-level stage in the game's loading process. + public enum LoadStage + { + /// A save is not loaded or loading. + None, + + /// The game is creating a new save slot, and has initialised the basic save info. + CreatedBasicInfo, + + /// The game is creating a new save slot, and has initialised the in-game locations. + CreatedLocations, + + /// The game is creating a new save slot, and has created the physical save files. + CreatedSaveFile, + + /// The game is loading a save slot, and has read the raw save data into . Not applicable when connecting to a multiplayer host. This is equivalent to value 20. + SaveParsed, + + /// The game is loading a save slot, and has applied the basic save info (including player data). Not applicable when connecting to a multiplayer host. Note that some basic info (like daily luck) is not initialised at this point. This is equivalent to value 36. + SaveLoadedBasicInfo, + + /// The game is loading a save slot, and has applied the in-game location data. Not applicable when connecting to a multiplayer host. This is equivalent to value 50. + SaveLoadedLocations, + + /// The final metadata has been loaded from the save file. This happens before the game applies problem fixes, checks for achievements, starts music, etc. Not applicable when connecting to a multiplayer host. + Preloaded, + + /// The save is fully loaded, but the world may not be fully initialised yet. + Loaded, + + /// The save is fully loaded, the world has been initialised, and is now true. + Ready + } +} diff --git a/src/StardewModdingAPI/Enums/SkillType.cs b/src/StardewModdingAPI/Enums/SkillType.cs new file mode 100644 index 00000000..10518ec9 --- /dev/null +++ b/src/StardewModdingAPI/Enums/SkillType.cs @@ -0,0 +1,26 @@ +using StardewValley; + +namespace StardewModdingAPI.Enums +{ + /// The player skill types. + public enum SkillType + { + /// The combat skill. + Combat = Farmer.combatSkill, + + /// The farming skill. + Farming = Farmer.farmingSkill, + + /// The fishing skill. + Fishing = Farmer.fishingSkill, + + /// The foraging skill. + Foraging = Farmer.foragingSkill, + + /// The mining skill. + Mining = Farmer.miningSkill, + + /// The luck skill. + Luck = Farmer.luckSkill + } +} diff --git a/src/StardewModdingAPI/Events/BuildingListChangedEventArgs.cs b/src/StardewModdingAPI/Events/BuildingListChangedEventArgs.cs new file mode 100644 index 00000000..74f37710 --- /dev/null +++ b/src/StardewModdingAPI/Events/BuildingListChangedEventArgs.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Buildings; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class BuildingListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The buildings added to the location. + public IEnumerable Added { get; } + + /// The buildings removed from the location. + public IEnumerable Removed { get; } + + /// Whether this is the location containing the local player. + public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The buildings added to the location. + /// The buildings removed from the location. + internal BuildingListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/StardewModdingAPI/Events/ButtonPressedEventArgs.cs b/src/StardewModdingAPI/Events/ButtonPressedEventArgs.cs new file mode 100644 index 00000000..5d922666 --- /dev/null +++ b/src/StardewModdingAPI/Events/ButtonPressedEventArgs.cs @@ -0,0 +1,60 @@ +using System; +using StardewModdingAPI.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is pressed. + public class ButtonPressedEventArgs : EventArgs + { + /********* + ** Fields + *********/ + /// The game's current input state. + private readonly SInputState InputState; + + + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// The game's current input state. + internal ButtonPressedEventArgs(SButton button, ICursorPosition cursor, SInputState inputState) + { + this.Button = button; + this.Cursor = cursor; + this.InputState = inputState; + } + + /// Whether a mod has indicated the key was already handled, so the game should handle it. + public bool IsSuppressed() + { + return this.IsSuppressed(this.Button); + } + + /// Whether a mod has indicated the key was already handled, so the game should handle it. + /// The button to check. + public bool IsSuppressed(SButton button) + { + return this.InputState.SuppressButtons.Contains(button); + } + + /// Get whether a given button was pressed or held. + /// The button to check. + public bool IsDown(SButton button) + { + return this.InputState.IsDown(button); + } + } +} diff --git a/src/StardewModdingAPI/Events/ButtonReleasedEventArgs.cs b/src/StardewModdingAPI/Events/ButtonReleasedEventArgs.cs new file mode 100644 index 00000000..f5282230 --- /dev/null +++ b/src/StardewModdingAPI/Events/ButtonReleasedEventArgs.cs @@ -0,0 +1,60 @@ +using System; +using StardewModdingAPI.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is released. + public class ButtonReleasedEventArgs : EventArgs + { + /********* + ** Fields + *********/ + /// The game's current input state. + private readonly SInputState InputState; + + + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// The game's current input state. + internal ButtonReleasedEventArgs(SButton button, ICursorPosition cursor, SInputState inputState) + { + this.Button = button; + this.Cursor = cursor; + this.InputState = inputState; + } + + /// Whether a mod has indicated the key was already handled, so the game should handle it. + public bool IsSuppressed() + { + return this.IsSuppressed(this.Button); + } + + /// Whether a mod has indicated the key was already handled, so the game should handle it. + /// The button to check. + public bool IsSuppressed(SButton button) + { + return this.InputState.SuppressButtons.Contains(button); + } + + /// Get whether a given button was pressed or held. + /// The button to check. + public bool IsDown(SButton button) + { + return this.InputState.IsDown(button); + } + } +} diff --git a/src/StardewModdingAPI/Events/ChangeType.cs b/src/StardewModdingAPI/Events/ChangeType.cs new file mode 100644 index 00000000..4b207f08 --- /dev/null +++ b/src/StardewModdingAPI/Events/ChangeType.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Events +{ + /// Indicates how an inventory item changed. + public enum ChangeType + { + /// The entire stack was removed. + Removed, + + /// The entire stack was added. + Added, + + /// The stack size changed. + StackChange + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/StardewModdingAPI/Events/ContentEvents.cs new file mode 100644 index 00000000..aca76ef7 --- /dev/null +++ b/src/StardewModdingAPI/Events/ContentEvents.cs @@ -0,0 +1,45 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the game loads content. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class ContentEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised after the content language changes. + public static event EventHandler> AfterLocaleChanged + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + ContentEvents.EventManager.Legacy_LocaleChanged.Add(value); + } + remove => ContentEvents.EventManager.Legacy_LocaleChanged.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + ContentEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/ControlEvents.cs b/src/StardewModdingAPI/Events/ControlEvents.cs new file mode 100644 index 00000000..45aedc9b --- /dev/null +++ b/src/StardewModdingAPI/Events/ControlEvents.cs @@ -0,0 +1,123 @@ +#if !SMAPI_3_0_STRICT +using System; +using Microsoft.Xna.Framework.Input; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player uses a controller, keyboard, or mouse. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class ControlEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised when the changes. That happens when the player presses or releases a key. + public static event EventHandler KeyboardChanged + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + ControlEvents.EventManager.Legacy_KeyboardChanged.Add(value); + } + remove => ControlEvents.EventManager.Legacy_KeyboardChanged.Remove(value); + } + + /// Raised after the player presses a keyboard key. + public static event EventHandler KeyPressed + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + ControlEvents.EventManager.Legacy_KeyPressed.Add(value); + } + remove => ControlEvents.EventManager.Legacy_KeyPressed.Remove(value); + } + + /// Raised after the player releases a keyboard key. + public static event EventHandler KeyReleased + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + ControlEvents.EventManager.Legacy_KeyReleased.Add(value); + } + remove => ControlEvents.EventManager.Legacy_KeyReleased.Remove(value); + } + + /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. + public static event EventHandler MouseChanged + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + ControlEvents.EventManager.Legacy_MouseChanged.Add(value); + } + remove => ControlEvents.EventManager.Legacy_MouseChanged.Remove(value); + } + + /// The player pressed a controller button. This event isn't raised for trigger buttons. + public static event EventHandler ControllerButtonPressed + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + ControlEvents.EventManager.Legacy_ControllerButtonPressed.Add(value); + } + remove => ControlEvents.EventManager.Legacy_ControllerButtonPressed.Remove(value); + } + + /// The player released a controller button. This event isn't raised for trigger buttons. + public static event EventHandler ControllerButtonReleased + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + ControlEvents.EventManager.Legacy_ControllerButtonReleased.Add(value); + } + remove => ControlEvents.EventManager.Legacy_ControllerButtonReleased.Remove(value); + } + + /// The player pressed a controller trigger button. + public static event EventHandler ControllerTriggerPressed + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + ControlEvents.EventManager.Legacy_ControllerTriggerPressed.Add(value); + } + remove => ControlEvents.EventManager.Legacy_ControllerTriggerPressed.Remove(value); + } + + /// The player released a controller trigger button. + public static event EventHandler ControllerTriggerReleased + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + ControlEvents.EventManager.Legacy_ControllerTriggerReleased.Add(value); + } + remove => ControlEvents.EventManager.Legacy_ControllerTriggerReleased.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + ControlEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/CursorMovedEventArgs.cs b/src/StardewModdingAPI/Events/CursorMovedEventArgs.cs new file mode 100644 index 00000000..43ff90ce --- /dev/null +++ b/src/StardewModdingAPI/Events/CursorMovedEventArgs.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when the in-game cursor is moved. + public class CursorMovedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous cursor position. + public ICursorPosition OldPosition { get; } + + /// The current cursor position. + public ICursorPosition NewPosition { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous cursor position. + /// The new cursor position. + internal CursorMovedEventArgs(ICursorPosition oldPosition, ICursorPosition newPosition) + { + this.OldPosition = oldPosition; + this.NewPosition = newPosition; + } + } +} diff --git a/src/StardewModdingAPI/Events/DayEndingEventArgs.cs b/src/StardewModdingAPI/Events/DayEndingEventArgs.cs new file mode 100644 index 00000000..5cb433bc --- /dev/null +++ b/src/StardewModdingAPI/Events/DayEndingEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class DayEndingEventArgs : EventArgs { } +} diff --git a/src/StardewModdingAPI/Events/DayStartedEventArgs.cs b/src/StardewModdingAPI/Events/DayStartedEventArgs.cs new file mode 100644 index 00000000..45823628 --- /dev/null +++ b/src/StardewModdingAPI/Events/DayStartedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class DayStartedEventArgs : EventArgs { } +} diff --git a/src/StardewModdingAPI/Events/DebrisListChangedEventArgs.cs b/src/StardewModdingAPI/Events/DebrisListChangedEventArgs.cs new file mode 100644 index 00000000..61b7590a --- /dev/null +++ b/src/StardewModdingAPI/Events/DebrisListChangedEventArgs.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class DebrisListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The debris added to the location. + public IEnumerable Added { get; } + + /// The debris removed from the location. + public IEnumerable Removed { get; } + + /// Whether this is the location containing the local player. + public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The debris added to the location. + /// The debris removed from the location. + internal DebrisListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs b/src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs new file mode 100644 index 00000000..a0b903b7 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs @@ -0,0 +1,33 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewValley.Menus; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsClickableMenuChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous menu. + public IClickableMenu NewMenu { get; } + + /// The current menu. + public IClickableMenu PriorMenu { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous menu. + /// The current menu. + public EventArgsClickableMenuChanged(IClickableMenu priorMenu, IClickableMenu newMenu) + { + this.NewMenu = newMenu; + this.PriorMenu = priorMenu; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs b/src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs new file mode 100644 index 00000000..77db69ea --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs @@ -0,0 +1,28 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewValley.Menus; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsClickableMenuClosed : EventArgs + { + /********* + ** Accessors + *********/ + /// The menu that was closed. + public IClickableMenu PriorMenu { get; } + + + /********* + ** Accessors + *********/ + /// Construct an instance. + /// The menu that was closed. + public EventArgsClickableMenuClosed(IClickableMenu priorMenu) + { + this.PriorMenu = priorMenu; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs b/src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs new file mode 100644 index 00000000..949446e1 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs @@ -0,0 +1,34 @@ +#if !SMAPI_3_0_STRICT +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsControllerButtonPressed : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who pressed the button. + public PlayerIndex PlayerIndex { get; } + + /// The controller button that was pressed. + public Buttons ButtonPressed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who pressed the button. + /// The controller button that was pressed. + public EventArgsControllerButtonPressed(PlayerIndex playerIndex, Buttons button) + { + this.PlayerIndex = playerIndex; + this.ButtonPressed = button; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs b/src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs new file mode 100644 index 00000000..d6d6d840 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs @@ -0,0 +1,34 @@ +#if !SMAPI_3_0_STRICT +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsControllerButtonReleased : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who pressed the button. + public PlayerIndex PlayerIndex { get; } + + /// The controller button that was pressed. + public Buttons ButtonReleased { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who pressed the button. + /// The controller button that was released. + public EventArgsControllerButtonReleased(PlayerIndex playerIndex, Buttons button) + { + this.PlayerIndex = playerIndex; + this.ButtonReleased = button; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs b/src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs new file mode 100644 index 00000000..33be2fa3 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs @@ -0,0 +1,39 @@ +#if !SMAPI_3_0_STRICT +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsControllerTriggerPressed : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who pressed the button. + public PlayerIndex PlayerIndex { get; } + + /// The controller button that was pressed. + public Buttons ButtonPressed { get; } + + /// The current trigger value. + public float Value { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who pressed the trigger button. + /// The trigger button that was pressed. + /// The current trigger value. + public EventArgsControllerTriggerPressed(PlayerIndex playerIndex, Buttons button, float value) + { + this.PlayerIndex = playerIndex; + this.ButtonPressed = button; + this.Value = value; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs b/src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs new file mode 100644 index 00000000..e90ff712 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs @@ -0,0 +1,39 @@ +#if !SMAPI_3_0_STRICT +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsControllerTriggerReleased : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who pressed the button. + public PlayerIndex PlayerIndex { get; } + + /// The controller button that was released. + public Buttons ButtonReleased { get; } + + /// The current trigger value. + public float Value { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who pressed the trigger button. + /// The trigger button that was released. + /// The current trigger value. + public EventArgsControllerTriggerReleased(PlayerIndex playerIndex, Buttons button, float value) + { + this.PlayerIndex = playerIndex; + this.ButtonReleased = button; + this.Value = value; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsInput.cs b/src/StardewModdingAPI/Events/EventArgsInput.cs new file mode 100644 index 00000000..5cff3408 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsInput.cs @@ -0,0 +1,64 @@ +#if !SMAPI_3_0_STRICT +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when a button is pressed or released. + public class EventArgsInput : EventArgs + { + /********* + ** Fields + *********/ + /// The buttons to suppress. + private readonly HashSet SuppressButtons; + + + /********* + ** Accessors + *********/ + /// The button on the controller, keyboard, or mouse. + public SButton Button { get; } + + /// The current cursor position. + public ICursorPosition Cursor { get; } + + /// Whether the input should trigger actions on the affected tile. + public bool IsActionButton => this.Button.IsActionButton(); + + /// Whether the input should use tools on the affected tile. + public bool IsUseToolButton => this.Button.IsUseToolButton(); + + /// Whether a mod has indicated the key was already handled. + public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The button on the controller, keyboard, or mouse. + /// The cursor position. + /// The buttons to suppress. + public EventArgsInput(SButton button, ICursorPosition cursor, HashSet suppressButtons) + { + this.Button = button; + this.Cursor = cursor; + this.SuppressButtons = suppressButtons; + } + + /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. + public void SuppressButton() + { + this.SuppressButton(this.Button); + } + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + public void SuppressButton(SButton button) + { + this.SuppressButtons.Add(button); + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsIntChanged.cs b/src/StardewModdingAPI/Events/EventArgsIntChanged.cs new file mode 100644 index 00000000..76ec6d08 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsIntChanged.cs @@ -0,0 +1,32 @@ +#if !SMAPI_3_0_STRICT +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an integer field that changed value. + public class EventArgsIntChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous value. + public int PriorInt { get; } + + /// The current value. + public int NewInt { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous value. + /// The current value. + public EventArgsIntChanged(int priorInt, int newInt) + { + this.PriorInt = priorInt; + this.NewInt = newInt; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs b/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs new file mode 100644 index 00000000..488dd23f --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs @@ -0,0 +1,43 @@ +#if !SMAPI_3_0_STRICT +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsInventoryChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The player's inventory. + public IList Inventory { get; } + + /// The added items. + public List Added { get; } + + /// The removed items. + public List Removed { get; } + + /// The items whose stack sizes changed. + public List QuantityChanged { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player's inventory. + /// The inventory changes. + public EventArgsInventoryChanged(IList inventory, ItemStackChange[] changedItems) + { + this.Inventory = inventory; + this.Added = changedItems.Where(n => n.ChangeType == ChangeType.Added).ToList(); + this.Removed = changedItems.Where(n => n.ChangeType == ChangeType.Removed).ToList(); + this.QuantityChanged = changedItems.Where(n => n.ChangeType == ChangeType.StackChange).ToList(); + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsKeyPressed.cs b/src/StardewModdingAPI/Events/EventArgsKeyPressed.cs new file mode 100644 index 00000000..6204d821 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsKeyPressed.cs @@ -0,0 +1,28 @@ +#if !SMAPI_3_0_STRICT +using System; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsKeyPressed : EventArgs + { + /********* + ** Accessors + *********/ + /// The keyboard button that was pressed. + public Keys KeyPressed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The keyboard button that was pressed. + public EventArgsKeyPressed(Keys key) + { + this.KeyPressed = key; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs b/src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs new file mode 100644 index 00000000..2c3203b1 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs @@ -0,0 +1,33 @@ +#if !SMAPI_3_0_STRICT +using System; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsKeyboardStateChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous keyboard state. + public KeyboardState NewState { get; } + + /// The current keyboard state. + public KeyboardState PriorState { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous keyboard state. + /// The current keyboard state. + public EventArgsKeyboardStateChanged(KeyboardState priorState, KeyboardState newState) + { + this.PriorState = priorState; + this.NewState = newState; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsLevelUp.cs b/src/StardewModdingAPI/Events/EventArgsLevelUp.cs new file mode 100644 index 00000000..06c70088 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsLevelUp.cs @@ -0,0 +1,55 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Enums; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLevelUp : EventArgs + { + /********* + ** Accessors + *********/ + /// The player skill that leveled up. + public LevelType Type { get; } + + /// The new skill level. + public int NewLevel { get; } + + /// The player skill types. + public enum LevelType + { + /// The combat skill. + Combat = SkillType.Combat, + + /// The farming skill. + Farming = SkillType.Farming, + + /// The fishing skill. + Fishing = SkillType.Fishing, + + /// The foraging skill. + Foraging = SkillType.Foraging, + + /// The mining skill. + Mining = SkillType.Mining, + + /// The luck skill. + Luck = SkillType.Luck + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player skill that leveled up. + /// The new skill level. + public EventArgsLevelUp(LevelType type, int newLevel) + { + this.Type = type; + this.NewLevel = newLevel; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsLocationBuildingsChanged.cs b/src/StardewModdingAPI/Events/EventArgsLocationBuildingsChanged.cs new file mode 100644 index 00000000..25e84722 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsLocationBuildingsChanged.cs @@ -0,0 +1,41 @@ +#if !SMAPI_3_0_STRICT +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.Buildings; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLocationBuildingsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The buildings added to the location. + public IEnumerable Added { get; } + + /// The buildings removed from the location. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The buildings added to the location. + /// The buildings removed from the location. + public EventArgsLocationBuildingsChanged(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs b/src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs new file mode 100644 index 00000000..9ca2e3e2 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs @@ -0,0 +1,42 @@ +#if !SMAPI_3_0_STRICT +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using SObject = StardewValley.Object; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLocationObjectsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The objects added to the location. + public IEnumerable> Added { get; } + + /// The objects removed from the location. + public IEnumerable> Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The objects added to the location. + /// The objects removed from the location. + public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsLocationsChanged.cs b/src/StardewModdingAPI/Events/EventArgsLocationsChanged.cs new file mode 100644 index 00000000..1a59e612 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsLocationsChanged.cs @@ -0,0 +1,35 @@ +#if !SMAPI_3_0_STRICT +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsLocationsChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The added locations. + public IEnumerable Added { get; } + + /// The removed locations. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The added locations. + /// The removed locations. + public EventArgsLocationsChanged(IEnumerable added, IEnumerable removed) + { + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs b/src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs new file mode 100644 index 00000000..c63b04e9 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs @@ -0,0 +1,32 @@ +#if !SMAPI_3_0_STRICT +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsMineLevelChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous mine level. + public int PreviousMineLevel { get; } + + /// The current mine level. + public int CurrentMineLevel { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous mine level. + /// The current mine level. + public EventArgsMineLevelChanged(int previousMineLevel, int currentMineLevel) + { + this.PreviousMineLevel = previousMineLevel; + this.CurrentMineLevel = currentMineLevel; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs b/src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs new file mode 100644 index 00000000..09f3f759 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs @@ -0,0 +1,44 @@ +#if !SMAPI_3_0_STRICT +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsMouseStateChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous mouse state. + public MouseState PriorState { get; } + + /// The current mouse state. + public MouseState NewState { get; } + + /// The previous mouse position on the screen adjusted for the zoom level. + public Point PriorPosition { get; } + + /// The current mouse position on the screen adjusted for the zoom level. + public Point NewPosition { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous mouse state. + /// The current mouse state. + /// The previous mouse position on the screen adjusted for the zoom level. + /// The current mouse position on the screen adjusted for the zoom level. + public EventArgsMouseStateChanged(MouseState priorState, MouseState newState, Point priorPosition, Point newPosition) + { + this.PriorState = priorState; + this.NewState = newState; + this.PriorPosition = priorPosition; + this.NewPosition = newPosition; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsPlayerWarped.cs b/src/StardewModdingAPI/Events/EventArgsPlayerWarped.cs new file mode 100644 index 00000000..d1aa1588 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsPlayerWarped.cs @@ -0,0 +1,34 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class EventArgsPlayerWarped : EventArgs + { + /********* + ** Accessors + *********/ + /// The player's previous location. + public GameLocation PriorLocation { get; } + + /// The player's current location. + public GameLocation NewLocation { get; } + + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player's previous location. + /// The player's current location. + public EventArgsPlayerWarped(GameLocation priorLocation, GameLocation newLocation) + { + this.NewLocation = newLocation; + this.PriorLocation = priorLocation; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/EventArgsValueChanged.cs b/src/StardewModdingAPI/Events/EventArgsValueChanged.cs new file mode 100644 index 00000000..7bfac7a2 --- /dev/null +++ b/src/StardewModdingAPI/Events/EventArgsValueChanged.cs @@ -0,0 +1,33 @@ +#if !SMAPI_3_0_STRICT +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a field that changed value. + /// The value type. + public class EventArgsValueChanged : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous value. + public T PriorValue { get; } + + /// The current value. + public T NewValue { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous value. + /// The current value. + public EventArgsValueChanged(T priorValue, T newValue) + { + this.PriorValue = priorValue; + this.NewValue = newValue; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs new file mode 100644 index 00000000..9d945277 --- /dev/null +++ b/src/StardewModdingAPI/Events/GameEvents.cs @@ -0,0 +1,122 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the game changes state. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class GameEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised when the game updates its state (≈60 times per second). + public static event EventHandler UpdateTick + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GameEvents.EventManager.Legacy_UpdateTick.Add(value); + } + remove => GameEvents.EventManager.Legacy_UpdateTick.Remove(value); + } + + /// Raised every other tick (≈30 times per second). + public static event EventHandler SecondUpdateTick + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GameEvents.EventManager.Legacy_SecondUpdateTick.Add(value); + } + remove => GameEvents.EventManager.Legacy_SecondUpdateTick.Remove(value); + } + + /// Raised every fourth tick (≈15 times per second). + public static event EventHandler FourthUpdateTick + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GameEvents.EventManager.Legacy_FourthUpdateTick.Add(value); + } + remove => GameEvents.EventManager.Legacy_FourthUpdateTick.Remove(value); + } + + /// Raised every eighth tick (≈8 times per second). + public static event EventHandler EighthUpdateTick + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GameEvents.EventManager.Legacy_EighthUpdateTick.Add(value); + } + remove => GameEvents.EventManager.Legacy_EighthUpdateTick.Remove(value); + } + + /// Raised every 15th tick (≈4 times per second). + public static event EventHandler QuarterSecondTick + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GameEvents.EventManager.Legacy_QuarterSecondTick.Add(value); + } + remove => GameEvents.EventManager.Legacy_QuarterSecondTick.Remove(value); + } + + /// Raised every 30th tick (≈twice per second). + public static event EventHandler HalfSecondTick + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GameEvents.EventManager.Legacy_HalfSecondTick.Add(value); + } + remove => GameEvents.EventManager.Legacy_HalfSecondTick.Remove(value); + } + + /// Raised every 60th tick (≈once per second). + public static event EventHandler OneSecondTick + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GameEvents.EventManager.Legacy_OneSecondTick.Add(value); + } + remove => GameEvents.EventManager.Legacy_OneSecondTick.Remove(value); + } + + /// Raised once after the game initialises and all methods have been called. + public static event EventHandler FirstUpdateTick + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GameEvents.EventManager.Legacy_FirstUpdateTick.Add(value); + } + remove => GameEvents.EventManager.Legacy_FirstUpdateTick.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + GameEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/GameLaunchedEventArgs.cs b/src/StardewModdingAPI/Events/GameLaunchedEventArgs.cs new file mode 100644 index 00000000..a4c78754 --- /dev/null +++ b/src/StardewModdingAPI/Events/GameLaunchedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class GameLaunchedEventArgs : EventArgs { } +} diff --git a/src/StardewModdingAPI/Events/GraphicsEvents.cs b/src/StardewModdingAPI/Events/GraphicsEvents.cs new file mode 100644 index 00000000..24a16a29 --- /dev/null +++ b/src/StardewModdingAPI/Events/GraphicsEvents.cs @@ -0,0 +1,120 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised during the game's draw loop, when the game is rendering content to the window. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class GraphicsEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised after the game window is resized. + public static event EventHandler Resize + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GraphicsEvents.EventManager.Legacy_Resize.Add(value); + } + remove => GraphicsEvents.EventManager.Legacy_Resize.Remove(value); + } + + /**** + ** Main render events + ****/ + /// Raised before drawing the world to the screen. + public static event EventHandler OnPreRenderEvent + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GraphicsEvents.EventManager.Legacy_OnPreRenderEvent.Add(value); + } + remove => GraphicsEvents.EventManager.Legacy_OnPreRenderEvent.Remove(value); + } + + /// Raised after drawing the world to the screen. + public static event EventHandler OnPostRenderEvent + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GraphicsEvents.EventManager.Legacy_OnPostRenderEvent.Add(value); + } + remove => GraphicsEvents.EventManager.Legacy_OnPostRenderEvent.Remove(value); + } + + /**** + ** HUD events + ****/ + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public static event EventHandler OnPreRenderHudEvent + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GraphicsEvents.EventManager.Legacy_OnPreRenderHudEvent.Add(value); + } + remove => GraphicsEvents.EventManager.Legacy_OnPreRenderHudEvent.Remove(value); + } + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public static event EventHandler OnPostRenderHudEvent + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GraphicsEvents.EventManager.Legacy_OnPostRenderHudEvent.Add(value); + } + remove => GraphicsEvents.EventManager.Legacy_OnPostRenderHudEvent.Remove(value); + } + + /**** + ** GUI events + ****/ + /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public static event EventHandler OnPreRenderGuiEvent + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GraphicsEvents.EventManager.Legacy_OnPreRenderGuiEvent.Add(value); + } + remove => GraphicsEvents.EventManager.Legacy_OnPreRenderGuiEvent.Remove(value); + } + + /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public static event EventHandler OnPostRenderGuiEvent + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + GraphicsEvents.EventManager.Legacy_OnPostRenderGuiEvent.Add(value); + } + remove => GraphicsEvents.EventManager.Legacy_OnPostRenderGuiEvent.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + GraphicsEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/IDisplayEvents.cs b/src/StardewModdingAPI/Events/IDisplayEvents.cs new file mode 100644 index 00000000..dbf8d90f --- /dev/null +++ b/src/StardewModdingAPI/Events/IDisplayEvents.cs @@ -0,0 +1,39 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Events related to UI and drawing to the screen. + public interface IDisplayEvents + { + /// Raised after a game menu is opened, closed, or replaced. + event EventHandler MenuChanged; + + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. + event EventHandler Rendering; + + /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). + event EventHandler Rendered; + + /// Raised before the game world is drawn to the screen. This event isn't useful for drawing to the screen, since the game will draw over it. + event EventHandler RenderingWorld; + + /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. Content drawn to the sprite batch at this point will be drawn over the world, but under any active menu, HUD elements, or cursor. + event EventHandler RenderedWorld; + + /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. This includes the game's internal menus like the title screen. Content drawn to the sprite batch at this point will appear under the menu. + event EventHandler RenderingActiveMenu; + + /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. Content drawn to the sprite batch at this point will appear over the menu and menu cursor. + event EventHandler RenderedActiveMenu; + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear under the HUD. + event EventHandler RenderingHud; + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear over the HUD. + event EventHandler RenderedHud; + + /// Raised after the game window is resized. + event EventHandler WindowResized; + } +} diff --git a/src/StardewModdingAPI/Events/IGameLoopEvents.cs b/src/StardewModdingAPI/Events/IGameLoopEvents.cs new file mode 100644 index 00000000..6fb56c8b --- /dev/null +++ b/src/StardewModdingAPI/Events/IGameLoopEvents.cs @@ -0,0 +1,50 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + public interface IGameLoopEvents + { + /// Raised after the game is launched, right before the first update tick. This happens once per game session (unrelated to loading saves). All mods are loaded and initialised at this point, so this is a good time to set up mod integrations. + event EventHandler GameLaunched; + + /// Raised before the game state is updated (≈60 times per second). + event EventHandler UpdateTicking; + + /// Raised after the game state is updated (≈60 times per second). + event EventHandler UpdateTicked; + + /// Raised once per second before the game state is updated. + event EventHandler OneSecondUpdateTicking; + + /// Raised once per second after the game state is updated. + event EventHandler OneSecondUpdateTicked; + + /// Raised before the game creates a new save file. + event EventHandler SaveCreating; + + /// Raised after the game finishes creating the save file. + event EventHandler SaveCreated; + + /// Raised before the game begins writes data to the save file (except the initial save creation). + event EventHandler Saving; + + /// Raised after the game finishes writing data to the save file (except the initial save creation). + event EventHandler Saved; + + /// Raised after the player loads a save slot and the world is initialised. + event EventHandler SaveLoaded; + + /// Raised after the game begins a new day (including when the player loads a save). + event EventHandler DayStarted; + + /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . + event EventHandler DayEnding; + + /// Raised after the in-game clock time changes. + event EventHandler TimeChanged; + + /// Raised after the game returns to the title screen. + event EventHandler ReturnedToTitle; + } +} diff --git a/src/StardewModdingAPI/Events/IInputEvents.cs b/src/StardewModdingAPI/Events/IInputEvents.cs new file mode 100644 index 00000000..5c40a438 --- /dev/null +++ b/src/StardewModdingAPI/Events/IInputEvents.cs @@ -0,0 +1,20 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player provides input using a controller, keyboard, or mouse. + public interface IInputEvents + { + /// Raised after the player presses a button on the keyboard, controller, or mouse. + event EventHandler ButtonPressed; + + /// Raised after the player releases a button on the keyboard, controller, or mouse. + event EventHandler ButtonReleased; + + /// Raised after the player moves the in-game cursor. + event EventHandler CursorMoved; + + /// Raised after the player scrolls the mouse wheel. + event EventHandler MouseWheelScrolled; + } +} diff --git a/src/StardewModdingAPI/Events/IModEvents.cs b/src/StardewModdingAPI/Events/IModEvents.cs new file mode 100644 index 00000000..bd7ab880 --- /dev/null +++ b/src/StardewModdingAPI/Events/IModEvents.cs @@ -0,0 +1,27 @@ +namespace StardewModdingAPI.Events +{ + /// Manages access to events raised by SMAPI. + public interface IModEvents + { + /// Events related to UI and drawing to the screen. + IDisplayEvents Display { get; } + + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + IGameLoopEvents GameLoop { get; } + + /// Events raised when the player provides input using a controller, keyboard, or mouse. + IInputEvents Input { get; } + + /// Events raised for multiplayer messages and connections. + IMultiplayerEvents Multiplayer { get; } + + /// Events raised when the player data changes. + IPlayerEvents Player { get; } + + /// Events raised when something changes in the world. + IWorldEvents World { get; } + + /// Events serving specialised edge cases that shouldn't be used by most mods. + ISpecialisedEvents Specialised { get; } + } +} diff --git a/src/StardewModdingAPI/Events/IMultiplayerEvents.cs b/src/StardewModdingAPI/Events/IMultiplayerEvents.cs new file mode 100644 index 00000000..4a31f48e --- /dev/null +++ b/src/StardewModdingAPI/Events/IMultiplayerEvents.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events raised for multiplayer messages and connections. + public interface IMultiplayerEvents + { + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + event EventHandler PeerContextReceived; + + /// Raised after a mod message is received over the network. + event EventHandler ModMessageReceived; + + /// Raised after the connection with a peer is severed. + event EventHandler PeerDisconnected; + } +} diff --git a/src/StardewModdingAPI/Events/IPlayerEvents.cs b/src/StardewModdingAPI/Events/IPlayerEvents.cs new file mode 100644 index 00000000..81e17b1a --- /dev/null +++ b/src/StardewModdingAPI/Events/IPlayerEvents.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player data changes. + public interface IPlayerEvents + { + /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the current player. + event EventHandler InventoryChanged; + + /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. NOTE: this event is currently only raised for the current player. + event EventHandler LevelChanged; + + /// Raised after a player warps to a new location. NOTE: this event is currently only raised for the current player. + event EventHandler Warped; + } +} diff --git a/src/StardewModdingAPI/Events/ISpecialisedEvents.cs b/src/StardewModdingAPI/Events/ISpecialisedEvents.cs new file mode 100644 index 00000000..ecb109e6 --- /dev/null +++ b/src/StardewModdingAPI/Events/ISpecialisedEvents.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events serving specialised edge cases that shouldn't be used by most mods. + public interface ISpecialisedEvents + { + /// Raised when the low-level stage in the game's loading process has changed. This is an advanced event for mods which need to run code at specific points in the loading process. The available stages or when they happen might change without warning in future versions (e.g. due to changes in the game's load process), so mods using this event are more likely to break or have bugs. Most mods should use instead. + event EventHandler LoadStageChanged; + + /// Raised before the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. + event EventHandler UnvalidatedUpdateTicking; + + /// Raised after the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. + event EventHandler UnvalidatedUpdateTicked; + } +} diff --git a/src/StardewModdingAPI/Events/IWorldEvents.cs b/src/StardewModdingAPI/Events/IWorldEvents.cs new file mode 100644 index 00000000..0ceffcc1 --- /dev/null +++ b/src/StardewModdingAPI/Events/IWorldEvents.cs @@ -0,0 +1,29 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Events raised when something changes in the world. + public interface IWorldEvents + { + /// Raised after a game location is added or removed. + event EventHandler LocationListChanged; + + /// Raised after buildings are added or removed in a location. + event EventHandler BuildingListChanged; + + /// Raised after debris are added or removed in a location. + event EventHandler DebrisListChanged; + + /// Raised after large terrain features (like bushes) are added or removed in a location. + event EventHandler LargeTerrainFeatureListChanged; + + /// Raised after NPCs are added or removed in a location. + event EventHandler NpcListChanged; + + /// Raised after objects are added or removed in a location. + event EventHandler ObjectListChanged; + + /// Raised after terrain features (like floors and trees) are added or removed in a location. + event EventHandler TerrainFeatureListChanged; + } +} diff --git a/src/StardewModdingAPI/Events/InputEvents.cs b/src/StardewModdingAPI/Events/InputEvents.cs new file mode 100644 index 00000000..c5ab8c83 --- /dev/null +++ b/src/StardewModdingAPI/Events/InputEvents.cs @@ -0,0 +1,56 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player uses a controller, keyboard, or mouse button. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class InputEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised when the player presses a button on the keyboard, controller, or mouse. + public static event EventHandler ButtonPressed + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + InputEvents.EventManager.Legacy_ButtonPressed.Add(value); + } + remove => InputEvents.EventManager.Legacy_ButtonPressed.Remove(value); + } + + /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. + public static event EventHandler ButtonReleased + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + InputEvents.EventManager.Legacy_ButtonReleased.Add(value); + } + remove => InputEvents.EventManager.Legacy_ButtonReleased.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + InputEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/InventoryChangedEventArgs.cs b/src/StardewModdingAPI/Events/InventoryChangedEventArgs.cs new file mode 100644 index 00000000..874c2e48 --- /dev/null +++ b/src/StardewModdingAPI/Events/InventoryChangedEventArgs.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class InventoryChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player whose inventory changed. + public Farmer Player { get; } + + /// The added items. + public IEnumerable Added { get; } + + /// The removed items. + public IEnumerable Removed { get; } + + /// The items whose stack sizes changed, with the relative change. + public IEnumerable QuantityChanged { get; } + + /// Whether the affected player is the local one. + public bool IsLocalPlayer => this.Player.IsLocalPlayer; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player whose inventory changed. + /// The inventory changes. + internal InventoryChangedEventArgs(Farmer player, ItemStackChange[] changedItems) + { + this.Player = player; + this.Added = changedItems + .Where(n => n.ChangeType == ChangeType.Added) + .Select(p => p.Item) + .ToArray(); + + this.Removed = changedItems + .Where(n => n.ChangeType == ChangeType.Removed) + .Select(p => p.Item) + .ToArray(); + + this.QuantityChanged = changedItems + .Where(n => n.ChangeType == ChangeType.StackChange) + .Select(change => new ItemStackSizeChange( + item: change.Item, + oldSize: change.Item.Stack - change.StackChange, + newSize: change.Item.Stack + )) + .ToArray(); + } + } +} diff --git a/src/StardewModdingAPI/Events/ItemStackChange.cs b/src/StardewModdingAPI/Events/ItemStackChange.cs new file mode 100644 index 00000000..f9ae6df6 --- /dev/null +++ b/src/StardewModdingAPI/Events/ItemStackChange.cs @@ -0,0 +1,20 @@ +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Represents an inventory slot that changed. + public class ItemStackChange + { + /********* + ** Accessors + *********/ + /// The item in the slot. + public Item Item { get; set; } + + /// The amount by which the item's stack size changed. + public int StackChange { get; set; } + + /// How the inventory slot changed. + public ChangeType ChangeType { get; set; } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/ItemStackSizeChange.cs b/src/StardewModdingAPI/Events/ItemStackSizeChange.cs new file mode 100644 index 00000000..35369be2 --- /dev/null +++ b/src/StardewModdingAPI/Events/ItemStackSizeChange.cs @@ -0,0 +1,35 @@ +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// An inventory item stack size change. + public class ItemStackSizeChange + { + /********* + ** Accessors + *********/ + /// The item whose stack size changed. + public Item Item { get; } + + /// The previous stack size. + public int OldSize { get; } + + /// The new stack size. + public int NewSize { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The item whose stack size changed. + /// The previous stack size. + /// The new stack size. + public ItemStackSizeChange(Item item, int oldSize, int newSize) + { + this.Item = item; + this.OldSize = oldSize; + this.NewSize = newSize; + } + } +} diff --git a/src/StardewModdingAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs b/src/StardewModdingAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs new file mode 100644 index 00000000..59d79f0f --- /dev/null +++ b/src/StardewModdingAPI/Events/LargeTerrainFeatureListChangedEventArgs.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class LargeTerrainFeatureListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The large terrain features added to the location. + public IEnumerable Added { get; } + + /// The large terrain features removed from the location. + public IEnumerable Removed { get; } + + /// Whether this is the location containing the local player. + public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The large terrain features added to the location. + /// The large terrain features removed from the location. + internal LargeTerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/StardewModdingAPI/Events/LevelChangedEventArgs.cs b/src/StardewModdingAPI/Events/LevelChangedEventArgs.cs new file mode 100644 index 00000000..c7303603 --- /dev/null +++ b/src/StardewModdingAPI/Events/LevelChangedEventArgs.cs @@ -0,0 +1,45 @@ +using System; +using StardewModdingAPI.Enums; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class LevelChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player whose skill level changed. + public Farmer Player { get; } + + /// The skill whose level changed. + public SkillType Skill { get; } + + /// The previous skill level. + public int OldLevel { get; } + + /// The new skill level. + public int NewLevel { get; } + + /// Whether the affected player is the local one. + public bool IsLocalPlayer => this.Player.IsLocalPlayer; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player whose skill level changed. + /// The skill whose level changed. + /// The previous skill level. + /// The new skill level. + internal LevelChangedEventArgs(Farmer player, SkillType skill, int oldLevel, int newLevel) + { + this.Player = player; + this.Skill = skill; + this.OldLevel = oldLevel; + this.NewLevel = newLevel; + } + } +} diff --git a/src/StardewModdingAPI/Events/LoadStageChangedEventArgs.cs b/src/StardewModdingAPI/Events/LoadStageChangedEventArgs.cs new file mode 100644 index 00000000..e837a5f1 --- /dev/null +++ b/src/StardewModdingAPI/Events/LoadStageChangedEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using StardewModdingAPI.Enums; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class LoadStageChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous load stage. + public LoadStage OldStage { get; } + + /// The new load stage. + public LoadStage NewStage { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous load stage. + /// The new load stage. + public LoadStageChangedEventArgs(LoadStage old, LoadStage current) + { + this.OldStage = old; + this.NewStage = current; + } + } +} diff --git a/src/StardewModdingAPI/Events/LocationEvents.cs b/src/StardewModdingAPI/Events/LocationEvents.cs new file mode 100644 index 00000000..0761bdd8 --- /dev/null +++ b/src/StardewModdingAPI/Events/LocationEvents.cs @@ -0,0 +1,67 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player transitions between game locations, a location is added or removed, or the objects in the current location change. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class LocationEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised after a game location is added or removed. + public static event EventHandler LocationsChanged + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + LocationEvents.EventManager.Legacy_LocationsChanged.Add(value); + } + remove => LocationEvents.EventManager.Legacy_LocationsChanged.Remove(value); + } + + /// Raised after buildings are added or removed in a location. + public static event EventHandler BuildingsChanged + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + LocationEvents.EventManager.Legacy_BuildingsChanged.Add(value); + } + remove => LocationEvents.EventManager.Legacy_BuildingsChanged.Remove(value); + } + + /// Raised after objects are added or removed in a location. + public static event EventHandler ObjectsChanged + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + LocationEvents.EventManager.Legacy_ObjectsChanged.Add(value); + } + remove => LocationEvents.EventManager.Legacy_ObjectsChanged.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + LocationEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/LocationListChangedEventArgs.cs b/src/StardewModdingAPI/Events/LocationListChangedEventArgs.cs new file mode 100644 index 00000000..1ebb3e2d --- /dev/null +++ b/src/StardewModdingAPI/Events/LocationListChangedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class LocationListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The added locations. + public IEnumerable Added { get; } + + /// The removed locations. + public IEnumerable Removed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The added locations. + /// The removed locations. + internal LocationListChangedEventArgs(IEnumerable added, IEnumerable removed) + { + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/StardewModdingAPI/Events/MenuChangedEventArgs.cs b/src/StardewModdingAPI/Events/MenuChangedEventArgs.cs new file mode 100644 index 00000000..977ba38b --- /dev/null +++ b/src/StardewModdingAPI/Events/MenuChangedEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using StardewValley.Menus; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class MenuChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous menu. + public IClickableMenu OldMenu { get; } + + /// The current menu. + public IClickableMenu NewMenu { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous menu. + /// The current menu. + internal MenuChangedEventArgs(IClickableMenu oldMenu, IClickableMenu newMenu) + { + this.OldMenu = oldMenu; + this.NewMenu = newMenu; + } + } +} diff --git a/src/StardewModdingAPI/Events/MenuEvents.cs b/src/StardewModdingAPI/Events/MenuEvents.cs new file mode 100644 index 00000000..8647c268 --- /dev/null +++ b/src/StardewModdingAPI/Events/MenuEvents.cs @@ -0,0 +1,56 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised when a game menu is opened or closed (including internal menus like the title screen). + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class MenuEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. + public static event EventHandler MenuChanged + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + MenuEvents.EventManager.Legacy_MenuChanged.Add(value); + } + remove => MenuEvents.EventManager.Legacy_MenuChanged.Remove(value); + } + + /// Raised after a game menu is closed. + public static event EventHandler MenuClosed + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + MenuEvents.EventManager.Legacy_MenuClosed.Add(value); + } + remove => MenuEvents.EventManager.Legacy_MenuClosed.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + MenuEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/MineEvents.cs b/src/StardewModdingAPI/Events/MineEvents.cs new file mode 100644 index 00000000..929da35b --- /dev/null +++ b/src/StardewModdingAPI/Events/MineEvents.cs @@ -0,0 +1,45 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised when something happens in the mines. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class MineEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised after the player warps to a new level of the mine. + public static event EventHandler MineLevelChanged + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + MineEvents.EventManager.Legacy_MineLevelChanged.Add(value); + } + remove => MineEvents.EventManager.Legacy_MineLevelChanged.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + MineEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/ModMessageReceivedEventArgs.cs b/src/StardewModdingAPI/Events/ModMessageReceivedEventArgs.cs new file mode 100644 index 00000000..d4370028 --- /dev/null +++ b/src/StardewModdingAPI/Events/ModMessageReceivedEventArgs.cs @@ -0,0 +1,46 @@ +using System; +using StardewModdingAPI.Framework.Networking; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class ModMessageReceivedEventArgs : EventArgs + { + /********* + ** Fields + *********/ + /// The underlying message model. + private readonly ModMessageModel Message; + + + /********* + ** Accessors + *********/ + /// The unique ID of the player from whose computer the message was sent. + public long FromPlayerID => this.Message.FromPlayerID; + + /// The unique ID of the mod which sent the message. + public string FromModID => this.Message.FromModID; + + /// A message type which can be used to decide whether it's the one you want to handle, like SetPlayerLocation. This doesn't need to be globally unique, so mods should check the . + public string Type => this.Message.Type; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The received message. + internal ModMessageReceivedEventArgs(ModMessageModel message) + { + this.Message = message; + } + + /// Read the message data into the given model type. + /// The message model type. + public TModel ReadAs() + { + return this.Message.Data.ToObject(); + } + } +} diff --git a/src/StardewModdingAPI/Events/MouseWheelScrolledEventArgs.cs b/src/StardewModdingAPI/Events/MouseWheelScrolledEventArgs.cs new file mode 100644 index 00000000..0c736b39 --- /dev/null +++ b/src/StardewModdingAPI/Events/MouseWheelScrolledEventArgs.cs @@ -0,0 +1,38 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments when the player scrolls the mouse wheel. + public class MouseWheelScrolledEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The cursor position. + public ICursorPosition Position { get; } + + /// The old scroll value. + public int OldValue { get; } + + /// The new scroll value. + public int NewValue { get; } + + /// The amount by which the scroll value changed. + public int Delta => this.NewValue - this.OldValue; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The cursor position. + /// The old scroll value. + /// The new scroll value. + internal MouseWheelScrolledEventArgs(ICursorPosition position, int oldValue, int newValue) + { + this.Position = position; + this.OldValue = oldValue; + this.NewValue = newValue; + } + } +} diff --git a/src/StardewModdingAPI/Events/MultiplayerEvents.cs b/src/StardewModdingAPI/Events/MultiplayerEvents.cs new file mode 100644 index 00000000..0650a8e2 --- /dev/null +++ b/src/StardewModdingAPI/Events/MultiplayerEvents.cs @@ -0,0 +1,78 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised during the multiplayer sync process. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class MultiplayerEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised before the game syncs changes from other players. + public static event EventHandler BeforeMainSync + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + MultiplayerEvents.EventManager.Legacy_BeforeMainSync.Add(value); + } + remove => MultiplayerEvents.EventManager.Legacy_BeforeMainSync.Remove(value); + } + + /// Raised after the game syncs changes from other players. + public static event EventHandler AfterMainSync + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + MultiplayerEvents.EventManager.Legacy_AfterMainSync.Add(value); + } + remove => MultiplayerEvents.EventManager.Legacy_AfterMainSync.Remove(value); + } + + /// Raised before the game broadcasts changes to other players. + public static event EventHandler BeforeMainBroadcast + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + MultiplayerEvents.EventManager.Legacy_BeforeMainBroadcast.Add(value); + } + remove => MultiplayerEvents.EventManager.Legacy_BeforeMainBroadcast.Remove(value); + } + + /// Raised after the game broadcasts changes to other players. + public static event EventHandler AfterMainBroadcast + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + MultiplayerEvents.EventManager.Legacy_AfterMainBroadcast.Add(value); + } + remove => MultiplayerEvents.EventManager.Legacy_AfterMainBroadcast.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + MultiplayerEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/NpcListChangedEventArgs.cs b/src/StardewModdingAPI/Events/NpcListChangedEventArgs.cs new file mode 100644 index 00000000..3a37f1e7 --- /dev/null +++ b/src/StardewModdingAPI/Events/NpcListChangedEventArgs.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class NpcListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The NPCs added to the location. + public IEnumerable Added { get; } + + /// The NPCs removed from the location. + public IEnumerable Removed { get; } + + /// Whether this is the location containing the local player. + public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The NPCs added to the location. + /// The NPCs removed from the location. + internal NpcListChangedEventArgs(GameLocation location, IEnumerable added, IEnumerable removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/StardewModdingAPI/Events/ObjectListChangedEventArgs.cs b/src/StardewModdingAPI/Events/ObjectListChangedEventArgs.cs new file mode 100644 index 00000000..b21d2867 --- /dev/null +++ b/src/StardewModdingAPI/Events/ObjectListChangedEventArgs.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class ObjectListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The objects added to the location. + public IEnumerable> Added { get; } + + /// The objects removed from the location. + public IEnumerable> Removed { get; } + + /// Whether this is the location containing the local player. + public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The objects added to the location. + /// The objects removed from the location. + internal ObjectListChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/StardewModdingAPI/Events/OneSecondUpdateTickedEventArgs.cs b/src/StardewModdingAPI/Events/OneSecondUpdateTickedEventArgs.cs new file mode 100644 index 00000000..48e08e5e --- /dev/null +++ b/src/StardewModdingAPI/Events/OneSecondUpdateTickedEventArgs.cs @@ -0,0 +1,26 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class OneSecondUpdateTickedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks => SGame.TicksElapsed; + + + /********* + ** Public methods + *********/ + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/StardewModdingAPI/Events/OneSecondUpdateTickingEventArgs.cs b/src/StardewModdingAPI/Events/OneSecondUpdateTickingEventArgs.cs new file mode 100644 index 00000000..58cf802a --- /dev/null +++ b/src/StardewModdingAPI/Events/OneSecondUpdateTickingEventArgs.cs @@ -0,0 +1,26 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class OneSecondUpdateTickingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, excluding the upcoming tick. + public uint Ticks => SGame.TicksElapsed; + + + /********* + ** Public methods + *********/ + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/StardewModdingAPI/Events/PeerContextReceivedEventArgs.cs b/src/StardewModdingAPI/Events/PeerContextReceivedEventArgs.cs new file mode 100644 index 00000000..151a295c --- /dev/null +++ b/src/StardewModdingAPI/Events/PeerContextReceivedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class PeerContextReceivedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The peer whose metadata was received. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The peer whose metadata was received. + internal PeerContextReceivedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/StardewModdingAPI/Events/PeerDisconnectedEventArgs.cs b/src/StardewModdingAPI/Events/PeerDisconnectedEventArgs.cs new file mode 100644 index 00000000..8517988a --- /dev/null +++ b/src/StardewModdingAPI/Events/PeerDisconnectedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class PeerDisconnectedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The peer who disconnected. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The peer who disconnected. + internal PeerDisconnectedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs new file mode 100644 index 00000000..11ba1e54 --- /dev/null +++ b/src/StardewModdingAPI/Events/PlayerEvents.cs @@ -0,0 +1,68 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the player data changes. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class PlayerEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). + public static event EventHandler InventoryChanged + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + PlayerEvents.EventManager.Legacy_InventoryChanged.Add(value); + } + remove => PlayerEvents.EventManager.Legacy_InventoryChanged.Remove(value); + } + + /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + public static event EventHandler LeveledUp + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + PlayerEvents.EventManager.Legacy_LeveledUp.Add(value); + } + remove => PlayerEvents.EventManager.Legacy_LeveledUp.Remove(value); + } + + /// Raised after the player warps to a new location. + public static event EventHandler Warped + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + PlayerEvents.EventManager.Legacy_PlayerWarped.Add(value); + } + remove => PlayerEvents.EventManager.Legacy_PlayerWarped.Remove(value); + } + + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + PlayerEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/RenderedActiveMenuEventArgs.cs b/src/StardewModdingAPI/Events/RenderedActiveMenuEventArgs.cs new file mode 100644 index 00000000..efd4163b --- /dev/null +++ b/src/StardewModdingAPI/Events/RenderedActiveMenuEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedActiveMenuEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/StardewModdingAPI/Events/RenderedEventArgs.cs b/src/StardewModdingAPI/Events/RenderedEventArgs.cs new file mode 100644 index 00000000..d6341b19 --- /dev/null +++ b/src/StardewModdingAPI/Events/RenderedEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/StardewModdingAPI/Events/RenderedHudEventArgs.cs b/src/StardewModdingAPI/Events/RenderedHudEventArgs.cs new file mode 100644 index 00000000..46e89013 --- /dev/null +++ b/src/StardewModdingAPI/Events/RenderedHudEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedHudEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/StardewModdingAPI/Events/RenderedWorldEventArgs.cs b/src/StardewModdingAPI/Events/RenderedWorldEventArgs.cs new file mode 100644 index 00000000..56145381 --- /dev/null +++ b/src/StardewModdingAPI/Events/RenderedWorldEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderedWorldEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/StardewModdingAPI/Events/RenderingActiveMenuEventArgs.cs b/src/StardewModdingAPI/Events/RenderingActiveMenuEventArgs.cs new file mode 100644 index 00000000..103f56df --- /dev/null +++ b/src/StardewModdingAPI/Events/RenderingActiveMenuEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingActiveMenuEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/StardewModdingAPI/Events/RenderingEventArgs.cs b/src/StardewModdingAPI/Events/RenderingEventArgs.cs new file mode 100644 index 00000000..5acbef09 --- /dev/null +++ b/src/StardewModdingAPI/Events/RenderingEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/StardewModdingAPI/Events/RenderingHudEventArgs.cs b/src/StardewModdingAPI/Events/RenderingHudEventArgs.cs new file mode 100644 index 00000000..84c96ecd --- /dev/null +++ b/src/StardewModdingAPI/Events/RenderingHudEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingHudEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/StardewModdingAPI/Events/RenderingWorldEventArgs.cs b/src/StardewModdingAPI/Events/RenderingWorldEventArgs.cs new file mode 100644 index 00000000..d0d44789 --- /dev/null +++ b/src/StardewModdingAPI/Events/RenderingWorldEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class RenderingWorldEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The sprite batch being drawn. Add anything you want to appear on-screen to this sprite batch. + public SpriteBatch SpriteBatch => Game1.spriteBatch; + } +} diff --git a/src/StardewModdingAPI/Events/ReturnedToTitleEventArgs.cs b/src/StardewModdingAPI/Events/ReturnedToTitleEventArgs.cs new file mode 100644 index 00000000..96309cde --- /dev/null +++ b/src/StardewModdingAPI/Events/ReturnedToTitleEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class ReturnedToTitleEventArgs : EventArgs { } +} diff --git a/src/StardewModdingAPI/Events/SaveCreatedEventArgs.cs b/src/StardewModdingAPI/Events/SaveCreatedEventArgs.cs new file mode 100644 index 00000000..5ae22531 --- /dev/null +++ b/src/StardewModdingAPI/Events/SaveCreatedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SaveCreatedEventArgs : EventArgs { } +} diff --git a/src/StardewModdingAPI/Events/SaveCreatingEventArgs.cs b/src/StardewModdingAPI/Events/SaveCreatingEventArgs.cs new file mode 100644 index 00000000..3c83f421 --- /dev/null +++ b/src/StardewModdingAPI/Events/SaveCreatingEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SaveCreatingEventArgs : EventArgs { } +} diff --git a/src/StardewModdingAPI/Events/SaveEvents.cs b/src/StardewModdingAPI/Events/SaveEvents.cs new file mode 100644 index 00000000..da276d22 --- /dev/null +++ b/src/StardewModdingAPI/Events/SaveEvents.cs @@ -0,0 +1,100 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised before and after the player saves/loads the game. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class SaveEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised before the game creates the save file. + public static event EventHandler BeforeCreate + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + SaveEvents.EventManager.Legacy_BeforeCreateSave.Add(value); + } + remove => SaveEvents.EventManager.Legacy_BeforeCreateSave.Remove(value); + } + + /// Raised after the game finishes creating the save file. + public static event EventHandler AfterCreate + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + SaveEvents.EventManager.Legacy_AfterCreateSave.Add(value); + } + remove => SaveEvents.EventManager.Legacy_AfterCreateSave.Remove(value); + } + + /// Raised before the game begins writes data to the save file. + public static event EventHandler BeforeSave + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + SaveEvents.EventManager.Legacy_BeforeSave.Add(value); + } + remove => SaveEvents.EventManager.Legacy_BeforeSave.Remove(value); + } + + /// Raised after the game finishes writing data to the save file. + public static event EventHandler AfterSave + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + SaveEvents.EventManager.Legacy_AfterSave.Add(value); + } + remove => SaveEvents.EventManager.Legacy_AfterSave.Remove(value); + } + + /// Raised after the player loads a save slot. + public static event EventHandler AfterLoad + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + SaveEvents.EventManager.Legacy_AfterLoad.Add(value); + } + remove => SaveEvents.EventManager.Legacy_AfterLoad.Remove(value); + } + + /// Raised after the game returns to the title screen. + public static event EventHandler AfterReturnToTitle + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + SaveEvents.EventManager.Legacy_AfterReturnToTitle.Add(value); + } + remove => SaveEvents.EventManager.Legacy_AfterReturnToTitle.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + SaveEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/SaveLoadedEventArgs.cs b/src/StardewModdingAPI/Events/SaveLoadedEventArgs.cs new file mode 100644 index 00000000..f8aaa7f7 --- /dev/null +++ b/src/StardewModdingAPI/Events/SaveLoadedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SaveLoadedEventArgs : EventArgs { } +} diff --git a/src/StardewModdingAPI/Events/SavedEventArgs.cs b/src/StardewModdingAPI/Events/SavedEventArgs.cs new file mode 100644 index 00000000..a4e90729 --- /dev/null +++ b/src/StardewModdingAPI/Events/SavedEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SavedEventArgs : EventArgs { } +} diff --git a/src/StardewModdingAPI/Events/SavingEventArgs.cs b/src/StardewModdingAPI/Events/SavingEventArgs.cs new file mode 100644 index 00000000..f323ca9e --- /dev/null +++ b/src/StardewModdingAPI/Events/SavingEventArgs.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class SavingEventArgs : EventArgs { } +} diff --git a/src/StardewModdingAPI/Events/SpecialisedEvents.cs b/src/StardewModdingAPI/Events/SpecialisedEvents.cs new file mode 100644 index 00000000..4f16e4da --- /dev/null +++ b/src/StardewModdingAPI/Events/SpecialisedEvents.cs @@ -0,0 +1,45 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events serving specialised edge cases that shouldn't be used by most mods. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class SpecialisedEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised when the game updates its state (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this method will trigger a stability warning in the SMAPI console. + public static event EventHandler UnvalidatedUpdateTick + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + SpecialisedEvents.EventManager.Legacy_UnvalidatedUpdateTick.Add(value); + } + remove => SpecialisedEvents.EventManager.Legacy_UnvalidatedUpdateTick.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + SpecialisedEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/TerrainFeatureListChangedEventArgs.cs b/src/StardewModdingAPI/Events/TerrainFeatureListChangedEventArgs.cs new file mode 100644 index 00000000..cdf1e6dc --- /dev/null +++ b/src/StardewModdingAPI/Events/TerrainFeatureListChangedEventArgs.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewValley; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for a event. + public class TerrainFeatureListChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The location which changed. + public GameLocation Location { get; } + + /// The terrain features added to the location. + public IEnumerable> Added { get; } + + /// The terrain features removed from the location. + public IEnumerable> Removed { get; } + + /// Whether this is the location containing the local player. + public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location which changed. + /// The terrain features added to the location. + /// The terrain features removed from the location. + internal TerrainFeatureListChangedEventArgs(GameLocation location, IEnumerable> added, IEnumerable> removed) + { + this.Location = location; + this.Added = added.ToArray(); + this.Removed = removed.ToArray(); + } + } +} diff --git a/src/StardewModdingAPI/Events/TimeChangedEventArgs.cs b/src/StardewModdingAPI/Events/TimeChangedEventArgs.cs new file mode 100644 index 00000000..d8349bd8 --- /dev/null +++ b/src/StardewModdingAPI/Events/TimeChangedEventArgs.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class TimeChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous time of day in 24-hour notation (like 1600 for 4pm). The clock time resets when the player sleeps, so 2am (before sleeping) is 2600. + public int OldTime { get; } + + /// The current time of day in 24-hour notation (like 1600 for 4pm). The clock time resets when the player sleeps, so 2am (before sleeping) is 2600. + public int NewTime { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous time of day in 24-hour notation (like 1600 for 4pm). + /// The current time of day in 24-hour notation (like 1600 for 4pm). + internal TimeChangedEventArgs(int oldTime, int newTime) + { + this.OldTime = oldTime; + this.NewTime = newTime; + } + } +} diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs new file mode 100644 index 00000000..389532d9 --- /dev/null +++ b/src/StardewModdingAPI/Events/TimeEvents.cs @@ -0,0 +1,56 @@ +#if !SMAPI_3_0_STRICT +using System; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the in-game date or time changes. + [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] + public static class TimeEvents + { + /********* + ** Fields + *********/ + /// The core event manager. + private static EventManager EventManager; + + + /********* + ** Events + *********/ + /// Raised after the game begins a new day, including when loading a save. + public static event EventHandler AfterDayStarted + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + TimeEvents.EventManager.Legacy_AfterDayStarted.Add(value); + } + remove => TimeEvents.EventManager.Legacy_AfterDayStarted.Remove(value); + } + + /// Raised after the in-game clock changes. + public static event EventHandler TimeOfDayChanged + { + add + { + SCore.DeprecationManager.WarnForOldEvents(); + TimeEvents.EventManager.Legacy_TimeOfDayChanged.Add(value); + } + remove => TimeEvents.EventManager.Legacy_TimeOfDayChanged.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Initialise the events. + /// The core event manager. + internal static void Init(EventManager eventManager) + { + TimeEvents.EventManager = eventManager; + } + } +} +#endif diff --git a/src/StardewModdingAPI/Events/UnvalidatedUpdateTickedEventArgs.cs b/src/StardewModdingAPI/Events/UnvalidatedUpdateTickedEventArgs.cs new file mode 100644 index 00000000..13c367a0 --- /dev/null +++ b/src/StardewModdingAPI/Events/UnvalidatedUpdateTickedEventArgs.cs @@ -0,0 +1,29 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class UnvalidatedUpdateTickedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks => SGame.TicksElapsed; + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond => this.Ticks % 60 == 0; + + + /********* + ** Public methods + *********/ + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/StardewModdingAPI/Events/UnvalidatedUpdateTickingEventArgs.cs b/src/StardewModdingAPI/Events/UnvalidatedUpdateTickingEventArgs.cs new file mode 100644 index 00000000..c2e60f25 --- /dev/null +++ b/src/StardewModdingAPI/Events/UnvalidatedUpdateTickingEventArgs.cs @@ -0,0 +1,29 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class UnvalidatedUpdateTickingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, excluding the upcoming tick. + public uint Ticks => SGame.TicksElapsed; + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond => this.Ticks % 60 == 0; + + + /********* + ** Public methods + *********/ + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/StardewModdingAPI/Events/UpdateTickedEventArgs.cs b/src/StardewModdingAPI/Events/UpdateTickedEventArgs.cs new file mode 100644 index 00000000..4f3329ac --- /dev/null +++ b/src/StardewModdingAPI/Events/UpdateTickedEventArgs.cs @@ -0,0 +1,29 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class UpdateTickedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, including the current tick. + public uint Ticks => SGame.TicksElapsed; + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond => this.Ticks % 60 == 0; + + + /********* + ** Public methods + *********/ + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/StardewModdingAPI/Events/UpdateTickingEventArgs.cs b/src/StardewModdingAPI/Events/UpdateTickingEventArgs.cs new file mode 100644 index 00000000..0d3187cd --- /dev/null +++ b/src/StardewModdingAPI/Events/UpdateTickingEventArgs.cs @@ -0,0 +1,29 @@ +using System; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class UpdateTickingEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The number of ticks elapsed since the game started, excluding the upcoming tick. + public uint Ticks => SGame.TicksElapsed; + + /// Whether is a multiple of 60, which happens approximately once per second. + public bool IsOneSecond => this.Ticks % 60 == 0; + + + /********* + ** Public methods + *********/ + /// Get whether is a multiple of the given . This is mainly useful if you want to run logic intermittently (e.g. e.IsMultipleOf(30) for every half-second). + /// The factor to check. + public bool IsMultipleOf(uint number) + { + return this.Ticks % number == 0; + } + } +} diff --git a/src/StardewModdingAPI/Events/WarpedEventArgs.cs b/src/StardewModdingAPI/Events/WarpedEventArgs.cs new file mode 100644 index 00000000..95c53ad9 --- /dev/null +++ b/src/StardewModdingAPI/Events/WarpedEventArgs.cs @@ -0,0 +1,40 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class WarpedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The player who warped to a new location. + public Farmer Player { get; } + + /// The player's previous location. + public GameLocation OldLocation { get; } + + /// The player's current location. + public GameLocation NewLocation { get; } + + /// Whether the affected player is the local one. + public bool IsLocalPlayer => this.Player.IsLocalPlayer; + + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player who warped to a new location. + /// The player's previous location. + /// The player's current location. + internal WarpedEventArgs(Farmer player, GameLocation oldLocation, GameLocation newLocation) + { + this.Player = player; + this.NewLocation = newLocation; + this.OldLocation = oldLocation; + } + } +} diff --git a/src/StardewModdingAPI/Events/WindowResizedEventArgs.cs b/src/StardewModdingAPI/Events/WindowResizedEventArgs.cs new file mode 100644 index 00000000..1852636a --- /dev/null +++ b/src/StardewModdingAPI/Events/WindowResizedEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class WindowResizedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous window size. + public Point OldSize { get; } + + /// The current window size. + public Point NewSize { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous window size. + /// The current window size. + internal WindowResizedEventArgs(Point oldSize, Point newSize) + { + this.OldSize = oldSize; + this.NewSize = newSize; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Command.cs b/src/StardewModdingAPI/Framework/Command.cs new file mode 100644 index 00000000..8c9df47d --- /dev/null +++ b/src/StardewModdingAPI/Framework/Command.cs @@ -0,0 +1,40 @@ +using System; + +namespace StardewModdingAPI.Framework +{ + /// A command that can be submitted through the SMAPI console to interact with SMAPI. + internal class Command + { + /********* + ** Accessor + *********/ + /// The mod that registered the command (or null if registered by SMAPI). + public IModMetadata Mod { get; } + + /// The command name, which the user must type to trigger it. + public string Name { get; } + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + public string Documentation { get; } + + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + public Action Callback { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod that registered the command (or null if registered by SMAPI). + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + public Command(IModMetadata mod, string name, string documentation, Action callback) + { + this.Mod = mod; + this.Name = name; + this.Documentation = documentation; + this.Callback = callback; + } + } +} diff --git a/src/StardewModdingAPI/Framework/CommandManager.cs b/src/StardewModdingAPI/Framework/CommandManager.cs new file mode 100644 index 00000000..fdaafff1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/CommandManager.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace StardewModdingAPI.Framework +{ + /// Manages console commands. + internal class CommandManager + { + /********* + ** Fields + *********/ + /// The commands registered with SMAPI. + private readonly IDictionary Commands = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Public methods + *********/ + /// Add a console command. + /// The mod adding the command (or null for a SMAPI command). + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + /// Whether to allow a null argument; this should only used for backwards compatibility. + /// The or is null or empty. + /// The is not a valid format. + /// There's already a command with that name. + public void Add(IModMetadata mod, string name, string documentation, Action callback, bool allowNullCallback = false) + { + name = this.GetNormalisedName(name); + + // 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) + throw new ArgumentNullException(nameof(callback), $"Can't register the '{name}' command because without a callback."); + + // ensure uniqueness + if (this.Commands.ContainsKey(name)) + throw new ArgumentException(nameof(callback), $"Can't register the '{name}' command because there's already a command with that name."); + + // add command + this.Commands.Add(name, new Command(mod, name, documentation, callback)); + } + + /// Get a command by its unique name. + /// The command name. + /// Returns the matching command, or null if not found. + public Command Get(string name) + { + name = this.GetNormalisedName(name); + this.Commands.TryGetValue(name, out Command command); + return command; + } + + /// Get all registered commands. + public IEnumerable GetAll() + { + return this.Commands + .Values + .OrderBy(p => p.Name); + } + + /// Try to parse a raw line of user input into an executable command. + /// The raw user input. + /// The parsed command name. + /// The parsed command arguments. + /// The command which can handle the input. + /// Returns true if the input was successfully parsed and matched to a command; else false. + public bool TryParse(string input, out string name, out string[] args, out Command command) + { + // ignore if blank + if (string.IsNullOrWhiteSpace(input)) + { + name = null; + args = null; + command = null; + return false; + } + + // parse input + args = this.ParseArgs(input); + name = this.GetNormalisedName(args[0]); + args = args.Skip(1).ToArray(); + + // get command + return this.Commands.TryGetValue(name, out command); + } + + /// Trigger a command. + /// The command name. + /// The command arguments. + /// Returns whether a matching command was triggered. + public bool Trigger(string name, string[] arguments) + { + // get normalised name + name = this.GetNormalisedName(name); + if (name == null) + return false; + + // get command + if (this.Commands.TryGetValue(name, out Command command)) + { + command.Callback.Invoke(name, arguments); + return true; + } + return false; + } + + + /********* + ** Private methods + *********/ + /// Parse a string into command arguments. + /// The string to parse. + private string[] ParseArgs(string input) + { + bool inQuotes = false; + IList args = new List(); + StringBuilder currentArg = new StringBuilder(); + foreach (char ch in input) + { + if (ch == '"') + inQuotes = !inQuotes; + else if (!inQuotes && char.IsWhiteSpace(ch)) + { + args.Add(currentArg.ToString()); + currentArg.Clear(); + } + else + currentArg.Append(ch); + } + + args.Add(currentArg.ToString()); + + return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray(); + } + + /// Get a normalised command name. + /// The command name. + private string GetNormalisedName(string name) + { + name = name?.Trim().ToLower(); + return !string.IsNullOrWhiteSpace(name) + ? name + : null; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetData.cs b/src/StardewModdingAPI/Framework/Content/AssetData.cs new file mode 100644 index 00000000..553404d3 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetData.cs @@ -0,0 +1,54 @@ +using System; + +namespace StardewModdingAPI.Framework.Content +{ + /// Base implementation for a content helper which encapsulates access and changes to content being read from a data file. + /// The interface value type. + internal class AssetData : AssetInfo, IAssetData + { + /********* + ** Fields + *********/ + /// A callback to invoke when the data is replaced (if any). + private readonly Action OnDataReplaced; + + + /********* + ** Accessors + *********/ + /// The content data being read. + public TValue Data { get; protected set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + /// A callback to invoke when the data is replaced (if any). + public AssetData(string locale, string assetName, TValue data, Func getNormalisedPath, Action onDataReplaced) + : base(locale, assetName, data.GetType(), getNormalisedPath) + { + this.Data = data; + this.OnDataReplaced = onDataReplaced; + } + + /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. + /// The new content value. + /// The is null. + /// The 's type is not compatible with the loaded asset's type. + public void ReplaceWith(TValue value) + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Can't set a loaded asset to a null value."); + if (!this.DataType.IsInstanceOfType(value)) + throw new InvalidCastException($"Can't replace loaded asset of type {this.GetFriendlyTypeName(this.DataType)} with value of type {this.GetFriendlyTypeName(value.GetType())}. The new type must be compatible to prevent game errors."); + + this.Data = value; + this.OnDataReplaced?.Invoke(value); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs new file mode 100644 index 00000000..11a2564c --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + internal class AssetDataForDictionary : AssetData>, IAssetDataForDictionary + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + /// A callback to invoke when the data is replaced (if any). + public AssetDataForDictionary(string locale, string assetName, IDictionary data, Func getNormalisedPath, Action> onDataReplaced) + : base(locale, assetName, data, getNormalisedPath, onDataReplaced) { } + +#if !SMAPI_3_0_STRICT + /// Add or replace an entry in the dictionary. + /// The entry key. + /// The entry value. + [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] + public void Set(TKey key, TValue value) + { + SCore.DeprecationManager.Warn($"AssetDataForDictionary.{nameof(Set)}", "2.10", DeprecationLevel.PendingRemoval); + this.Data[key] = value; + } + + /// Add or replace an entry in the dictionary. + /// The entry key. + /// A callback which accepts the current value and returns the new value. + [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] + public void Set(TKey key, Func value) + { + SCore.DeprecationManager.Warn($"AssetDataForDictionary.{nameof(Set)}", "2.10", DeprecationLevel.PendingRemoval); + this.Data[key] = value(this.Data[key]); + } + + /// Dynamically replace values in the dictionary. + /// A lambda which takes the current key and value for an entry, and returns the new value. + [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] + public void Set(Func replacer) + { + SCore.DeprecationManager.Warn($"AssetDataForDictionary.{nameof(Set)}", "2.10", DeprecationLevel.PendingRemoval); + foreach (var pair in this.Data.ToArray()) + this.Data[pair.Key] = replacer(pair.Key, pair.Value); + } +#endif + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs new file mode 100644 index 00000000..f2d21b5e --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs @@ -0,0 +1,106 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to image content being read from a data file. + internal class AssetDataForImage : AssetData, IAssetDataForImage + { + /********* + ** Fields + *********/ + /// The minimum value to consider non-transparent. + /// On Linux/Mac, fully transparent pixels may have an alpha up to 4 for some reason. + private const byte MinOpacity = 5; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + /// A callback to invoke when the data is replaced (if any). + public AssetDataForImage(string locale, string assetName, Texture2D data, Func getNormalisedPath, Action onDataReplaced) + : base(locale, assetName, data, getNormalisedPath, onDataReplaced) { } + + /// Overwrite part of the image. + /// The image to patch into the content. + /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. + /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. + /// Indicates how an image should be patched. + /// One of the arguments is null. + /// The is outside the bounds of the spritesheet. + public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) + { + // get texture + if (source == null) + throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); + Texture2D target = this.Data; + + // get areas + sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); + targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); + + // validate + if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) + throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); + if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) + throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); + if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) + throw new InvalidOperationException("The source and target areas must be the same size."); + + // get source data + int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; + Color[] sourceData = new Color[pixelCount]; + source.GetData(0, sourceArea, sourceData, 0, pixelCount); + + // merge data in overlay mode + if (patchMode == PatchMode.Overlay) + { + // get target data + Color[] targetData = new Color[pixelCount]; + target.GetData(0, targetArea, targetData, 0, pixelCount); + + // merge pixels + Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; + target.GetData(0, targetArea, newData, 0, newData.Length); + for (int i = 0; i < sourceData.Length; i++) + { + Color above = sourceData[i]; + Color below = targetData[i]; + + // shortcut transparency + if (above.A < AssetDataForImage.MinOpacity) + continue; + if (below.A < AssetDataForImage.MinOpacity) + { + newData[i] = above; + continue; + } + + // merge pixels + // This performs a conventional alpha blend for the pixels, which are already + // premultiplied by the content pipeline. The formula is derived from + // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. + // Note: don't use named arguments here since they're different between + // Linux/Mac and Windows. + float alphaBelow = 1 - (above.A / 255f); + newData[i] = new Color( + (int)(above.R + (below.R * alphaBelow)), // r + (int)(above.G + (below.G * alphaBelow)), // g + (int)(above.B + (below.B * alphaBelow)), // b + Math.Max(above.A, below.A) // a + ); + } + sourceData = newData; + } + + // patch target texture + target.SetData(0, targetArea, sourceData, 0, pixelCount); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs new file mode 100644 index 00000000..90f9e2d4 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to content being read from a data file. + internal class AssetDataForObject : AssetData, IAssetData + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForObject(string locale, string assetName, object data, Func getNormalisedPath) + : base(locale, assetName, data, getNormalisedPath, onDataReplaced: null) { } + + /// Construct an instance. + /// The asset metadata. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForObject(IAssetInfo info, object data, Func getNormalisedPath) + : this(info.Locale, info.AssetName, data, getNormalisedPath) { } + + /// Get a helper to manipulate the data as a dictionary. + /// The expected dictionary key. + /// The expected dictionary balue. + /// The content being read isn't a dictionary. + public IAssetDataForDictionary AsDictionary() + { + return new AssetDataForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalisedPath, this.ReplaceWith); + } + + /// Get a helper to manipulate the data as an image. + /// The content being read isn't an image. + public IAssetDataForImage AsImage() + { + return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalisedPath, this.ReplaceWith); + } + + /// Get the data as a given type. + /// The expected data type. + /// The data can't be converted to . + public TData GetData() + { + if (!(this.Data is TData)) + throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}."); + return (TData)this.Data; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs b/src/StardewModdingAPI/Framework/Content/AssetInfo.cs new file mode 100644 index 00000000..e5211290 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetInfo.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + internal class AssetInfo : IAssetInfo + { + /********* + ** Fields + *********/ + /// Normalises an asset key to match the cache key. + protected readonly Func GetNormalisedPath; + + + /********* + ** Accessors + *********/ + /// The content's locale code, if the content is localised. + public string Locale { get; } + + /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + public string AssetName { get; } + + /// The content data type. + public Type DataType { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content type being read. + /// Normalises an asset key to match the cache key. + public AssetInfo(string locale, string assetName, Type type, Func getNormalisedPath) + { + this.Locale = locale; + this.AssetName = assetName; + this.DataType = type; + this.GetNormalisedPath = getNormalisedPath; + } + + /// Get whether the asset name being loaded matches a given name after normalisation. + /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). + public bool AssetNameEquals(string path) + { + path = this.GetNormalisedPath(path); + return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase); + } + + + /********* + ** Protected methods + *********/ + /// Get a human-readable type name. + /// The type to name. + protected string GetFriendlyTypeName(Type type) + { + // dictionary + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + Type[] genericArgs = type.GetGenericArguments(); + return $"Dictionary<{this.GetFriendlyTypeName(genericArgs[0])}, {this.GetFriendlyTypeName(genericArgs[1])}>"; + } + + // texture + if (type == typeof(Texture2D)) + return type.Name; + + // native type + if (type == typeof(int)) + return "int"; + if (type == typeof(string)) + return "string"; + + // default + return type.FullName; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/ContentCache.cs b/src/StardewModdingAPI/Framework/Content/ContentCache.cs new file mode 100644 index 00000000..55a96ed2 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/ContentCache.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Framework.Content +{ + /// A low-level wrapper around the content cache which handles reading, writing, and invalidating entries in the cache. This doesn't handle any higher-level logic like localisation, loading content, etc. It assumes all keys passed in are already normalised. + internal class ContentCache + { + /********* + ** Fields + *********/ + /// The underlying asset cache. + private readonly IDictionary Cache; + + /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. + private readonly Func NormaliseAssetNameForPlatform; + + + /********* + ** Accessors + *********/ + /// Get or set the value of a raw cache entry. + /// The cache key. + public object this[string key] + { + get => this.Cache[key]; + set => this.Cache[key] = value; + } + + /// The current cache keys. + public IEnumerable Keys => this.Cache.Keys; + + + /********* + ** Public methods + *********/ + /**** + ** Constructor + ****/ + /// Construct an instance. + /// The underlying content manager whose cache to manage. + /// Simplifies access to private game code. + public ContentCache(LocalizedContentManager contentManager, Reflector reflection) + { + // init + this.Cache = reflection.GetField>(contentManager, "loadedAssets").GetValue(); + + // get key normalisation logic + if (Constants.Platform == Platform.Windows) + { + IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath"); + this.NormaliseAssetNameForPlatform = path => method.Invoke(path); + } + else + this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic + } + + /**** + ** Fetch + ****/ + /// Get whether the cache contains a given key. + /// The cache key. + public bool ContainsKey(string key) + { + return this.Cache.ContainsKey(key); + } + + + /**** + ** Normalise + ****/ + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + public string NormalisePathSeparators(string path) + { + return PathUtilities.NormalisePathSeparators(path); + } + + /// Normalise a cache key so it's consistent with the underlying cache. + /// The asset key. + [Pure] + public string NormaliseKey(string key) + { + key = this.NormalisePathSeparators(key); + return key.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase) + ? key.Substring(0, key.Length - 4) + : this.NormaliseAssetNameForPlatform(key); + } + + /**** + ** Remove + ****/ + /// Remove an asset with the given key. + /// The cache key. + /// Whether to dispose the entry value, if applicable. + /// Returns the removed key (if any). + public bool Remove(string key, bool dispose) + { + // get entry + if (!this.Cache.TryGetValue(key, out object value)) + return false; + + // dispose & remove entry + if (dispose && value is IDisposable disposable) + disposable.Dispose(); + + return this.Cache.Remove(key); + } + + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the removed keys (if any). + public IEnumerable Remove(Func predicate, bool dispose = false) + { + List removed = new List(); + foreach (string key in this.Cache.Keys.ToArray()) + { + Type type = this.Cache[key].GetType(); + if (predicate(key, type)) + { + this.Remove(key, dispose); + removed.Add(key); + } + } + return removed; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ContentCoordinator.cs b/src/StardewModdingAPI/Framework/ContentCoordinator.cs new file mode 100644 index 00000000..ee654081 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ContentCoordinator.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.ContentManagers; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Metadata; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// The central logic for creating content managers, invalidating caches, and propagating asset changes. + internal class ContentCoordinator : IDisposable + { + /********* + ** Fields + *********/ + /// An asset key prefix for assets from SMAPI mod folders. + private readonly string ManagedPrefix = "SMAPI"; + + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// Provides metadata for core game assets. + private readonly CoreAssetPropagator CoreAssets; + + /// Simplifies access to private code. + private readonly Reflector Reflection; + + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + /// The loaded content managers (including the ). + private readonly IList ContentManagers = new List(); + + /// Whether the content coordinator has been disposed. + private bool IsDisposed; + + + /********* + ** Accessors + *********/ + /// The primary content manager used for most assets. + public GameContentManager MainContentManager { get; private set; } + + /// The current language as a constant. + public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language; + + /// Interceptors which provide the initial versions of matching assets. + public IDictionary> Loaders { get; } = new Dictionary>(); + + /// Interceptors which edit matching assets after they're loaded. + public IDictionary> Editors { get; } = new Dictionary>(); + + /// The absolute path to the . + public string FullRootDirectory { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// Encapsulates SMAPI's JSON file parsing. + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper) + { + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.Reflection = reflection; + this.JsonHelper = jsonHelper; + this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory); + this.ContentManagers.Add( + this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing) + ); + this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormaliseAssetName, reflection, monitor); + } + + /// Get a new content manager which handles reading files from the game content folder with support for interception. + /// A name for the mod manager. Not guaranteed to be unique. + public GameContentManager CreateGameContentManager(string name) + { + GameContentManager manager = new GameContentManager(name, this.MainContentManager.ServiceProvider, this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing); + this.ContentManagers.Add(manager); + return manager; + } + + /// Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files. + /// A name for the mod manager. Not guaranteed to be unique. + /// The root directory to search for content (or null for the default). + public ModContentManager CreateModContentManager(string name, string rootDirectory) + { + ModContentManager manager = new ModContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.JsonHelper, this.OnDisposing); + this.ContentManagers.Add(manager); + return manager; + } + + /// Get the current content locale. + public string GetLocale() + { + return this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode); + } + + /// Get whether this asset is mapped to a mod folder. + /// The asset key. + public bool IsManagedAssetKey(string key) + { + return key.StartsWith(this.ManagedPrefix); + } + + /// Parse a managed SMAPI asset key which maps to a mod folder. + /// The asset key. + /// The unique name for the content manager which should load this asset. + /// The relative path within the mod folder. + /// Returns whether the asset was parsed successfully. + public bool TryParseManagedAssetKey(string key, out string contentManagerID, out string relativePath) + { + contentManagerID = null; + relativePath = null; + + // not a managed asset + if (!key.StartsWith(this.ManagedPrefix)) + return false; + + // parse + string[] parts = PathUtilities.GetSegments(key, 3); + if (parts.Length != 3) // managed key prefix, mod id, relative path + return false; + contentManagerID = Path.Combine(parts[0], parts[1]); + relativePath = parts[2]; + return true; + } + + /// Get the managed asset key prefix for a mod. + /// The mod's unique ID. + public string GetManagedAssetPrefix(string modID) + { + return Path.Combine(this.ManagedPrefix, modID.ToLower()); + } + + /// Get a copy of an asset from a mod folder. + /// The asset type. + /// The internal asset key. + /// The unique name for the content manager which should load this asset. + /// The internal SMAPI asset key. + /// The language code for which to load content. + public T LoadAndCloneManagedAsset(string internalKey, string contentManagerID, string relativePath, LocalizedContentManager.LanguageCode language) + { + // get content manager + IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.Name == contentManagerID); + if (contentManager == null) + throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); + + // get cloned asset + T data = contentManager.Load(internalKey, language); + return contentManager.CloneIfPossible(data); + } + + /// Purge assets from the cache that match one of the interceptors. + /// The asset editors for which to purge matching assets. + /// The asset loaders for which to purge matching assets. + /// Returns the invalidated asset names. + public IEnumerable InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) + { + if (!editors.Any() && !loaders.Any()) + return new string[0]; + + // get CanEdit/Load methods + MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); + MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); + if (canEdit == null || canLoad == null) + throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen + + // invalidate matching keys + return this.InvalidateCache(asset => + { + // check loaders + MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType); + foreach (IAssetLoader loader in loaders) + { + try + { + if ((bool)canLoadGeneric.Invoke(loader, new object[] { asset })) + return true; + } + catch (Exception ex) + { + this.GetModFor(loader).LogAsMod($"Mod failed when checking whether it could load asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + // check editors + MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType); + foreach (IAssetEditor editor in editors) + { + try + { + if ((bool)canEditGeneric.Invoke(editor, new object[] { asset })) + return true; + } + catch (Exception ex) + { + this.GetModFor(editor).LogAsMod($"Mod failed when checking whether it could edit asset '{asset.AssetName}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + // asset not affected by a loader or editor + return false; + }); + } + + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the invalidated asset keys. + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + { + string locale = this.GetLocale(); + return this.InvalidateCache((assetName, type) => + { + IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.AssertAndNormaliseAssetName); + return predicate(info); + }, dispose); + } + + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the invalidated asset names. + public IEnumerable InvalidateCache(Func predicate, bool dispose = false) + { + // invalidate cache + IDictionary removedAssetNames = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (IContentManager contentManager in this.ContentManagers) + { + foreach (Tuple asset in contentManager.InvalidateCache(predicate, dispose)) + removedAssetNames[asset.Item1] = asset.Item2; + } + + // reload core game assets + int reloaded = 0; + foreach (var pair in removedAssetNames) + { + string key = pair.Key; + Type type = pair.Value; + if (this.CoreAssets.Propagate(this.MainContentManager, key, type)) // use an intercepted content manager + reloaded++; + } + + // report result + if (removedAssetNames.Any()) + this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + else + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + + return removedAssetNames.Keys; + } + + /// Dispose held resources. + public void Dispose() + { + if (this.IsDisposed) + return; + this.IsDisposed = true; + + this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace); + foreach (IContentManager contentManager in this.ContentManagers) + contentManager.Dispose(); + this.ContentManagers.Clear(); + this.MainContentManager = null; + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when a content manager is disposed. + /// The content manager being disposed. + private void OnDisposing(IContentManager contentManager) + { + if (this.IsDisposed) + return; + + this.ContentManagers.Remove(contentManager); + } + + /// Get the mod which registered an asset loader. + /// The asset loader. + /// The given loader couldn't be matched to a mod. + private IModMetadata GetModFor(IAssetLoader loader) + { + foreach (var pair in this.Loaders) + { + if (pair.Value.Contains(loader)) + return pair.Key; + } + + throw new KeyNotFoundException("This loader isn't associated with a known mod."); + } + + /// Get the mod which registered an asset editor. + /// The asset editor. + /// The given editor couldn't be matched to a mod. + private IModMetadata GetModFor(IAssetEditor editor) + { + foreach (var pair in this.Editors) + { + if (pair.Value.Contains(editor)) + return pair.Key; + } + + throw new KeyNotFoundException("This editor isn't associated with a known mod."); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ContentManagers/BaseContentManager.cs b/src/StardewModdingAPI/Framework/ContentManagers/BaseContentManager.cs new file mode 100644 index 00000000..0a955eee --- /dev/null +++ b/src/StardewModdingAPI/Framework/ContentManagers/BaseContentManager.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files from a SMAPI mod folder with support for unpacked files. + internal abstract class BaseContentManager : LocalizedContentManager, IContentManager + { + /********* + ** Fields + *********/ + /// The central coordinator which manages content managers. + protected readonly ContentCoordinator Coordinator; + + /// The underlying asset cache. + protected readonly ContentCache Cache; + + /// Encapsulates monitoring and logging. + protected readonly IMonitor Monitor; + + /// Whether the content coordinator has been disposed. + private bool IsDisposed; + + /// A callback to invoke when the content manager is being disposed. + private readonly Action OnDisposing; + + /// The language enum values indexed by locale code. + protected IDictionary LanguageCodes { get; } + + /// Reflector. + protected readonly Reflector Reflector; + + + /********* + ** Accessors + *********/ + /// A name for the mod manager. Not guaranteed to be unique. + public string Name { get; } + + /// The current language as a constant. + public LanguageCode Language => this.GetCurrentLanguage(); + + /// The absolute path to the . + public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); + + /// Whether this content manager is for a mod folder. + public bool IsModContentManager { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// A callback to invoke when the content manager is being disposed. + /// Whether this content manager is for a mod folder. + protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, bool isModFolder) + : base(serviceProvider, rootDirectory, currentCulture) + { + // init + this.Name = name; + this.Coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator)); + this.Cache = new ContentCache(this, reflection); + this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); + this.OnDisposing = onDisposing; + this.Reflector = reflection; + this.IsModContentManager = isModFolder; + + // get asset data + this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T Load(string assetName) + { + return this.Load(assetName, LocalizedContentManager.CurrentLanguageCode); + } + + /// Load the base asset without localisation. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T LoadBase(string assetName) + { + return this.Load(assetName, LanguageCode.en); + } + + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + public void Inject(string assetName, T value) + { + assetName = this.AssertAndNormaliseAssetName(assetName); + this.Cache[assetName] = value; + + } + + /// Get a copy of the given asset if supported. + /// The asset type. + /// The asset to clone. + public T CloneIfPossible(T asset) + { + switch (asset as object) + { + case Texture2D source: + { + int[] pixels = new int[source.Width * source.Height]; + source.GetData(pixels); + + Texture2D clone = new Texture2D(source.GraphicsDevice, source.Width, source.Height); + clone.SetData(pixels); + return (T)(object)clone; + } + + case Dictionary source: + return (T)(object)new Dictionary(source); + + case Dictionary source: + return (T)(object)new Dictionary(source); + + default: + return asset; + } + } + + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + public string NormalisePathSeparators(string path) + { + return this.Cache.NormalisePathSeparators(path); + } + + /// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + public string AssertAndNormaliseAssetName(string assetName) + { + // NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid + // throwing other types like ArgumentException here. + if (string.IsNullOrWhiteSpace(assetName)) + throw new SContentLoadException("The asset key or local path is empty."); + if (assetName.Intersect(Path.GetInvalidPathChars()).Any()) + throw new SContentLoadException("The asset key or local path contains invalid characters."); + + return this.Cache.NormaliseKey(assetName); + } + + /**** + ** Content loading + ****/ + /// Get the current content locale. + public string GetLocale() + { + return this.GetLocale(this.GetCurrentLanguage()); + } + + /// The locale for a language. + /// The language. + public string GetLocale(LanguageCode language) + { + return this.LanguageCodeString(language); + } + + /// Get whether the content manager has already loaded and cached the given asset. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public bool IsLoaded(string assetName) + { + assetName = this.Cache.NormaliseKey(assetName); + return this.IsNormalisedKeyLoaded(assetName); + } + + /// Get the cached asset keys. + public IEnumerable GetAssetKeys() + { + return this.Cache.Keys + .Select(this.GetAssetName) + .Distinct(); + } + + /**** + ** Cache invalidation + ****/ + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the invalidated asset names and types. + public IEnumerable> InvalidateCache(Func predicate, bool dispose = false) + { + Dictionary removeAssetNames = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, type) => + { + this.ParseCacheKey(key, out string assetName, out _); + + if (removeAssetNames.ContainsKey(assetName)) + return true; + if (predicate(assetName, type)) + { + removeAssetNames[assetName] = type; + return true; + } + return false; + }); + + return removeAssetNames.Select(p => Tuple.Create(p.Key, p.Value)); + } + + /// Dispose held resources. + /// Whether the content manager is being disposed (rather than finalized). + protected override void Dispose(bool isDisposing) + { + if (this.IsDisposed) + return; + this.IsDisposed = true; + + this.OnDisposing(this); + base.Dispose(isDisposing); + } + + /// + public override void Unload() + { + if (this.IsDisposed) + return; // base logic doesn't allow unloading twice, which happens due to SMAPI and the game both unloading + + base.Unload(); + } + + + /********* + ** Private methods + *********/ + /// Get the locale codes (like ja-JP) used in asset keys. + private IDictionary GetKeyLocales() + { + // create locale => code map + IDictionary map = new Dictionary(); + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + map[code] = this.GetLocale(code); + + return map; + } + + /// Get the asset name from a cache key. + /// The input cache key. + private string GetAssetName(string cacheKey) + { + this.ParseCacheKey(cacheKey, out string assetName, out string _); + return assetName; + } + + /// Parse a cache key into its component parts. + /// The input cache key. + /// The original asset name. + /// The asset locale code (or null if not localised). + protected void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) + { + // handle localised key + if (!string.IsNullOrWhiteSpace(cacheKey)) + { + int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); + if (lastSepIndex >= 0) + { + string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + if (this.LanguageCodes.ContainsKey(suffix)) + { + assetName = cacheKey.Substring(0, lastSepIndex); + localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + return; + } + } + } + + // handle simple key + assetName = cacheKey; + localeCode = null; + } + + /// Get whether an asset has already been loaded. + /// The normalised asset name. + protected abstract bool IsNormalisedKeyLoaded(string normalisedAssetName); + } +} diff --git a/src/StardewModdingAPI/Framework/ContentManagers/GameContentManager.cs b/src/StardewModdingAPI/Framework/ContentManagers/GameContentManager.cs new file mode 100644 index 00000000..ee940cc7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ContentManagers/GameContentManager.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files from the game content folder with support for interception. + internal class GameContentManager : BaseContentManager + { + /********* + ** Fields + *********/ + /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. + private readonly ContextHash AssetsBeingLoaded = new ContextHash(); + + /// Interceptors which provide the initial versions of matching assets. + private IDictionary> Loaders => this.Coordinator.Loaders; + + /// Interceptors which edit matching assets after they're loaded. + private IDictionary> Editors => this.Coordinator.Editors; + + /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. + private readonly IDictionary IsLocalisableLookup; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// A callback to invoke when the content manager is being disposed. + public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: false) + { + this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The language code for which to load content. + public override T Load(string assetName, LanguageCode language) + { + // normalise asset name + assetName = this.AssertAndNormaliseAssetName(assetName); + if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) + return this.Load(newAssetName, newLanguage); + + // get from cache + if (this.IsLoaded(assetName)) + return base.Load(assetName, language); + + // get managed asset + if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) + { + T managedAsset = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); + this.Inject(assetName, managedAsset); + return managedAsset; + } + + // load asset + T data; + if (this.AssetsBeingLoaded.Contains(assetName)) + { + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName, language); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + string locale = this.GetLocale(language); + IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormaliseAssetName); + IAssetData asset = + this.ApplyLoader(info) + ?? new AssetDataForObject(info, base.Load(assetName, language), this.AssertAndNormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); + } + + // update cache & return data + this.Inject(assetName, data); + return data; + } + + /// Create a new content manager for temporary use. + public override LocalizedContentManager CreateTemporary() + { + return this.Coordinator.CreateGameContentManager("(temporary)"); + } + + + /********* + ** Private methods + *********/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + protected override bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + // default English + if (this.Language == LocalizedContentManager.LanguageCode.en || this.Coordinator.IsManagedAssetKey(normalisedAssetName)) + return this.Cache.ContainsKey(normalisedAssetName); + + // translated + string localeKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; + if (this.IsLocalisableLookup.TryGetValue(localeKey, out bool localisable)) + { + return localisable + ? this.Cache.ContainsKey(localeKey) + : this.Cache.ContainsKey(normalisedAssetName); + } + + // not loaded yet + return false; + } + + /// Parse an asset key that contains an explicit language into its asset name and language, if applicable. + /// The asset key to parse. + /// The asset name without the language code. + /// The language code removed from the asset name. + private bool TryParseExplicitLanguageAssetKey(string rawAsset, out string assetName, out LanguageCode language) + { + if (string.IsNullOrWhiteSpace(rawAsset)) + throw new SContentLoadException("The asset key is empty."); + + // extract language code + int splitIndex = rawAsset.LastIndexOf('.'); + if (splitIndex != -1 && this.LanguageCodes.TryGetValue(rawAsset.Substring(splitIndex + 1), out language)) + { + assetName = rawAsset.Substring(0, splitIndex); + return true; + } + + // no explicit language code found + assetName = rawAsset; + language = this.Language; + return false; + } + + /// Load the initial asset from the registered . + /// The basic asset metadata. + /// Returns the loaded asset metadata, or null if no loader matched. + private IAssetData ApplyLoader(IAssetInfo info) + { + // find matching loaders + var loaders = this.GetInterceptors(this.Loaders) + .Where(entry => + { + try + { + return entry.Value.CanLoad(info); + } + catch (Exception ex) + { + entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return false; + } + }) + .ToArray(); + + // validate loaders + if (!loaders.Any()) + return null; + if (loaders.Length > 1) + { + string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); + this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); + return null; + } + + // fetch asset from loader + IModMetadata mod = loaders[0].Key; + IAssetLoader loader = loaders[0].Value; + T data; + try + { + data = this.CloneIfPossible(loader.Load(info)); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return null; + } + + // validate asset + if (data == null) + { + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); + return null; + } + + // return matched asset + return new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName); + } + + /// Apply any to a loaded asset. + /// The asset type. + /// The basic asset metadata. + /// The loaded asset. + private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) + { + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName); + + // edit asset + foreach (var entry in this.GetInterceptors(this.Editors)) + { + // check for match + IModMetadata mod = entry.Key; + IAssetEditor editor = entry.Value; + try + { + if (!editor.CanEdit(info)) + continue; + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // try edit + object prevAsset = asset.Data; + try + { + editor.Edit(asset); + this.Monitor.Log($"{mod.DisplayName} edited {info.AssetName}.", LogLevel.Trace); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + + // validate edit + if (asset.Data == null) + { + mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + else if (!(asset.Data is T)) + { + mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); + } + } + + // return result + return asset; + } + + /// Get all registered interceptors from a list. + private IEnumerable> GetInterceptors(IDictionary> entries) + { + foreach (var entry in entries) + { + IModMetadata mod = entry.Key; + IList interceptors = entry.Value; + + // registered editors + foreach (T interceptor in interceptors) + yield return new KeyValuePair(mod, interceptor); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/ContentManagers/IContentManager.cs b/src/StardewModdingAPI/Framework/ContentManagers/IContentManager.cs new file mode 100644 index 00000000..17618edd --- /dev/null +++ b/src/StardewModdingAPI/Framework/ContentManagers/IContentManager.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Framework.Exceptions; +using StardewValley; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files. + internal interface IContentManager : IDisposable + { + /********* + ** Accessors + *********/ + /// A name for the mod manager. Not guaranteed to be unique. + string Name { get; } + + /// The current language as a constant. + LocalizedContentManager.LanguageCode Language { get; } + + /// The absolute path to the . + string FullRootDirectory { get; } + + /// Whether this content manager is for a mod folder. + bool IsModContentManager { get; } + + + /********* + ** Methods + *********/ + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + T Load(string assetName); + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The language code for which to load content. + T Load(string assetName, LocalizedContentManager.LanguageCode language); + + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + void Inject(string assetName, T value); + + /// Get a copy of the given asset if supported. + /// The asset type. + /// The asset to clone. + T CloneIfPossible(T asset); + + /// Normalise path separators in a file path. For asset keys, see instead. + /// The file path to normalise. + [Pure] + string NormalisePathSeparators(string path); + + /// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + string AssertAndNormaliseAssetName(string assetName); + + /// Get the current content locale. + string GetLocale(); + + /// The locale for a language. + /// The language. + string GetLocale(LocalizedContentManager.LanguageCode language); + + /// Get whether the content manager has already loaded and cached the given asset. + /// The asset path relative to the loader root directory, not including the .xnb extension. + bool IsLoaded(string assetName); + + /// Get the cached asset keys. + IEnumerable GetAssetKeys(); + + /// Purge matched assets from the cache. + /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. + /// Returns the invalidated asset names and types. + IEnumerable> InvalidateCache(Func predicate, bool dispose = false); + } +} diff --git a/src/StardewModdingAPI/Framework/ContentManagers/ModContentManager.cs b/src/StardewModdingAPI/Framework/ContentManagers/ModContentManager.cs new file mode 100644 index 00000000..9875c424 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ContentManagers/ModContentManager.cs @@ -0,0 +1,285 @@ +using System; +using System.Globalization; +using System.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewValley; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Xna.Framework.Content; + +namespace StardewModdingAPI.Framework.ContentManagers +{ + /// A content manager which handles reading files from a SMAPI mod folder with support for unpacked files. + internal class ModContentManager : BaseContentManager + { + /********* + ** Fields + *********/ + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A name for the mod manager. Not guaranteed to be unique. + /// The service provider to use to locate services. + /// The root directory to search for content. + /// The current culture for which to localise content. + /// The central coordinator which manages content managers. + /// Encapsulates monitoring and logging. + /// Simplifies access to private code. + /// Encapsulates SMAPI's JSON file parsing. + /// A callback to invoke when the content manager is being disposed. + public ModContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing) + : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: true) + { + this.JsonHelper = jsonHelper; + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The language code for which to load content. + public override T Load(string assetName, LanguageCode language) + { + assetName = this.AssertAndNormaliseAssetName(assetName); + + // get from cache + if (this.IsLoaded(assetName)) + return base.Load(assetName, language); + + // get managed asset + if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) + { + if (contentManagerID != this.Name) + { + T data = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); + this.Inject(assetName, data); + return data; + } + + return this.LoadManagedAsset(assetName, contentManagerID, relativePath, language); + } + + throw new NotSupportedException("Can't load content folder asset from a mod content manager."); + } + + /// Create a new content manager for temporary use. + public override LocalizedContentManager CreateTemporary() + { + throw new NotSupportedException("Can't create a temporary mod content manager."); + } + + + /********* + ** Private methods + *********/ + /// Get whether an asset has already been loaded. + /// The normalised asset name. + protected override bool IsNormalisedKeyLoaded(string normalisedAssetName) + { + return this.Cache.ContainsKey(normalisedAssetName); + } + + /// Load a managed mod asset. + /// The type of asset to load. + /// The internal asset key. + /// The unique name for the content manager which should load this asset. + /// The relative path within the mod folder. + /// The language code for which to load content. + private T LoadManagedAsset(string internalKey, string contentManagerID, string relativePath, LanguageCode language) + { + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{relativePath}' from {contentManagerID}: {reasonPhrase}"); + try + { + // get file + FileInfo file = this.GetModFile(relativePath); + if (!file.Exists) + throw GetContentError("the specified path doesn't exist."); + + // load content + switch (file.Extension.ToLower()) + { + // XNB file + case ".xnb": + return this.ModedLoad(relativePath, language); + + // unpacked data + case ".json": + { + if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T data)) + throw GetContentError("the JSON file is invalid."); // should never happen since we check for file existence above + + return data; + } + + // unpacked image + case ".png": + // validate + if (typeof(T) != typeof(Texture2D)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + + // fetch & cache + using (FileStream stream = File.OpenRead(file.FullName)) + { + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + texture = this.PremultiplyTransparency(texture); + this.Inject(internalKey, texture); + return (T)(object)texture; + } + + // unpacked map + case ".tbin": + throw GetContentError($"can't read unpacked map file directly from the underlying content manager. It must be loaded through the mod's {typeof(IModHelper)}.{nameof(IModHelper.Content)} helper."); + + default: + throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); + } + } + catch (Exception ex) when (!(ex is SContentLoadException)) + { + if (ex.GetInnermostException() is DllNotFoundException dllEx && dllEx.Message == "libgdiplus.dylib") + throw GetContentError("couldn't find libgdiplus, which is needed to load mod images. Make sure Mono is installed and you're running the game through the normal launcher."); + throw new SContentLoadException($"The content manager failed loading content asset '{relativePath}' from {contentManagerID}.", ex); + } + } + + /// Get a file from the mod folder. + /// The asset path relative to the content folder. + private FileInfo GetModFile(string path) + { + // try exact match + FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path)); + + // try with default extension + if (!file.Exists && file.Extension.ToLower() != ".xnb") + { + FileInfo result = new FileInfo(file.FullName + ".xnb"); + if (result.Exists) + file = result; + } + + return file; + } + + /// Premultiply a texture's alpha values to avoid transparency issues in the game. + /// The texture to premultiply. + /// Returns a premultiplied texture. + /// Based on code by David Gouveia. + private Texture2D PremultiplyTransparency(Texture2D texture) + { + // Textures loaded by Texture2D.FromStream are already premultiplied on Linux/Mac, even + // though the XNA documentation explicitly says otherwise. That's a glitch in MonoGame + // fixed in newer versions, but the game uses a bundled version that will always be + // affected. See https://github.com/MonoGame/MonoGame/issues/4820 for more info. + if (Constants.TargetPlatform != GamePlatform.Windows) + return texture; + + // premultiply pixels + Color[] data = new Color[texture.Width * texture.Height]; + texture.GetData(data); + for (int i = 0; i < data.Length; i++) + data[i] = Color.FromNonPremultiplied(data[i].ToVector4()); + texture.SetData(data); + return texture; + } + + public T ModedLoad(string assetName, LanguageCode language) + { + if (language != LanguageCode.en) + { + string key = assetName + "." + this.LanguageCodeString(language); + Dictionary _localizedAsset = this.Reflector.GetField>(this, "_localizedAsset").GetValue(); + if (!_localizedAsset.TryGetValue(key, out bool flag) | flag) + { + try + { + _localizedAsset[key] = true; + return this.ModedLoad(key); + } + catch (ContentLoadException) + { + _localizedAsset[key] = false; + } + } + } + return this.ModedLoad(assetName); + } + + public T ModedLoad(string assetName) + { + if (string.IsNullOrEmpty(assetName)) + { + throw new ArgumentNullException("assetName"); + } + T local = default(T); + string key = assetName.Replace('\\', '/'); + Dictionary loadedAssets = this.Reflector.GetField>(this, "loadedAssets").GetValue(); + if (loadedAssets.TryGetValue(key, out object obj2) && (obj2 is T)) + { + return (T)obj2; + } + local = this.ReadAsset(assetName, null); + loadedAssets[key] = local; + return local; + } + + protected override Stream OpenStream(string assetName) + { + Stream stream; + try + { + stream = new FileStream(Path.Combine(this.RootDirectory, assetName) + ".xnb", FileMode.Open, FileAccess.Read); + MemoryStream destination = new MemoryStream(); + stream.CopyTo(destination); + destination.Seek(0L, SeekOrigin.Begin); + stream.Close(); + stream = destination; + } + catch (Exception exception3) + { + throw new ContentLoadException("Opening stream error.", exception3); + } + return stream; + } + protected new T ReadAsset(string assetName, Action recordDisposableObject) + { + if (string.IsNullOrEmpty(assetName)) + { + throw new ArgumentNullException("assetName"); + } + ; + string str = assetName; + object obj2 = null; + if (this.Reflector.GetField(this, "graphicsDeviceService").GetValue() == null) + { + this.Reflector.GetField(this, "graphicsDeviceService").SetValue(this.ServiceProvider.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService); + } + Stream input = this.OpenStream(assetName); + using (BinaryReader reader = new BinaryReader(input)) + { + using (ContentReader reader2 = this.Reflector.GetMethod(this, "GetContentReaderFromXnb").Invoke(assetName, input, reader, recordDisposableObject)) + { + MethodInfo method = reader2.GetType().GetMethod("ReadAsset", BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.NonPublic, null, new Type[] { }, new ParameterModifier[] { }); + obj2 = method.MakeGenericMethod(new Type[] { typeof(T) }).Invoke(reader2, null); + if (obj2 is GraphicsResource graphics) + { + graphics.Name = str; + } + } + } + if (obj2 == null) + { + throw new Exception("Could not load " + str + " asset!"); + } + return (T)obj2; + } + + } +} diff --git a/src/StardewModdingAPI/Framework/ContentPack.cs b/src/StardewModdingAPI/Framework/ContentPack.cs new file mode 100644 index 00000000..e39d03a1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ContentPack.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Utilities; +using xTile; + +namespace StardewModdingAPI.Framework +{ + /// Manages access to a content pack's metadata and files. + internal class ContentPack : IContentPack + { + /********* + ** Fields + *********/ + /// Provides an API for loading content assets. + private readonly IContentHelper Content; + + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + + /********* + ** Accessors + *********/ + /// The full path to the content pack's folder. + public string DirectoryPath { get; } + + /// The content pack's manifest. + public IManifest Manifest { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full path to the content pack's folder. + /// The content pack's manifest. + /// Provides an API for loading content assets. + /// Encapsulates SMAPI's JSON file parsing. + public ContentPack(string directoryPath, IManifest manifest, IContentHelper content, JsonHelper jsonHelper) + { + this.DirectoryPath = directoryPath; + this.Manifest = manifest; + this.Content = content; + this.JsonHelper = jsonHelper; + } + + /// Read a JSON file from the content pack folder. + /// The model type. + /// The file path relative to the contnet directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The is not relative or contains directory climbing (../). + public TModel ReadJsonFile(string path) where TModel : class + { + if (!PathUtilities.IsSafeRelativePath(path)) + throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{nameof(this.ReadJsonFile)} with a relative path."); + + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); + return this.JsonHelper.ReadJsonFileIfExists(path, out TModel model) + ? model + : null; + } + + /// Save data to a JSON file in the content pack's folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the mod folder. + /// The arbitrary data to save. + /// The is not relative or contains directory climbing (../). + public void WriteJsonFile(string path, TModel data) where TModel : class + { + if (!PathUtilities.IsSafeRelativePath(path)) + throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{nameof(this.WriteJsonFile)} with a relative path."); + + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); + this.JsonHelper.WriteJsonFile(path, data); + } + + /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. + /// The local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + public T LoadAsset(string key) + { + return this.Content.Load(key, ContentSource.ModFolder); + } + + /// 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. + /// The the local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + public string GetActualAssetKey(string key) + { + return this.Content.GetActualAssetKey(key, ContentSource.ModFolder); + } + + } +} diff --git a/src/StardewModdingAPI/Framework/CursorPosition.cs b/src/StardewModdingAPI/Framework/CursorPosition.cs new file mode 100644 index 00000000..079917f2 --- /dev/null +++ b/src/StardewModdingAPI/Framework/CursorPosition.cs @@ -0,0 +1,47 @@ +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI.Framework +{ + /// Defines a position on a given map at different reference points. + internal class CursorPosition : ICursorPosition + { + /********* + ** Accessors + *********/ + /// The pixel position relative to the top-left corner of the in-game map. + public Vector2 AbsolutePixels { get; } + + /// The pixel position relative to the top-left corner of the visible screen. + public Vector2 ScreenPixels { get; } + + /// The tile position under the cursor relative to the top-left corner of the map. + public Vector2 Tile { get; } + + /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. + public Vector2 GrabTile { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The pixel position relative to the top-left corner of the in-game map. + /// The pixel position relative to the top-left corner of the visible screen. + /// The tile position relative to the top-left corner of the map. + /// The tile position that the game considers under the cursor for purposes of clicking actions. + public CursorPosition(Vector2 absolutePixels, Vector2 screenPixels, Vector2 tile, Vector2 grabTile) + { + this.AbsolutePixels = absolutePixels; + this.ScreenPixels = screenPixels; + this.Tile = tile; + this.GrabTile = grabTile; + } + + /// Get whether the current object is equal to another object of the same type. + /// An object to compare with this object. + public bool Equals(ICursorPosition other) + { + return other != null && this.AbsolutePixels == other.AbsolutePixels; + } + } +} diff --git a/src/StardewModdingAPI/Framework/DeprecationLevel.cs b/src/StardewModdingAPI/Framework/DeprecationLevel.cs new file mode 100644 index 00000000..c0044053 --- /dev/null +++ b/src/StardewModdingAPI/Framework/DeprecationLevel.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework +{ + /// Indicates how deprecated something is. + internal enum DeprecationLevel + { + /// It's deprecated but won't be removed soon. Mod authors have some time to update their mods. Deprecation warnings should be logged, but not written to the console. + Notice, + + /// Mods should no longer be using it. Deprecation messages should be debug entries in the console. + Info, + + /// The code will be removed soon. Deprecation messages should be warnings in the console. + PendingRemoval + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/StardewModdingAPI/Framework/DeprecationManager.cs new file mode 100644 index 00000000..3153bbb4 --- /dev/null +++ b/src/StardewModdingAPI/Framework/DeprecationManager.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework +{ + /// Manages deprecation warnings. + internal class DeprecationManager + { + /********* + ** Fields + *********/ + /// The deprecations which have already been logged (as 'mod name::noun phrase::version'). + private readonly HashSet LoggedDeprecations = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + /// Encapsulates monitoring and logging for a given module. +#if !SMAPI_3_0_STRICT + private readonly Monitor Monitor; +#else + private readonly IMonitor Monitor; +#endif + + /// Tracks the installed mods. + private readonly ModRegistry ModRegistry; + + /// The queued deprecation warnings to display. + private readonly IList QueuedWarnings = new List(); + +#if !SMAPI_3_0_STRICT + /// Whether the one-time deprecation message has been shown. + private bool DeprecationHeaderShown = false; +#endif + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging for a given module. + /// Tracks the installed mods. +#if !SMAPI_3_0_STRICT + public DeprecationManager(Monitor monitor, ModRegistry modRegistry) +#else + public DeprecationManager(IMonitor monitor, ModRegistry modRegistry) +#endif + { + this.Monitor = monitor; + this.ModRegistry = modRegistry; + } + + /// Log a deprecation warning for the old-style events. + public void WarnForOldEvents() + { + this.Warn("legacy events", "2.9", DeprecationLevel.PendingRemoval); + } + + /// Log a deprecation warning. + /// A noun phrase describing what is deprecated. + /// The SMAPI version which deprecated it. + /// How deprecated the code is. + public void Warn(string nounPhrase, string version, DeprecationLevel severity) + { + this.Warn(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version, severity); + } + + /// Log a deprecation warning. + /// The friendly mod name which used the deprecated code. + /// A noun phrase describing what is deprecated. + /// The SMAPI version which deprecated it. + /// How deprecated the code is. + public void Warn(string source, string nounPhrase, string version, DeprecationLevel severity) + { + // ignore if already warned + if (!this.MarkWarned(source ?? "", nounPhrase, version)) + return; + + // queue warning + this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, Environment.StackTrace)); + } + + /// Print any queued messages. + public void PrintQueued() + { +#if !SMAPI_3_0_STRICT + if (!this.DeprecationHeaderShown && this.QueuedWarnings.Any()) + { + this.Monitor.Newline(); + this.Monitor.Log("Some of your mods will break in the upcoming SMAPI 3.0. Please update your mods now, or notify the author if no update is available. See https://mods.smapi.io for links to the latest versions.", LogLevel.Warn); + this.Monitor.Newline(); + this.DeprecationHeaderShown = true; + } +#endif + + foreach (DeprecationWarning warning in this.QueuedWarnings.OrderBy(p => p.ModName).ThenBy(p => p.NounPhrase)) + { + // build message +#if SMAPI_3_0_STRICT + string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase} is deprecated since SMAPI {warning.Version})."; +#else + string message = warning.NounPhrase == "legacy events" + ? $"{warning.ModName ?? "An unknown mod"} will break in the upcoming SMAPI 3.0 (legacy events are deprecated since SMAPI {warning.Version})." + : $"{warning.ModName ?? "An unknown mod"} will break in the upcoming SMAPI 3.0 ({warning.NounPhrase} is deprecated since SMAPI {warning.Version})."; +#endif + + // get log level + LogLevel level; + switch (warning.Level) + { + case DeprecationLevel.Notice: + level = LogLevel.Trace; + break; + + case DeprecationLevel.Info: + level = LogLevel.Debug; + break; + + case DeprecationLevel.PendingRemoval: + level = LogLevel.Warn; + break; + + default: + throw new NotSupportedException($"Unknown deprecation level '{warning.Level}'."); + } + + // log message + if (warning.ModName != null) + this.Monitor.Log(message, 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); + } + } + } + this.QueuedWarnings.Clear(); + } + + /// Mark a deprecation warning as already logged. + /// A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method"). + /// The SMAPI version which deprecated it. + /// Returns whether the deprecation was successfully marked as warned. Returns false if it was already marked. + public bool MarkWarned(string nounPhrase, string version) + { + return this.MarkWarned(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version); + } + + /// Mark a deprecation warning as already logged. + /// The friendly name of the assembly which used the deprecated code. + /// A noun phrase describing what is deprecated (e.g. "the Extensions.AsInt32 method"). + /// The SMAPI version which deprecated it. + /// Returns whether the deprecation was successfully marked as warned. Returns false if it was already marked. + public bool MarkWarned(string source, string nounPhrase, string version) + { + if (string.IsNullOrWhiteSpace(source)) + throw new InvalidOperationException("The deprecation source cannot be empty."); + + string key = $"{source}::{nounPhrase}::{version}"; + if (this.LoggedDeprecations.Contains(key)) + return false; + this.LoggedDeprecations.Add(key); + return true; + } + } +} diff --git a/src/StardewModdingAPI/Framework/DeprecationWarning.cs b/src/StardewModdingAPI/Framework/DeprecationWarning.cs new file mode 100644 index 00000000..5201b06c --- /dev/null +++ b/src/StardewModdingAPI/Framework/DeprecationWarning.cs @@ -0,0 +1,43 @@ +namespace StardewModdingAPI.Framework +{ + /// A deprecation warning for a mod. + internal class DeprecationWarning + { + /********* + ** Accessors + *********/ + /// The affected mod's display name. + public string ModName { get; } + + /// A noun phrase describing what is deprecated. + public string NounPhrase { get; } + + /// The SMAPI version which deprecated it. + public string Version { get; } + + /// The deprecation level for the affected code. + public DeprecationLevel Level { get; } + + /// The stack trace when the deprecation warning was raised. + public string StackTrace { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The affected mod's display name. + /// A noun phrase describing what is deprecated. + /// The SMAPI version which deprecated it. + /// The deprecation level for the affected code. + /// The stack trace when the deprecation warning was raised. + public DeprecationWarning(string modName, string nounPhrase, string version, DeprecationLevel level, string stackTrace) + { + this.ModName = modName; + this.NounPhrase = nounPhrase; + this.Version = version; + this.Level = level; + this.StackTrace = stackTrace; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/EventManager.cs b/src/StardewModdingAPI/Framework/Events/EventManager.cs new file mode 100644 index 00000000..13244601 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/EventManager.cs @@ -0,0 +1,491 @@ +using System.Diagnostics.CodeAnalysis; +#if !SMAPI_3_0_STRICT +using Microsoft.Xna.Framework.Input; +#endif +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Manages SMAPI events. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Private fields are deliberately named to simplify organisation.")] + internal class EventManager + { + /********* + ** Events (new) + *********/ + /**** + ** Display + ****/ + /// Raised after a game menu is opened, closed, or replaced. + public readonly ManagedEvent MenuChanged; + + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. + public readonly ManagedEvent Rendering; + + /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). + public readonly ManagedEvent Rendered; + + /// Raised before the game world is drawn to the screen. + public readonly ManagedEvent RenderingWorld; + + /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. + public readonly ManagedEvent RenderedWorld; + + /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. + public readonly ManagedEvent RenderingActiveMenu; + + /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. + public readonly ManagedEvent RenderedActiveMenu; + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. + public readonly ManagedEvent RenderingHud; + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. + public readonly ManagedEvent RenderedHud; + + /// Raised after the game window is resized. + public readonly ManagedEvent WindowResized; + + /**** + ** Game loop + ****/ + /// Raised after the game is launched, right before the first update tick. + public readonly ManagedEvent GameLaunched; + + /// Raised before the game performs its overall update tick (≈60 times per second). + public readonly ManagedEvent UpdateTicking; + + /// Raised after the game performs its overall update tick (≈60 times per second). + public readonly ManagedEvent UpdateTicked; + + /// Raised once per second before the game performs its overall update tick. + public readonly ManagedEvent OneSecondUpdateTicking; + + /// Raised once per second after the game performs its overall update tick. + public readonly ManagedEvent OneSecondUpdateTicked; + + /// Raised before the game creates the save file. + public readonly ManagedEvent SaveCreating; + + /// Raised after the game finishes creating the save file. + public readonly ManagedEvent SaveCreated; + + /// Raised before the game begins writes data to the save file (except the initial save creation). + public readonly ManagedEvent Saving; + + /// Raised after the game finishes writing data to the save file (except the initial save creation). + public readonly ManagedEvent Saved; + + /// Raised after the player loads a save slot and the world is initialised. + public readonly ManagedEvent SaveLoaded; + + /// Raised after the game begins a new day, including when loading a save. + public readonly ManagedEvent DayStarted; + + /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . + public readonly ManagedEvent DayEnding; + + /// Raised after the in-game clock time changes. + public readonly ManagedEvent TimeChanged; + + /// Raised after the game returns to the title screen. + public readonly ManagedEvent ReturnedToTitle; + + /**** + ** Input + ****/ + /// Raised after the player presses a button on the keyboard, controller, or mouse. + public readonly ManagedEvent ButtonPressed; + + /// Raised after the player released a button on the keyboard, controller, or mouse. + public readonly ManagedEvent ButtonReleased; + + /// Raised after the player moves the in-game cursor. + public readonly ManagedEvent CursorMoved; + + /// Raised after the player scrolls the mouse wheel. + public readonly ManagedEvent MouseWheelScrolled; + + /**** + ** Multiplayer + ****/ + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + public readonly ManagedEvent PeerContextReceived; + + /// Raised after a mod message is received over the network. + public readonly ManagedEvent ModMessageReceived; + + /// Raised after the connection with a peer is severed. + public readonly ManagedEvent PeerDisconnected; + + /**** + ** Player + ****/ + /// Raised after items are added or removed to a player's inventory. + public readonly ManagedEvent InventoryChanged; + + /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + public readonly ManagedEvent LevelChanged; + + /// Raised after a player warps to a new location. + public readonly ManagedEvent Warped; + + /**** + ** World + ****/ + /// Raised after a game location is added or removed. + public readonly ManagedEvent LocationListChanged; + + /// Raised after buildings are added or removed in a location. + public readonly ManagedEvent BuildingListChanged; + + /// Raised after debris are added or removed in a location. + public readonly ManagedEvent DebrisListChanged; + + /// Raised after large terrain features (like bushes) are added or removed in a location. + public readonly ManagedEvent LargeTerrainFeatureListChanged; + + /// Raised after NPCs are added or removed in a location. + public readonly ManagedEvent NpcListChanged; + + /// Raised after objects are added or removed in a location. + public readonly ManagedEvent ObjectListChanged; + + /// Raised after terrain features (like floors and trees) are added or removed in a location. + public readonly ManagedEvent TerrainFeatureListChanged; + + /**** + ** Specialised + ****/ + /// Raised when the low-level stage in the game's loading process has changed. See notes on . + public readonly ManagedEvent LoadStageChanged; + + /// Raised before the game performs its overall update tick (≈60 times per second). See notes on . + public readonly ManagedEvent UnvalidatedUpdateTicking; + + /// Raised after the game performs its overall update tick (≈60 times per second). See notes on . + public readonly ManagedEvent UnvalidatedUpdateTicked; + + +#if !SMAPI_3_0_STRICT + /********* + ** Events (old) + *********/ + /**** + ** ContentEvents + ****/ + /// Raised after the content language changes. + public readonly ManagedEvent> Legacy_LocaleChanged; + + /**** + ** ControlEvents + ****/ + /// Raised when the changes. That happens when the player presses or releases a key. + public readonly ManagedEvent Legacy_KeyboardChanged; + + /// Raised after the player presses a keyboard key. + public readonly ManagedEvent Legacy_KeyPressed; + + /// Raised after the player releases a keyboard key. + public readonly ManagedEvent Legacy_KeyReleased; + + /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. + public readonly ManagedEvent Legacy_MouseChanged; + + /// The player pressed a controller button. This event isn't raised for trigger buttons. + public readonly ManagedEvent Legacy_ControllerButtonPressed; + + /// The player released a controller button. This event isn't raised for trigger buttons. + public readonly ManagedEvent Legacy_ControllerButtonReleased; + + /// The player pressed a controller trigger button. + public readonly ManagedEvent Legacy_ControllerTriggerPressed; + + /// The player released a controller trigger button. + public readonly ManagedEvent Legacy_ControllerTriggerReleased; + + /**** + ** GameEvents + ****/ + /// Raised once after the game initialises and all methods have been called. + public readonly ManagedEvent Legacy_FirstUpdateTick; + + /// Raised when the game updates its state (≈60 times per second). + public readonly ManagedEvent Legacy_UpdateTick; + + /// Raised every other tick (≈30 times per second). + public readonly ManagedEvent Legacy_SecondUpdateTick; + + /// Raised every fourth tick (≈15 times per second). + public readonly ManagedEvent Legacy_FourthUpdateTick; + + /// Raised every eighth tick (≈8 times per second). + public readonly ManagedEvent Legacy_EighthUpdateTick; + + /// Raised every 15th tick (≈4 times per second). + public readonly ManagedEvent Legacy_QuarterSecondTick; + + /// Raised every 30th tick (≈twice per second). + public readonly ManagedEvent Legacy_HalfSecondTick; + + /// Raised every 60th tick (≈once per second). + public readonly ManagedEvent Legacy_OneSecondTick; + + /**** + ** GraphicsEvents + ****/ + /// Raised after the game window is resized. + public readonly ManagedEvent Legacy_Resize; + + /// Raised before drawing the world to the screen. + public readonly ManagedEvent Legacy_OnPreRenderEvent; + + /// Raised after drawing the world to the screen. + public readonly ManagedEvent Legacy_OnPostRenderEvent; + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public readonly ManagedEvent Legacy_OnPreRenderHudEvent; + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) + public readonly ManagedEvent Legacy_OnPostRenderHudEvent; + + /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public readonly ManagedEvent Legacy_OnPreRenderGuiEvent; + + /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. + public readonly ManagedEvent Legacy_OnPostRenderGuiEvent; + + /**** + ** InputEvents + ****/ + /// Raised after the player presses a button on the keyboard, controller, or mouse. + public readonly ManagedEvent Legacy_ButtonPressed; + + /// Raised after the player releases a keyboard key on the keyboard, controller, or mouse. + public readonly ManagedEvent Legacy_ButtonReleased; + + /**** + ** LocationEvents + ****/ + /// Raised after a game location is added or removed. + public readonly ManagedEvent Legacy_LocationsChanged; + + /// Raised after buildings are added or removed in a location. + public readonly ManagedEvent Legacy_BuildingsChanged; + + /// Raised after objects are added or removed in a location. + public readonly ManagedEvent Legacy_ObjectsChanged; + + /**** + ** MenuEvents + ****/ + /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. + public readonly ManagedEvent Legacy_MenuChanged; + + /// Raised after a game menu is closed. + public readonly ManagedEvent Legacy_MenuClosed; + + /**** + ** MultiplayerEvents + ****/ + /// Raised before the game syncs changes from other players. + public readonly ManagedEvent Legacy_BeforeMainSync; + + /// Raised after the game syncs changes from other players. + public readonly ManagedEvent Legacy_AfterMainSync; + + /// Raised before the game broadcasts changes to other players. + public readonly ManagedEvent Legacy_BeforeMainBroadcast; + + /// Raised after the game broadcasts changes to other players. + public readonly ManagedEvent Legacy_AfterMainBroadcast; + + /**** + ** MineEvents + ****/ + /// Raised after the player warps to a new level of the mine. + public readonly ManagedEvent Legacy_MineLevelChanged; + + /**** + ** PlayerEvents + ****/ + /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). + public readonly ManagedEvent Legacy_InventoryChanged; + + /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. + public readonly ManagedEvent Legacy_LeveledUp; + + /// Raised after the player warps to a new location. + public readonly ManagedEvent Legacy_PlayerWarped; + + + /**** + ** SaveEvents + ****/ + /// Raised before the game creates the save file. + public readonly ManagedEvent Legacy_BeforeCreateSave; + + /// Raised after the game finishes creating the save file. + public readonly ManagedEvent Legacy_AfterCreateSave; + + /// Raised before the game begins writes data to the save file. + public readonly ManagedEvent Legacy_BeforeSave; + + /// Raised after the game finishes writing data to the save file. + public readonly ManagedEvent Legacy_AfterSave; + + /// Raised after the player loads a save slot. + public readonly ManagedEvent Legacy_AfterLoad; + + /// Raised after the game returns to the title screen. + public readonly ManagedEvent Legacy_AfterReturnToTitle; + + /**** + ** SpecialisedEvents + ****/ + /// Raised when the game updates its state (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this method will trigger a stability warning in the SMAPI console. + public readonly ManagedEvent Legacy_UnvalidatedUpdateTick; + + /**** + ** TimeEvents + ****/ + /// Raised after the game begins a new day, including when loading a save. + public readonly ManagedEvent Legacy_AfterDayStarted; + + /// Raised after the in-game clock changes. + public readonly ManagedEvent Legacy_TimeOfDayChanged; +#endif + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + public EventManager(IMonitor monitor, ModRegistry modRegistry) + { + // create shortcut initialisers + ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); +#if !SMAPI_3_0_STRICT + ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); +#endif + + // init events (new) + this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); + this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering)); + this.Rendered = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered)); + this.RenderingWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld)); + this.RenderedWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld)); + this.RenderingActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu)); + this.RenderedActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu)); + this.RenderingHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud)); + this.RenderedHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud)); + this.WindowResized = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.WindowResized)); + + this.GameLaunched = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.GameLaunched)); + this.UpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking)); + this.UpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked)); + this.OneSecondUpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking)); + this.OneSecondUpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked)); + this.SaveCreating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreating)); + this.SaveCreated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreated)); + this.Saving = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saving)); + this.Saved = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saved)); + this.SaveLoaded = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveLoaded)); + this.DayStarted = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.DayStarted)); + this.DayEnding = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.DayEnding)); + this.TimeChanged = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.TimeChanged)); + this.ReturnedToTitle = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.ReturnedToTitle)); + + this.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); + this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); + this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); + this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); + + this.PeerContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); + this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); + this.PeerDisconnected = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected)); + + this.InventoryChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.InventoryChanged)); + this.LevelChanged = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.LevelChanged)); + this.Warped = ManageEventOf(nameof(IModEvents.Player), nameof(IPlayerEvents.Warped)); + + this.BuildingListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LocationListChanged)); + this.DebrisListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.DebrisListChanged)); + this.LargeTerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.LargeTerrainFeatureListChanged)); + this.LocationListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.BuildingListChanged)); + this.NpcListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.NpcListChanged)); + this.ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); + this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); + + this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.LoadStageChanged)); + this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking)); + this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked)); + +#if !SMAPI_3_0_STRICT + // init events (old) + this.Legacy_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); + + this.Legacy_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); + this.Legacy_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); + this.Legacy_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); + this.Legacy_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); + this.Legacy_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); + this.Legacy_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); + this.Legacy_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); + this.Legacy_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); + + this.Legacy_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); + this.Legacy_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); + this.Legacy_SecondUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.SecondUpdateTick)); + this.Legacy_FourthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FourthUpdateTick)); + this.Legacy_EighthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.EighthUpdateTick)); + this.Legacy_QuarterSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.QuarterSecondTick)); + this.Legacy_HalfSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.HalfSecondTick)); + this.Legacy_OneSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.OneSecondTick)); + + this.Legacy_Resize = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.Resize)); + this.Legacy_OnPreRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderEvent)); + this.Legacy_OnPostRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderEvent)); + this.Legacy_OnPreRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderHudEvent)); + this.Legacy_OnPostRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderHudEvent)); + this.Legacy_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); + this.Legacy_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); + + this.Legacy_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); + this.Legacy_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); + + this.Legacy_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); + this.Legacy_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); + this.Legacy_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); + + this.Legacy_MenuChanged = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); + this.Legacy_MenuClosed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); + + this.Legacy_BeforeMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainBroadcast)); + this.Legacy_AfterMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainBroadcast)); + this.Legacy_BeforeMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainSync)); + this.Legacy_AfterMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainSync)); + + this.Legacy_MineLevelChanged = ManageEventOf(nameof(MineEvents), nameof(MineEvents.MineLevelChanged)); + + this.Legacy_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); + this.Legacy_LeveledUp = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); + this.Legacy_PlayerWarped = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.Warped)); + + this.Legacy_BeforeCreateSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate)); + this.Legacy_AfterCreateSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate)); + this.Legacy_BeforeSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeSave)); + this.Legacy_AfterSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterSave)); + this.Legacy_AfterLoad = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterLoad)); + this.Legacy_AfterReturnToTitle = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterReturnToTitle)); + + this.Legacy_UnvalidatedUpdateTick = ManageEvent(nameof(SpecialisedEvents), nameof(SpecialisedEvents.UnvalidatedUpdateTick)); + + this.Legacy_AfterDayStarted = ManageEvent(nameof(TimeEvents), nameof(TimeEvents.AfterDayStarted)); + this.Legacy_TimeOfDayChanged = ManageEventOf(nameof(TimeEvents), nameof(TimeEvents.TimeOfDayChanged)); +#endif + } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/ManagedEvent.cs b/src/StardewModdingAPI/Framework/Events/ManagedEvent.cs new file mode 100644 index 00000000..f9e7f6ec --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ManagedEvent.cs @@ -0,0 +1,161 @@ +using System; +using System.Linq; + +namespace StardewModdingAPI.Framework.Events +{ + /// An event wrapper which intercepts and logs errors in handler code. + /// The event arguments type. + internal class ManagedEvent : ManagedEventBase> + { + /********* + ** Fields + *********/ + /// The underlying event. + private event EventHandler Event; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A human-readable name for the event. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) + : base(eventName, monitor, modRegistry) { } + + /// Add an event handler. + /// The event handler. + public void Add(EventHandler handler) + { + this.Add(handler, this.ModRegistry.GetFromStack()); + } + + /// Add an event handler. + /// The event handler. + /// The mod which added the event handler. + public void Add(EventHandler handler, IModMetadata mod) + { + this.Event += handler; + this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast>()); + } + + /// Remove an event handler. + /// The event handler. + public void Remove(EventHandler handler) + { + this.Event -= handler; + this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); + } + + /// Raise the event and notify all handlers. + /// The event arguments to pass. + public void Raise(TEventArgs args) + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + try + { + handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + + /// Raise the event and notify all handlers. + /// The event arguments to pass. + /// A lambda which returns true if the event should be raised for the given mod. + public void RaiseForMods(TEventArgs args, Func match) + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + if (match(this.GetSourceMod(handler))) + { + try + { + handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } + } + +#if !SMAPI_3_0_STRICT + /// An event wrapper which intercepts and logs errors in handler code. + internal class ManagedEvent : ManagedEventBase + { + /********* + ** Fields + *********/ + /// The underlying event. + private event EventHandler Event; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A human-readable name for the event. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) + : base(eventName, monitor, modRegistry) { } + + /// Add an event handler. + /// The event handler. + public void Add(EventHandler handler) + { + this.Add(handler, this.ModRegistry.GetFromStack()); + } + + /// Add an event handler. + /// The event handler. + /// The mod which added the event handler. + public void Add(EventHandler handler, IModMetadata mod) + { + this.Event += handler; + this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast()); + } + + /// Remove an event handler. + /// The event handler. + public void Remove(EventHandler handler) + { + this.Event -= handler; + this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast()); + } + + /// Raise the event and notify all handlers. + public void Raise() + { + if (this.Event == null) + return; + + foreach (EventHandler handler in this.CachedInvocationList) + { + try + { + handler.Invoke(null, EventArgs.Empty); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } + } + } +#endif +} diff --git a/src/StardewModdingAPI/Framework/Events/ManagedEventBase.cs b/src/StardewModdingAPI/Framework/Events/ManagedEventBase.cs new file mode 100644 index 00000000..c8c3516b --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ManagedEventBase.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework.Events +{ + /// The base implementation for an event wrapper which intercepts and logs errors in handler code. + internal abstract class ManagedEventBase + { + /********* + ** Fields + *********/ + /// A human-readable name for the event. + private readonly string EventName; + + /// Writes messages to the log. + private readonly IMonitor Monitor; + + /// The mod registry with which to identify mods. + protected readonly ModRegistry ModRegistry; + + /// The display names for the mods which added each delegate. + private readonly IDictionary SourceMods = new Dictionary(); + + /// The cached invocation list. + protected TEventHandler[] CachedInvocationList { get; private set; } + + + /********* + ** Public methods + *********/ + /// Get whether anything is listening to the event. + public bool HasListeners() + { + return this.CachedInvocationList?.Length > 0; + } + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// A human-readable name for the event. + /// Writes messages to the log. + /// The mod registry with which to identify mods. + protected ManagedEventBase(string eventName, IMonitor monitor, ModRegistry modRegistry) + { + this.EventName = eventName; + this.Monitor = monitor; + this.ModRegistry = modRegistry; + } + + /// Track an event handler. + /// The mod which added the handler. + /// The event handler. + /// The updated event invocation list. + protected void AddTracking(IModMetadata mod, TEventHandler handler, IEnumerable invocationList) + { + this.SourceMods[handler] = mod; + this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; + } + + /// Remove tracking for an event handler. + /// The event handler. + /// The updated event invocation list. + protected void RemoveTracking(TEventHandler handler, IEnumerable invocationList) + { + this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; + if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) + this.SourceMods.Remove(handler); + } + + /// Get the mod which registered the given event handler, if available. + /// The event handler. + protected IModMetadata GetSourceMod(TEventHandler handler) + { + return this.SourceMods.TryGetValue(handler, out IModMetadata mod) + ? mod + : null; + } + + /// Log an exception from an event handler. + /// The event handler instance. + /// The exception that was raised. + protected void LogError(TEventHandler handler, Exception ex) + { + IModMetadata mod = this.GetSourceMod(handler); + if (mod != null) + mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/ModDisplayEvents.cs b/src/StardewModdingAPI/Framework/Events/ModDisplayEvents.cs new file mode 100644 index 00000000..e383eec6 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ModDisplayEvents.cs @@ -0,0 +1,93 @@ +using System; +using StardewModdingAPI.Events; +using StardewValley; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events related to UI and drawing to the screen. + internal class ModDisplayEvents : ModEventsBase, IDisplayEvents + { + /********* + ** Accessors + *********/ + /// Raised after a game menu is opened, closed, or replaced. + public event EventHandler MenuChanged + { + add => this.EventManager.MenuChanged.Add(value); + remove => this.EventManager.MenuChanged.Remove(value); + } + + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. + public event EventHandler Rendering + { + add => this.EventManager.Rendering.Add(value); + remove => this.EventManager.Rendering.Remove(value); + } + + /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). + public event EventHandler Rendered + { + add => this.EventManager.Rendered.Add(value); + remove => this.EventManager.Rendered.Remove(value); + } + + /// Raised before the game world is drawn to the screen. This event isn't useful for drawing to the screen, since the game will draw over it. + public event EventHandler RenderingWorld + { + add => this.EventManager.RenderingWorld.Add(value); + remove => this.EventManager.RenderingWorld.Remove(value); + } + + /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. Content drawn to the sprite batch at this point will be drawn over the world, but under any active menu, HUD elements, or cursor. + public event EventHandler RenderedWorld + { + add => this.EventManager.RenderedWorld.Add(value); + remove => this.EventManager.RenderedWorld.Remove(value); + } + + /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. This includes the game's internal menus like the title screen. Content drawn to the sprite batch at this point will appear under the menu. + public event EventHandler RenderingActiveMenu + { + add => this.EventManager.RenderingActiveMenu.Add(value); + remove => this.EventManager.RenderingActiveMenu.Remove(value); + } + + /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. Content drawn to the sprite batch at this point will appear over the menu and menu cursor. + public event EventHandler RenderedActiveMenu + { + add => this.EventManager.RenderedActiveMenu.Add(value); + remove => this.EventManager.RenderedActiveMenu.Remove(value); + } + + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear under the HUD. + public event EventHandler RenderingHud + { + add => this.EventManager.RenderingHud.Add(value); + remove => this.EventManager.RenderingHud.Remove(value); + } + + /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear over the HUD. + public event EventHandler RenderedHud + { + add => this.EventManager.RenderedHud.Add(value); + remove => this.EventManager.RenderedHud.Remove(value); + } + + /// Raised after the game window is resized. + public event EventHandler WindowResized + { + add => this.EventManager.WindowResized.Add(value); + remove => this.EventManager.WindowResized.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModDisplayEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/ModEvents.cs b/src/StardewModdingAPI/Framework/Events/ModEvents.cs new file mode 100644 index 00000000..8ad3936c --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ModEvents.cs @@ -0,0 +1,50 @@ +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Manages access to events raised by SMAPI. + internal class ModEvents : IModEvents + { + /********* + ** Accessors + *********/ + /// Events related to UI and drawing to the screen. + public IDisplayEvents Display { get; } + + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + public IGameLoopEvents GameLoop { get; } + + /// Events raised when the player provides input using a controller, keyboard, or mouse. + public IInputEvents Input { get; } + + /// Events raised for multiplayer messages and connections. + public IMultiplayerEvents Multiplayer { get; } + + /// Events raised when the player data changes. + public IPlayerEvents Player { get; } + + /// Events raised when something changes in the world. + public IWorldEvents World { get; } + + /// Events serving specialised edge cases that shouldn't be used by most mods. + public ISpecialisedEvents Specialised { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + public ModEvents(IModMetadata mod, EventManager eventManager) + { + this.Display = new ModDisplayEvents(mod, eventManager); + this.GameLoop = new ModGameLoopEvents(mod, eventManager); + this.Input = new ModInputEvents(mod, eventManager); + this.Multiplayer = new ModMultiplayerEvents(mod, eventManager); + this.Player = new ModPlayerEvents(mod, eventManager); + this.World = new ModWorldEvents(mod, eventManager); + this.Specialised = new ModSpecialisedEvents(mod, eventManager); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/ModEventsBase.cs b/src/StardewModdingAPI/Framework/Events/ModEventsBase.cs new file mode 100644 index 00000000..77708fc1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ModEventsBase.cs @@ -0,0 +1,28 @@ +namespace StardewModdingAPI.Framework.Events +{ + /// An internal base class for event API classes. + internal abstract class ModEventsBase + { + /********* + ** Fields + *********/ + /// The underlying event manager. + protected readonly EventManager EventManager; + + /// The mod which uses this instance. + protected readonly IModMetadata Mod; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModEventsBase(IModMetadata mod, EventManager eventManager) + { + this.Mod = mod; + this.EventManager = eventManager; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/ModGameLoopEvents.cs b/src/StardewModdingAPI/Framework/Events/ModGameLoopEvents.cs new file mode 100644 index 00000000..0177c22e --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ModGameLoopEvents.cs @@ -0,0 +1,121 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. + internal class ModGameLoopEvents : ModEventsBase, IGameLoopEvents + { + /********* + ** Accessors + *********/ + /// Raised after the game is launched, right before the first update tick. + public event EventHandler GameLaunched + { + add => this.EventManager.GameLaunched.Add(value); + remove => this.EventManager.GameLaunched.Remove(value); + } + + /// Raised before the game performs its overall update tick (≈60 times per second). + public event EventHandler UpdateTicking + { + add => this.EventManager.UpdateTicking.Add(value); + remove => this.EventManager.UpdateTicking.Remove(value); + } + + /// Raised after the game performs its overall update tick (≈60 times per second). + public event EventHandler UpdateTicked + { + add => this.EventManager.UpdateTicked.Add(value); + remove => this.EventManager.UpdateTicked.Remove(value); + } + + /// Raised once per second before the game state is updated. + public event EventHandler OneSecondUpdateTicking + { + add => this.EventManager.OneSecondUpdateTicking.Add(value); + remove => this.EventManager.OneSecondUpdateTicking.Remove(value); + } + + /// Raised once per second after the game state is updated. + public event EventHandler OneSecondUpdateTicked + { + add => this.EventManager.OneSecondUpdateTicked.Add(value); + remove => this.EventManager.OneSecondUpdateTicked.Remove(value); + } + + /// Raised before the game creates a new save file. + public event EventHandler SaveCreating + { + add => this.EventManager.SaveCreating.Add(value); + remove => this.EventManager.SaveCreating.Remove(value); + } + + /// Raised after the game finishes creating the save file. + public event EventHandler SaveCreated + { + add => this.EventManager.SaveCreated.Add(value); + remove => this.EventManager.SaveCreated.Remove(value); + } + + /// Raised before the game begins writes data to the save file. + public event EventHandler Saving + { + add => this.EventManager.Saving.Add(value); + remove => this.EventManager.Saving.Remove(value); + } + + /// Raised after the game finishes writing data to the save file. + public event EventHandler Saved + { + add => this.EventManager.Saved.Add(value); + remove => this.EventManager.Saved.Remove(value); + } + + /// Raised after the player loads a save slot and the world is initialised. + public event EventHandler SaveLoaded + { + add => this.EventManager.SaveLoaded.Add(value); + remove => this.EventManager.SaveLoaded.Remove(value); + } + + /// Raised after the game begins a new day (including when the player loads a save). + public event EventHandler DayStarted + { + add => this.EventManager.DayStarted.Add(value); + remove => this.EventManager.DayStarted.Remove(value); + } + + /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . + public event EventHandler DayEnding + { + add => this.EventManager.DayEnding.Add(value); + remove => this.EventManager.DayEnding.Remove(value); + } + + /// Raised after the in-game clock time changes. + public event EventHandler TimeChanged + { + + add => this.EventManager.TimeChanged.Add(value); + remove => this.EventManager.TimeChanged.Remove(value); + } + + /// Raised after the game returns to the title screen. + public event EventHandler ReturnedToTitle + { + add => this.EventManager.ReturnedToTitle.Add(value); + remove => this.EventManager.ReturnedToTitle.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModGameLoopEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/ModInputEvents.cs b/src/StardewModdingAPI/Framework/Events/ModInputEvents.cs new file mode 100644 index 00000000..6a4298b4 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ModInputEvents.cs @@ -0,0 +1,50 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised when the player provides input using a controller, keyboard, or mouse. + internal class ModInputEvents : ModEventsBase, IInputEvents + { + /********* + ** Accessors + *********/ + /// Raised after the player presses a button on the keyboard, controller, or mouse. + public event EventHandler ButtonPressed + { + add => this.EventManager.ButtonPressed.Add(value); + remove => this.EventManager.ButtonPressed.Remove(value); + } + + /// Raised after the player releases a button on the keyboard, controller, or mouse. + public event EventHandler ButtonReleased + { + add => this.EventManager.ButtonReleased.Add(value); + remove => this.EventManager.ButtonReleased.Remove(value); + } + + /// Raised after the player moves the in-game cursor. + public event EventHandler CursorMoved + { + add => this.EventManager.CursorMoved.Add(value); + remove => this.EventManager.CursorMoved.Remove(value); + } + + /// Raised after the player scrolls the mouse wheel. + public event EventHandler MouseWheelScrolled + { + add => this.EventManager.MouseWheelScrolled.Add(value); + remove => this.EventManager.MouseWheelScrolled.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModInputEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/ModMultiplayerEvents.cs b/src/StardewModdingAPI/Framework/Events/ModMultiplayerEvents.cs new file mode 100644 index 00000000..152c4e0c --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ModMultiplayerEvents.cs @@ -0,0 +1,43 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised for multiplayer messages and connections. + internal class ModMultiplayerEvents : ModEventsBase, IMultiplayerEvents + { + /********* + ** Accessors + *********/ + /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. + public event EventHandler PeerContextReceived + { + add => this.EventManager.PeerContextReceived.Add(value); + remove => this.EventManager.PeerContextReceived.Remove(value); + } + + /// Raised after a mod message is received over the network. + public event EventHandler ModMessageReceived + { + add => this.EventManager.ModMessageReceived.Add(value); + remove => this.EventManager.ModMessageReceived.Remove(value); + } + + /// Raised after the connection with a peer is severed. + public event EventHandler PeerDisconnected + { + add => this.EventManager.PeerDisconnected.Add(value); + remove => this.EventManager.PeerDisconnected.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModMultiplayerEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/ModPlayerEvents.cs b/src/StardewModdingAPI/Framework/Events/ModPlayerEvents.cs new file mode 100644 index 00000000..ca7cfd96 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ModPlayerEvents.cs @@ -0,0 +1,43 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised when the player data changes. + internal class ModPlayerEvents : ModEventsBase, IPlayerEvents + { + /********* + ** Accessors + *********/ + /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the local player. + public event EventHandler InventoryChanged + { + add => this.EventManager.InventoryChanged.Add(value); + remove => this.EventManager.InventoryChanged.Remove(value); + } + + /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. NOTE: this event is currently only raised for the local player. + public event EventHandler LevelChanged + { + add => this.EventManager.LevelChanged.Add(value); + remove => this.EventManager.LevelChanged.Remove(value); + } + + /// Raised after a player warps to a new location. NOTE: this event is currently only raised for the local player. + public event EventHandler Warped + { + add => this.EventManager.Warped.Add(value); + remove => this.EventManager.Warped.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModPlayerEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/ModSpecialisedEvents.cs b/src/StardewModdingAPI/Framework/Events/ModSpecialisedEvents.cs new file mode 100644 index 00000000..7c3e9dee --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ModSpecialisedEvents.cs @@ -0,0 +1,43 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events serving specialised edge cases that shouldn't be used by most mods. + internal class ModSpecialisedEvents : ModEventsBase, ISpecialisedEvents + { + /********* + ** Accessors + *********/ + /// Raised when the low-level stage in the game's loading process has changed. This is an advanced event for mods which need to run code at specific points in the loading process. The available stages or when they happen might change without warning in future versions (e.g. due to changes in the game's load process), so mods using this event are more likely to break or have bugs. Most mods should use instead. + public event EventHandler LoadStageChanged + { + add => this.EventManager.LoadStageChanged.Add(value); + remove => this.EventManager.LoadStageChanged.Remove(value); + } + + /// Raised before the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. + public event EventHandler UnvalidatedUpdateTicking + { + add => this.EventManager.UnvalidatedUpdateTicking.Add(value); + remove => this.EventManager.UnvalidatedUpdateTicking.Remove(value); + } + + /// Raised after the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. + public event EventHandler UnvalidatedUpdateTicked + { + add => this.EventManager.UnvalidatedUpdateTicked.Add(value); + remove => this.EventManager.UnvalidatedUpdateTicked.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModSpecialisedEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/StardewModdingAPI/Framework/Events/ModWorldEvents.cs b/src/StardewModdingAPI/Framework/Events/ModWorldEvents.cs new file mode 100644 index 00000000..b85002a3 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Events/ModWorldEvents.cs @@ -0,0 +1,71 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// Events raised when something changes in the world. + internal class ModWorldEvents : ModEventsBase, IWorldEvents + { + /********* + ** Accessors + *********/ + /// Raised after a game location is added or removed. + public event EventHandler LocationListChanged + { + add => this.EventManager.LocationListChanged.Add(value, this.Mod); + remove => this.EventManager.LocationListChanged.Remove(value); + } + + /// Raised after buildings are added or removed in a location. + public event EventHandler BuildingListChanged + { + add => this.EventManager.BuildingListChanged.Add(value, this.Mod); + remove => this.EventManager.BuildingListChanged.Remove(value); + } + + /// Raised after debris are added or removed in a location. + public event EventHandler DebrisListChanged + { + add => this.EventManager.DebrisListChanged.Add(value, this.Mod); + remove => this.EventManager.DebrisListChanged.Remove(value); + } + + /// Raised after large terrain features (like bushes) are added or removed in a location. + public event EventHandler LargeTerrainFeatureListChanged + { + add => this.EventManager.LargeTerrainFeatureListChanged.Add(value, this.Mod); + remove => this.EventManager.LargeTerrainFeatureListChanged.Remove(value); + } + + /// Raised after NPCs are added or removed in a location. + public event EventHandler NpcListChanged + { + add => this.EventManager.NpcListChanged.Add(value); + remove => this.EventManager.NpcListChanged.Remove(value); + } + + /// Raised after objects are added or removed in a location. + public event EventHandler ObjectListChanged + { + add => this.EventManager.ObjectListChanged.Add(value); + remove => this.EventManager.ObjectListChanged.Remove(value); + } + + /// Raised after terrain features (like floors and trees) are added or removed in a location. + public event EventHandler TerrainFeatureListChanged + { + add => this.EventManager.TerrainFeatureListChanged.Add(value); + remove => this.EventManager.TerrainFeatureListChanged.Remove(value); + } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod which uses this instance. + /// The underlying event manager. + internal ModWorldEvents(IModMetadata mod, EventManager eventManager) + : base(mod, eventManager) { } + } +} diff --git a/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs b/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs new file mode 100644 index 00000000..ec9279f1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs @@ -0,0 +1,16 @@ +using System; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// An exception thrown when an assembly can't be loaded by SMAPI, with all the relevant details in the message. + internal class SAssemblyLoadFailedException : Exception + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + public SAssemblyLoadFailedException(string message) + : base(message) { } + } +} diff --git a/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs b/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs new file mode 100644 index 00000000..85d85e3d --- /dev/null +++ b/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// An implementation of used by SMAPI to detect whether it was thrown by SMAPI or the underlying framework. + internal class SContentLoadException : ContentLoadException + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public SContentLoadException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/StardewModdingAPI/Framework/GameVersion.cs b/src/StardewModdingAPI/Framework/GameVersion.cs new file mode 100644 index 00000000..261de374 --- /dev/null +++ b/src/StardewModdingAPI/Framework/GameVersion.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework +{ + /// An implementation of that correctly handles the non-semantic versions used by older Stardew Valley releases. + internal class GameVersion : SemanticVersion + { + /********* + ** Private methods + *********/ + /// A mapping of game to semantic versions. + private static readonly IDictionary VersionMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["1.01"] = "1.0.1", + ["1.02"] = "1.0.2", + ["1.03"] = "1.0.3", + ["1.04"] = "1.0.4", + ["1.05"] = "1.0.5", + ["1.051"] = "1.0.6-prerelease1", // not a very good mapping, but good enough for SMAPI's purposes. + ["1.051b"] = "1.0.6-prelease2", + ["1.06"] = "1.0.6", + ["1.07"] = "1.0.7", + ["1.07a"] = "1.0.8-prerelease1", + ["1.08"] = "1.0.8", + ["1.11"] = "1.1.1" + }; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The game version string. + public GameVersion(string version) + : base(GameVersion.GetSemanticVersionString(version)) { } + + /// Get a string representation of the version. + public override string ToString() + { + return GameVersion.GetGameVersionString(base.ToString()); + } + + + /********* + ** Private methods + *********/ + /// Convert a game version string to a semantic version string. + /// The game version string. + private static string GetSemanticVersionString(string gameVersion) + { + return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) + ? semanticVersion + : gameVersion; + } + + /// Convert a semantic version string to the equivalent game version string. + /// The semantic version string. + private static string GetGameVersionString(string semanticVersion) + { + foreach (var mapping in GameVersion.VersionMap) + { + if (mapping.Value.Equals(semanticVersion, StringComparison.InvariantCultureIgnoreCase)) + return mapping.Key; + } + return semanticVersion; + } + } +} diff --git a/src/StardewModdingAPI/Framework/IModMetadata.cs b/src/StardewModdingAPI/Framework/IModMetadata.cs new file mode 100644 index 00000000..38514959 --- /dev/null +++ b/src/StardewModdingAPI/Framework/IModMetadata.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Framework +{ + /// Metadata for a mod. + internal interface IModMetadata : IModInfo + { + /********* + ** Accessors + *********/ + /// The mod's display name. + string DisplayName { get; } + + /// The mod's full directory path. + string DirectoryPath { get; } + + /// The relative to the game's Mods folder. + string RelativeDirectoryPath { get; } + + /// Metadata about the mod from SMAPI's internal data (if any). + ModDataRecordVersionedFields DataRecord { get; } + + /// The metadata resolution status. + ModMetadataStatus Status { get; } + + /// Indicates non-error issues with the mod. + ModWarning Warnings { get; } + + /// The reason the metadata is invalid, if any. + string Error { get; } + + /// Whether the mod folder should be ignored. This is true if it was found within a folder whose name starts with a dot. + bool IsIgnored { get; } + + /// The mod instance (if loaded and is false). + IMod Mod { get; } + + /// The content pack instance (if loaded and is true). + IContentPack ContentPack { get; } + + /// Writes messages to the console and log file as this mod. + IMonitor Monitor { get; } + + /// The mod-provided API (if any). + object Api { get; } + + /// The update-check metadata for this mod (if any). + ModEntryModel UpdateCheckData { get; } + + + /********* + ** Public methods + *********/ + /// Set the mod status. + /// The metadata resolution status. + /// The reason the metadata is invalid, if any. + /// Return the instance for chaining. + IModMetadata SetStatus(ModMetadataStatus status, string error = null); + + /// Set a warning flag for the mod. + /// The warning to set. + IModMetadata SetWarning(ModWarning warning); + + /// Set the mod instance. + /// The mod instance to set. + IModMetadata SetMod(IMod mod); + + /// Set the mod instance. + /// The contentPack instance to set. + /// Writes messages to the console and log file. + IModMetadata SetMod(IContentPack contentPack, IMonitor monitor); + + /// Set the mod-provided API instance. + /// The mod-provided API. + IModMetadata SetApi(object api); + + /// Set the update-check metadata for this mod. + /// The update-check metadata. + IModMetadata SetUpdateData(ModEntryModel data); + + /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). + bool HasManifest(); + + /// Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded). + bool HasID(); + + /// Whether the mod has the given ID. + /// The mod ID to check. + bool HasID(string id); + + /// Get the defined update keys. + /// Only return valid update keys. + IEnumerable GetUpdateKeys(bool validOnly = true); + + /// Whether the mod has at least one valid update key set. + bool HasValidUpdateKeys(); + + /// Get whether the mod has a given warning and it hasn't been suppressed in the . + /// The warning to check. + bool HasUnsuppressWarning(ModWarning warning); + } +} diff --git a/src/StardewModdingAPI/Framework/Input/GamePadStateBuilder.cs b/src/StardewModdingAPI/Framework/Input/GamePadStateBuilder.cs new file mode 100644 index 00000000..a20e1248 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Input/GamePadStateBuilder.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Framework.Input +{ + /// An abstraction for manipulating controller state. + internal class GamePadStateBuilder + { + /********* + ** Fields + *********/ + /// The current button states. + private readonly IDictionary ButtonStates; + + /// The left trigger value. + private float LeftTrigger; + + /// The right trigger value. + private float RightTrigger; + + /// The left thumbstick position. + private Vector2 LeftStickPos; + + /// The left thumbstick position. + private Vector2 RightStickPos; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial controller state. + public GamePadStateBuilder(GamePadState state) + { + this.ButtonStates = new Dictionary + { + [SButton.DPadUp] = state.DPad.Up, + [SButton.DPadDown] = state.DPad.Down, + [SButton.DPadLeft] = state.DPad.Left, + [SButton.DPadRight] = state.DPad.Right, + + [SButton.ControllerA] = state.Buttons.A, + [SButton.ControllerB] = state.Buttons.B, + [SButton.ControllerX] = state.Buttons.X, + [SButton.ControllerY] = state.Buttons.Y, + [SButton.LeftStick] = state.Buttons.LeftStick, + [SButton.RightStick] = state.Buttons.RightStick, + [SButton.LeftShoulder] = state.Buttons.LeftShoulder, + [SButton.RightShoulder] = state.Buttons.RightShoulder, + [SButton.ControllerBack] = state.Buttons.Back, + [SButton.ControllerStart] = state.Buttons.Start, + [SButton.BigButton] = state.Buttons.BigButton + }; + this.LeftTrigger = state.Triggers.Left; + this.RightTrigger = state.Triggers.Right; + this.LeftStickPos = state.ThumbSticks.Left; + this.RightStickPos = state.ThumbSticks.Right; + } + + /// Mark all matching buttons unpressed. + /// The buttons. + public void SuppressButtons(IEnumerable buttons) + { + foreach (SButton button in buttons) + this.SuppressButton(button); + } + + /// Mark a button unpressed. + /// The button. + public void SuppressButton(SButton button) + { + switch (button) + { + // left thumbstick + case SButton.LeftThumbstickUp: + if (this.LeftStickPos.Y > 0) + this.LeftStickPos.Y = 0; + break; + case SButton.LeftThumbstickDown: + if (this.LeftStickPos.Y < 0) + this.LeftStickPos.Y = 0; + break; + case SButton.LeftThumbstickLeft: + if (this.LeftStickPos.X < 0) + this.LeftStickPos.X = 0; + break; + case SButton.LeftThumbstickRight: + if (this.LeftStickPos.X > 0) + this.LeftStickPos.X = 0; + break; + + // right thumbstick + case SButton.RightThumbstickUp: + if (this.RightStickPos.Y > 0) + this.RightStickPos.Y = 0; + break; + case SButton.RightThumbstickDown: + if (this.RightStickPos.Y < 0) + this.RightStickPos.Y = 0; + break; + case SButton.RightThumbstickLeft: + if (this.RightStickPos.X < 0) + this.RightStickPos.X = 0; + break; + case SButton.RightThumbstickRight: + if (this.RightStickPos.X > 0) + this.RightStickPos.X = 0; + break; + + // triggers + case SButton.LeftTrigger: + this.LeftTrigger = 0; + break; + case SButton.RightTrigger: + this.RightTrigger = 0; + break; + + // buttons + default: + if (this.ButtonStates.ContainsKey(button)) + this.ButtonStates[button] = ButtonState.Released; + break; + } + } + + /// Construct an equivalent gamepad state. + public GamePadState ToGamePadState() + { + return new GamePadState( + leftThumbStick: this.LeftStickPos, + rightThumbStick: this.RightStickPos, + leftTrigger: this.LeftTrigger, + rightTrigger: this.RightTrigger, + buttons: this.GetBitmask(this.GetPressedButtons()) // MonoDevelop requires one bitmask here; don't specify multiple values + ); + } + + /********* + ** Private methods + *********/ + /// Get all pressed buttons. + private IEnumerable GetPressedButtons() + { + foreach (var pair in this.ButtonStates) + { + if (pair.Value == ButtonState.Pressed && pair.Key.TryGetController(out Buttons button)) + yield return button; + } + } + + /// Get a bitmask representing the given buttons. + /// The buttons to represent. + private Buttons GetBitmask(IEnumerable buttons) + { + Buttons flag = 0; + foreach (Buttons button in buttons) + flag |= button; + return flag; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Input/InputStatus.cs b/src/StardewModdingAPI/Framework/Input/InputStatus.cs new file mode 100644 index 00000000..99b0006c --- /dev/null +++ b/src/StardewModdingAPI/Framework/Input/InputStatus.cs @@ -0,0 +1,29 @@ +namespace StardewModdingAPI.Framework.Input +{ + /// The input status for a button during an update frame. + internal enum InputStatus + { + /// The button was neither pressed, held, nor released. + None, + + /// The button was pressed in this frame. + Pressed, + + /// The button has been held since the last frame. + Held, + + /// The button was released in this frame. + Released + } + + /// Extension methods for . + internal static class InputStatusExtensions + { + /// Whether the button was pressed or held. + /// The button status. + public static bool IsDown(this InputStatus status) + { + return status == InputStatus.Held || status == InputStatus.Pressed; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Input/SInputState.cs b/src/StardewModdingAPI/Framework/Input/SInputState.cs new file mode 100644 index 00000000..96a7003a --- /dev/null +++ b/src/StardewModdingAPI/Framework/Input/SInputState.cs @@ -0,0 +1,386 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using StardewValley; + +#pragma warning disable 809 // obsolete override of non-obsolete method (this is deliberate) +namespace StardewModdingAPI.Framework.Input +{ + /// Manages the game's input state. + internal sealed class SInputState : InputState + { + /********* + ** Accessors + *********/ + /// The maximum amount of direction to ignore for the left thumbstick. + private const float LeftThumbstickDeadZone = 0.2f; + + /// The cursor position on the screen adjusted for the zoom level. + private CursorPosition CursorPositionImpl; + + /// The player's last known tile position. + private Vector2? LastPlayerTile; + + + /********* + ** Accessors + *********/ + /// The controller state as of the last update. + public GamePadState RealController { get; private set; } + + /// The keyboard state as of the last update. + public KeyboardState RealKeyboard { get; private set; } + + /// The mouse state as of the last update. + public MouseState RealMouse { get; private set; } + + /// A derivative of which suppresses the buttons in . + public GamePadState SuppressedController { get; private set; } + + /// A derivative of which suppresses the buttons in . + public KeyboardState SuppressedKeyboard { get; private set; } + + /// A derivative of which suppresses the buttons in . + public MouseState SuppressedMouse { get; private set; } + + /// The cursor position on the screen adjusted for the zoom level. + public ICursorPosition CursorPosition => this.CursorPositionImpl; + + /// The buttons which were pressed, held, or released. + public IDictionary ActiveButtons { get; private set; } = new Dictionary(); + + /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. + public HashSet SuppressButtons { get; } = new HashSet(); + + + /********* + ** Public methods + *********/ + /// Get a copy of the current state. + public SInputState Clone() + { + return new SInputState + { + ActiveButtons = this.ActiveButtons, + RealController = this.RealController, + RealKeyboard = this.RealKeyboard, + RealMouse = this.RealMouse, + CursorPositionImpl = this.CursorPositionImpl + }; + } + + /// This method is called by the game, and does nothing since SMAPI will already have updated by that point. + [Obsolete("This method should only be called by the game itself.")] + public override void Update() { } + + /// Update the current button statuses for the given tick. + public void TrueUpdate() + { + try + { + // get new states + GamePadState realController = GamePad.GetState(PlayerIndex.One); + KeyboardState realKeyboard = Keyboard.GetState(); + MouseState realMouse = Mouse.GetState(); + var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController); + Vector2 cursorAbsolutePos = new Vector2(realMouse.X + Game1.viewport.X, realMouse.Y + Game1.viewport.Y); + Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null; + + // update real states + this.ActiveButtons = activeButtons; + this.RealController = realController; + this.RealKeyboard = realKeyboard; + this.RealMouse = realMouse; + if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile) + this.CursorPositionImpl = this.GetCursorPosition(realMouse, cursorAbsolutePos); + + // update suppressed states + this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); + this.UpdateSuppression(); + } + catch (InvalidOperationException) + { + // GetState() may crash for some players if window doesn't have focus but game1.IsActive == true + } + } + + /// Apply input suppression to current input. + public void UpdateSuppression() + { + GamePadState suppressedController = this.RealController; + KeyboardState suppressedKeyboard = this.RealKeyboard; + MouseState suppressedMouse = this.RealMouse; + + this.SuppressGivenStates(this.ActiveButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController); + + this.SuppressedController = suppressedController; + this.SuppressedKeyboard = suppressedKeyboard; + this.SuppressedMouse = suppressedMouse; + } + + /// Get the gamepad state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override GamePadState GetGamePadState() + { + return this.ShouldSuppressNow() + ? this.SuppressedController + : this.RealController; + } + + /// Get the keyboard state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override KeyboardState GetKeyboardState() + { + return this.ShouldSuppressNow() + ? this.SuppressedKeyboard + : this.RealKeyboard; + } + + /// Get the keyboard state visible to the game. + [Obsolete("This method should only be called by the game itself.")] + public override MouseState GetMouseState() + { + return this.ShouldSuppressNow() + ? this.SuppressedMouse + : this.RealMouse; + } + + /// Get whether a given button was pressed or held. + /// The button to check. + public bool IsDown(SButton button) + { + return this.GetStatus(this.ActiveButtons, button).IsDown(); + } + + /// Get whether any of the given buttons were pressed or held. + /// The buttons to check. + public bool IsAnyDown(InputButton[] buttons) + { + return buttons.Any(button => this.IsDown(button.ToSButton())); + } + + + /********* + ** Private methods + *********/ + /// Get the current cursor position. + /// The current mouse state. + /// The absolute pixel position relative to the map. + private CursorPosition GetCursorPosition(MouseState mouseState, Vector2 absolutePixels) + { + Vector2 rawPixels = new Vector2(mouseState.X, mouseState.Y); + Vector2 screenPixels = rawPixels * new Vector2((float)1.0 / Game1.options.zoomLevel); // derived from Game1::getMouseX + Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); + Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton + ? tile + : Game1.player.GetGrabTile(); + return new CursorPosition(absolutePixels, screenPixels, tile, grabTile); + } + + /// Whether input should be suppressed in the current context. + private bool ShouldSuppressNow() + { + return Game1.chatBox == null || !Game1.chatBox.isActive(); + } + + /// Apply input suppression to the given input states. + /// The current button states to check. + /// The game's keyboard state for the current tick. + /// The game's mouse state for the current tick. + /// The game's controller state for the current tick. + private void SuppressGivenStates(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) + { + if (this.SuppressButtons.Count == 0) + return; + + // gather info + HashSet suppressKeys = new HashSet(); + HashSet suppressButtons = new HashSet(); + HashSet suppressMouse = new HashSet(); + foreach (SButton button in this.SuppressButtons) + { + if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) + suppressMouse.Add(button); + else if (button.TryGetKeyboard(out Keys key)) + suppressKeys.Add(key); + else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) + suppressButtons.Add(button); + } + + // suppress keyboard keys + if (keyboardState.GetPressedKeys().Any() && suppressKeys.Any()) + keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(suppressKeys).ToArray()); + + // suppress controller keys + if (gamePadState.IsConnected && suppressButtons.Any()) + { + GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); + builder.SuppressButtons(suppressButtons); + gamePadState = builder.ToGamePadState(); + } + + // suppress mouse buttons + if (suppressMouse.Any()) + { + mouseState = new MouseState( + x: mouseState.X, + y: mouseState.Y, + scrollWheel: mouseState.ScrollWheelValue, + leftButton: suppressMouse.Contains(SButton.MouseLeft) ? ButtonState.Released : mouseState.LeftButton, + middleButton: suppressMouse.Contains(SButton.MouseMiddle) ? ButtonState.Released : mouseState.MiddleButton, + rightButton: suppressMouse.Contains(SButton.MouseRight) ? ButtonState.Released : mouseState.RightButton, + xButton1: suppressMouse.Contains(SButton.MouseX1) ? ButtonState.Released : mouseState.XButton1, + xButton2: suppressMouse.Contains(SButton.MouseX2) ? ButtonState.Released : mouseState.XButton2 + ); + } + } + + /// Get the status of all pressed or released buttons relative to their previous status. + /// The previous button statuses. + /// The keyboard state. + /// The mouse state. + /// The controller state. + private IDictionary DeriveStatuses(IDictionary previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller) + { + IDictionary activeButtons = new Dictionary(); + + // handle pressed keys + SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray(); + foreach (SButton button in down) + activeButtons[button] = this.DeriveStatus(this.GetStatus(previousStatuses, button), isDown: true); + + // handle released keys + foreach (KeyValuePair prev in previousStatuses) + { + if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key)) + activeButtons[prev.Key] = InputStatus.Released; + } + + return activeButtons; + } + + /// Get the status of a button relative to its previous status. + /// The previous button status. + /// Whether the button is currently down. + private InputStatus DeriveStatus(InputStatus oldStatus, bool isDown) + { + if (isDown && oldStatus.IsDown()) + return InputStatus.Held; + if (isDown) + return InputStatus.Pressed; + return InputStatus.Released; + } + + /// Get the status of a button. + /// The current button states to check. + /// The button to check. + private InputStatus GetStatus(IDictionary activeButtons, SButton button) + { + return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None; + } + + /// Get the buttons pressed in the given stats. + /// The keyboard state. + /// The mouse state. + /// The controller state. + /// Thumbstick direction logic derived from . + private IEnumerable GetPressedButtons(KeyboardState keyboard, MouseState mouse, GamePadState controller) + { + // keyboard + foreach (Keys key in keyboard.GetPressedKeys()) + yield return key.ToSButton(); + + // mouse + if (mouse.LeftButton == ButtonState.Pressed) + yield return SButton.MouseLeft; + if (mouse.RightButton == ButtonState.Pressed) + yield return SButton.MouseRight; + if (mouse.MiddleButton == ButtonState.Pressed) + yield return SButton.MouseMiddle; + if (mouse.XButton1 == ButtonState.Pressed) + yield return SButton.MouseX1; + if (mouse.XButton2 == ButtonState.Pressed) + yield return SButton.MouseX2; + + // controller + if (controller.IsConnected) + { + // main buttons + if (controller.Buttons.A == ButtonState.Pressed) + yield return SButton.ControllerA; + if (controller.Buttons.B == ButtonState.Pressed) + yield return SButton.ControllerB; + if (controller.Buttons.X == ButtonState.Pressed) + yield return SButton.ControllerX; + if (controller.Buttons.Y == ButtonState.Pressed) + yield return SButton.ControllerY; + if (controller.Buttons.LeftStick == ButtonState.Pressed) + yield return SButton.LeftStick; + if (controller.Buttons.RightStick == ButtonState.Pressed) + yield return SButton.RightStick; + if (controller.Buttons.Start == ButtonState.Pressed) + yield return SButton.ControllerStart; + + // directional pad + if (controller.DPad.Up == ButtonState.Pressed) + yield return SButton.DPadUp; + if (controller.DPad.Down == ButtonState.Pressed) + yield return SButton.DPadDown; + if (controller.DPad.Left == ButtonState.Pressed) + yield return SButton.DPadLeft; + if (controller.DPad.Right == ButtonState.Pressed) + yield return SButton.DPadRight; + + // secondary buttons + if (controller.Buttons.Back == ButtonState.Pressed) + yield return SButton.ControllerBack; + if (controller.Buttons.BigButton == ButtonState.Pressed) + yield return SButton.BigButton; + + // shoulders + if (controller.Buttons.LeftShoulder == ButtonState.Pressed) + yield return SButton.LeftShoulder; + if (controller.Buttons.RightShoulder == ButtonState.Pressed) + yield return SButton.RightShoulder; + + // triggers + if (controller.Triggers.Left > 0.2f) + yield return SButton.LeftTrigger; + if (controller.Triggers.Right > 0.2f) + yield return SButton.RightTrigger; + + // left thumbstick direction + if (controller.ThumbSticks.Left.Y > SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickUp; + if (controller.ThumbSticks.Left.Y < -SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickDown; + if (controller.ThumbSticks.Left.X > SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickRight; + if (controller.ThumbSticks.Left.X < -SInputState.LeftThumbstickDeadZone) + yield return SButton.LeftThumbstickLeft; + + // right thumbstick direction + if (this.IsRightThumbstickOutsideDeadZone(controller.ThumbSticks.Right)) + { + if (controller.ThumbSticks.Right.Y > 0) + yield return SButton.RightThumbstickUp; + if (controller.ThumbSticks.Right.Y < 0) + yield return SButton.RightThumbstickDown; + if (controller.ThumbSticks.Right.X > 0) + yield return SButton.RightThumbstickRight; + if (controller.ThumbSticks.Right.X < 0) + yield return SButton.RightThumbstickLeft; + } + } + } + + /// Get whether the right thumbstick should be considered outside the dead zone. + /// The right thumbstick value. + private bool IsRightThumbstickOutsideDeadZone(Vector2 direction) + { + return direction.Length() > 0.9f; + } + } +} diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs new file mode 100644 index 00000000..f52bfe2b --- /dev/null +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Events; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// Provides extension methods for SMAPI's internal use. + internal static class InternalExtensions + { + /**** + ** IMonitor + ****/ + /// Log a message for the player or developer the first time it occurs. + /// The monitor through which to log the message. + /// The hash of logged messages. + /// The message to log. + /// The log severity level. + public static void LogOnce(this IMonitor monitor, HashSet hash, string message, LogLevel level = LogLevel.Trace) + { + if (!hash.Contains(message)) + { + monitor.Log(message, level); + hash.Add(message); + } + } + + /**** + ** IModMetadata + ****/ + /// Log a message using the mod's monitor. + /// The mod whose monitor to use. + /// The message to log. + /// The log severity level. + public static void LogAsMod(this IModMetadata metadata, string message, LogLevel level = LogLevel.Trace) + { + metadata.Monitor.Log(message, level); + } + + /**** + ** ManagedEvent + ****/ + /// Raise the event using the default event args and notify all handlers. + /// The event args type to construct. + /// The event to raise. + public static void RaiseEmpty(this ManagedEvent @event) where TEventArgs : new() + { + @event.Raise(Singleton.Instance); + } + + /**** + ** Exceptions + ****/ + /// Get a string representation of an exception suitable for writing to the error log. + /// The error to summarise. + public static string GetLogSummary(this Exception exception) + { + switch (exception) + { + case TypeLoadException ex: + return $"Failed loading type '{ex.TypeName}': {exception}"; + + case ReflectionTypeLoadException ex: + string summary = exception.ToString(); + foreach (Exception childEx in ex.LoaderExceptions) + summary += $"\n\n{childEx.GetLogSummary()}"; + return summary; + + default: + return exception.ToString(); + } + } + + /// Get the lowest exception in an exception stack. + /// The exception from which to search. + public static Exception GetInnermostException(this Exception exception) + { + while (exception.InnerException != null) + exception = exception.InnerException; + return exception; + } + + /**** + ** Sprite batch + ****/ + /// Get whether the sprite batch is between a begin and end pair. + /// The sprite batch to check. + /// The reflection helper with which to access private fields. + public static bool IsOpen(this SpriteBatch spriteBatch, Reflector reflection) + { + // get field name + const string fieldName = +#if SMAPI_FOR_WINDOWS + "inBeginEndPair"; +#else + "_beginCalled"; +#endif + + // get result + return reflection.GetField(Game1.spriteBatch, fieldName).GetValue(); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs new file mode 100644 index 00000000..ef42e536 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs @@ -0,0 +1,59 @@ +using System; + +namespace StardewModdingAPI.Framework.Logging +{ + /// Manages console output interception. + internal class ConsoleInterceptionManager : IDisposable + { + /********* + ** Fields + *********/ + /// The intercepting console writer. + private readonly InterceptingTextWriter Output; + + + /********* + ** Accessors + *********/ + /// The event raised when a message is written to the console directly. + public event Action OnMessageIntercepted; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ConsoleInterceptionManager() + { + // redirect output through interceptor + this.Output = new InterceptingTextWriter(Console.Out); + this.Output.OnMessageIntercepted += line => this.OnMessageIntercepted?.Invoke(line); + Console.SetOut(this.Output); + } + + /// Get an exclusive lock and write to the console output without interception. + /// The action to perform within the exclusive write block. + public void ExclusiveWriteWithoutInterception(Action action) + { + lock (Console.Out) + { + try + { + this.Output.ShouldIntercept = false; + action(); + } + finally + { + this.Output.ShouldIntercept = true; + } + } + } + + /// Release all resources. + public void Dispose() + { + Console.SetOut(this.Output.Out); + this.Output.Dispose(); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs b/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs new file mode 100644 index 00000000..9ca61b59 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Text; + +namespace StardewModdingAPI.Framework.Logging +{ + /// A text writer which allows intercepting output. + internal class InterceptingTextWriter : TextWriter + { + /********* + ** Accessors + *********/ + /// The underlying console output. + public TextWriter Out { get; } + + /// The character encoding in which the output is written. + public override Encoding Encoding => this.Out.Encoding; + + /// Whether to intercept console output. + public bool ShouldIntercept { get; set; } + + /// The event raised when a message is written to the console directly. + public event Action OnMessageIntercepted; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying output writer. + public InterceptingTextWriter(TextWriter output) + { + this.Out = output; + } + + /// Writes a subarray of characters to the text string or stream. + /// The character array to write data from. + /// The character position in the buffer at which to start retrieving data. + /// The number of characters to write. + public override void Write(char[] buffer, int index, int count) + { + if (this.ShouldIntercept) + this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n')); + else + this.Out.Write(buffer, index, count); + } + + /// Writes a character to the text string or stream. + /// The character to write to the text stream. + /// Console log messages from the game should be caught by . This method passes through anything that bypasses that method for some reason, since it's better to show it to users than hide it from everyone. + public override void Write(char ch) + { + this.Out.Write(ch); + } + + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + this.OnMessageIntercepted = null; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs b/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs new file mode 100644 index 00000000..6b5babcd --- /dev/null +++ b/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; + +namespace StardewModdingAPI.Framework.Logging +{ + /// Manages reading and writing to log file. + internal class LogFileManager : IDisposable + { + /********* + ** Fields + *********/ + /// The underlying stream writer. + private readonly StreamWriter Stream; + + + /********* + ** Accessors + *********/ + /// The full path to the log file being written. + public string Path { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The log file to write. + public LogFileManager(string path) + { + this.Path = path; + + // create log directory if needed + string logDir = System.IO.Path.GetDirectoryName(path); + if (logDir == null) + throw new ArgumentException($"The log path '{path}' is not valid."); + Directory.CreateDirectory(logDir); + + // open log file stream + this.Stream = new StreamWriter(path, append: false) { AutoFlush = true }; + } + + /// Write a message to the log. + /// The message to log. + public void WriteLine(string message) + { + // always use Windows-style line endings for convenience + // (Linux/Mac editors are fine with them, Windows editors often require them) + this.Stream.Write(message + "\r\n"); + } + + /// Release all resources. + public void Dispose() + { + this.Stream.Dispose(); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs new file mode 100644 index 00000000..16032da1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs @@ -0,0 +1,23 @@ +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// The common base class for mod helpers. + internal abstract class BaseHelper : IModLinked + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod for which the helper was created. + public string ModID { get; } + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + protected BaseHelper(string modID) + { + this.ModID = modID; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs new file mode 100644 index 00000000..e9d53d84 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs @@ -0,0 +1,53 @@ +using System; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for managing console commands. + internal class CommandHelper : BaseHelper, ICommandHelper + { + /********* + ** Fields + *********/ + /// The mod using this instance. + private readonly IModMetadata Mod; + + /// Manages console commands. + private readonly CommandManager CommandManager; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod using this instance. + /// Manages console commands. + public CommandHelper(IModMetadata mod, CommandManager commandManager) + : base(mod?.Manifest?.UniqueID ?? "SMAPI") + { + this.Mod = mod; + this.CommandManager = commandManager; + } + + /// Add a console command. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + /// The or is null or empty. + /// The is not a valid format. + /// There's already a command with that name. + public ICommandHelper Add(string name, string documentation, Action callback) + { + this.CommandManager.Add(this.Mod, name, documentation, callback); + return this; + } + + /// Trigger a command. + /// The command name. + /// The command arguments. + /// Returns whether a matching command was triggered. + public bool Trigger(string name, string[] arguments) + { + return this.CommandManager.Trigger(name, arguments); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs new file mode 100644 index 00000000..8b86fdeb --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -0,0 +1,381 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.ContentManagers; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; +using xTile; +using xTile.Format; +using xTile.Tiles; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for loading content assets. + internal class ContentHelper : BaseHelper, IContentHelper + { + /********* + ** Fields + *********/ + /// SMAPI's core content logic. + private readonly ContentCoordinator ContentCore; + + /// A content manager for this mod which manages files from the game's Content folder. + private readonly IContentManager GameContentManager; + + /// A content manager for this mod which manages files from the mod's folder. + private readonly IContentManager ModContentManager; + + /// The absolute path to the mod folder. + private readonly string ModFolderPath; + + /// The friendly mod name for use in errors. + private readonly string ModName; + + /// Encapsulates monitoring and logging for a given module. + private readonly IMonitor Monitor; + + + /********* + ** Accessors + *********/ + /// The game's current locale code (like pt-BR). + public string CurrentLocale => this.GameContentManager.GetLocale(); + + /// The game's current locale as an enum value. + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language; + + /// The observable implementation of . + internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); + + /// The observable implementation of . + internal ObservableCollection ObservableAssetLoaders { get; } = new ObservableCollection(); + + /// Interceptors which provide the initial versions of matching content assets. + public IList AssetLoaders => this.ObservableAssetLoaders; + + /// Interceptors which edit matching content assets after they're loaded. + public IList AssetEditors => this.ObservableAssetEditors; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// SMAPI's core content logic. + /// The absolute path to the mod folder. + /// The unique ID of the relevant mod. + /// The friendly mod name for use in errors. + /// Encapsulates monitoring and logging. + public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor) + : base(modID) + { + this.ContentCore = contentCore; + this.GameContentManager = contentCore.CreateGameContentManager(this.ContentCore.GetManagedAssetPrefix(modID) + ".content"); + this.ModContentManager = contentCore.CreateModContentManager(this.ContentCore.GetManagedAssetPrefix(modID), rootDirectory: modFolderPath); + this.ModFolderPath = modFolderPath; + this.ModName = modName; + this.Monitor = monitor; + } + + /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + public T Load(string key, ContentSource source = ContentSource.ModFolder) + { + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}."); + + try + { + this.AssertAndNormaliseAssetName(key); + switch (source) + { + case ContentSource.GameContent: + return this.GameContentManager.Load(key); + + case ContentSource.ModFolder: + // get file + FileInfo file = this.GetModFile(key); + if (!file.Exists) + throw GetContentError($"there's no matching file at path '{file.FullName}'."); + string internalKey = this.GetInternalModAssetKey(file); + + // try cache + if (this.ModContentManager.IsLoaded(internalKey)) + return this.ModContentManager.Load(internalKey); + + // fix map tilesheets + if (file.Extension.ToLower() == ".tbin") + { + // validate + if (typeof(T) != typeof(Map)) + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + + // fetch & cache + FormatManager formatManager = FormatManager.Instance; + Map map = formatManager.LoadMap(file.FullName); + this.FixCustomTilesheetPaths(map, relativeMapPath: key); + + // inject map + this.ModContentManager.Inject(internalKey, map); + return (T)(object)map; + } + + // load through content manager + return this.ModContentManager.Load(internalKey); + + default: + throw GetContentError($"unknown content source '{source}'."); + } + } + catch (Exception ex) when (!(ex is SContentLoadException)) + { + throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}.", ex); + } + } + + /// Normalise an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like on generated asset names, and isn't necessary when passing asset names into other content helper methods. + /// The asset key. + [Pure] + public string NormaliseAssetName(string assetName) + { + return this.ModContentManager.AssertAndNormaliseAssetName(assetName); + } + + /// 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. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + public string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder) + { + switch (source) + { + case ContentSource.GameContent: + return this.GameContentManager.AssertAndNormaliseAssetName(key); + + case ContentSource.ModFolder: + FileInfo file = this.GetModFile(key); + return this.GetInternalModAssetKey(file); + + default: + throw new NotSupportedException($"Unknown content source '{source}'."); + } + } + + /// 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. + /// The asset key to invalidate in the content folder. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + public bool InvalidateCache(string key) + { + string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); + this.Monitor.Log($"Requested cache invalidation for '{actualKey}'.", LogLevel.Trace); + return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)).Any(); + } + + /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. + /// The asset type to remove from the cache. + /// Returns whether any assets were invalidated. + public bool InvalidateCache() + { + this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); + return this.ContentCore.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)).Any(); + } + + /// 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. + /// A predicate matching the assets to invalidate. + /// Returns whether any cache entries were invalidated. + public bool InvalidateCache(Func predicate) + { + this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.", LogLevel.Trace); + return this.ContentCore.InvalidateCache(predicate).Any(); + } + + /********* + ** Private methods + *********/ + /// Assert that the given key has a valid format. + /// The asset key to check. + /// The asset key is empty or contains invalid characters. + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] + private void AssertAndNormaliseAssetName(string key) + { + this.ModContentManager.AssertAndNormaliseAssetName(key); + if (Path.IsPathRooted(key)) + throw new ArgumentException("The asset key must not be an absolute path."); + } + + /// Get the internal key in the content cache for a mod asset. + /// The asset file. + private string GetInternalModAssetKey(FileInfo modFile) + { + string relativePath = PathUtilities.GetRelativePath(this.ModFolderPath, modFile.FullName); + return Path.Combine(this.ModContentManager.Name, relativePath); + } + + /// Fix custom map tilesheet paths so they can be found by the content manager. + /// The map whose tilesheets to fix. + /// The relative map path within the mod folder. + /// A map tilesheet couldn't be resolved. + /// + /// The game's logic for tilesheets in is a bit specialised. It boils + /// down to this: + /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded + /// as-is relative to the Content folder. + /// * Else it's loaded from Content\Maps with a seasonal prefix. + /// + /// That logic doesn't work well in our case, mainly because we have no location metadata at this point. + /// Instead we use a more heuristic approach: check relative to the map file first, then relative to + /// Content\Maps, then Content. If the image source filename contains a seasonal prefix, try for a + /// seasonal variation and then an exact match. + /// + /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference. + /// + private void FixCustomTilesheetPaths(Map map, string relativeMapPath) + { + // get map info + if (!map.TileSheets.Any()) + return; + relativeMapPath = this.ModContentManager.AssertAndNormaliseAssetName(relativeMapPath); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators + string relativeMapFolder = Path.GetDirectoryName(relativeMapPath) ?? ""; // folder path containing the map, relative to the mod folder + + // fix tilesheets + foreach (TileSheet tilesheet in map.TileSheets) + { + string imageSource = tilesheet.ImageSource; + + // validate tilesheet path + if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains("..")) + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../)."); + + // get seasonal name (if applicable) + string seasonalImageSource = null; + if (Context.IsSaveLoaded && Game1.currentSeason != null) + { + string filename = Path.GetFileName(imageSource) ?? throw new InvalidOperationException($"The '{imageSource}' tilesheet couldn't be loaded: filename is unexpectedly null."); + bool hasSeasonalPrefix = + filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase); + if (hasSeasonalPrefix && !filename.StartsWith(Game1.currentSeason + "_")) + { + string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename, StringComparison.CurrentCultureIgnoreCase)); + seasonalImageSource = $"{dirPath}{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}"; + } + } + + // load best match + try + { + string key = + this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource) + ?? this.GetTilesheetAssetName(relativeMapFolder, imageSource); + if (key != null) + { + tilesheet.ImageSource = key; + continue; + } + } + catch (Exception ex) + { + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); + } + + // none found + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder."); + } + } + + /// Get the actual asset name for a tilesheet. + /// The folder path containing the map, relative to the mod folder. + /// The tilesheet image source to load. + /// Returns the asset name. + /// See remarks on . + private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource) + { + if (imageSource == null) + return null; + + // check relative to map file + { + string localKey = Path.Combine(modRelativeMapFolder, imageSource); + FileInfo localFile = this.GetModFile(localKey); + if (localFile.Exists) + return this.GetActualAssetKey(localKey); + } + + // check relative to content folder + { + foreach (string candidateKey in new[] { imageSource, Path.Combine("Maps", imageSource) }) + { + string contentKey = candidateKey.EndsWith(".png") + ? candidateKey.Substring(0, candidateKey.Length - 4) + : candidateKey; + + try + { + this.Load(contentKey, ContentSource.GameContent); + return contentKey; + } + catch + { + // ignore file-not-found errors + // TODO: while it's useful to suppress an asset-not-found error here to avoid + // confusion, this is a pretty naive approach. Even if the file doesn't exist, + // the file may have been loaded through an IAssetLoader which failed. So even + // if the content file doesn't exist, that doesn't mean the error here is a + // content-not-found error. Unfortunately XNA doesn't provide a good way to + // detect the error type. + if (this.GetContentFolderFile(contentKey).Exists) + throw; + } + } + } + + // not found + return null; + } + + /// Get a file from the mod folder. + /// The asset path relative to the mod folder. + private FileInfo GetModFile(string path) + { + // try exact match + path = Path.Combine(this.ModFolderPath, this.ModContentManager.NormalisePathSeparators(path)); + FileInfo file = new FileInfo(path); + + // try with default extension + if (!file.Exists && file.Extension.ToLower() != ".xnb") + { + FileInfo result = new FileInfo(path + ".xnb"); + if (result.Exists) + file = result; + } + + return file; + } + + /// Get a file from the game's content folder. + /// The asset key. + private FileInfo GetContentFolderFile(string key) + { + // get file path + string path = Path.Combine(this.GameContentManager.FullRootDirectory, key); + if (!path.EndsWith(".xnb")) + path += ".xnb"; + + // get file + return new FileInfo(path); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentPackHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentPackHelper.cs new file mode 100644 index 00000000..34f24d65 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentPackHelper.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.IO; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for managing content packs. + internal class ContentPackHelper : BaseHelper, IContentPackHelper + { + /********* + ** Fields + *********/ + /// The content packs loaded for this mod. + private readonly Lazy ContentPacks; + + /// Create a temporary content pack. + private readonly Func CreateContentPack; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The content packs loaded for this mod. + /// Create a temporary content pack. + public ContentPackHelper(string modID, Lazy contentPacks, Func createContentPack) + : base(modID) + { + this.ContentPacks = contentPacks; + this.CreateContentPack = createContentPack; + } + + /// Get all content packs loaded for this mod. + public IEnumerable GetOwned() + { + return this.ContentPacks.Value; + } + + /// Create a temporary content pack to read files from a directory, using randomised manifest fields. This will generate fake manifest data; any manifest.json in the directory will be ignored. Temporary content packs will not appear in the SMAPI log and update checks will not be performed. + /// The absolute directory path containing the content pack files. + public IContentPack CreateFake(string directoryPath) + { + string id = Guid.NewGuid().ToString("N"); + return this.CreateTemporary(directoryPath, id, id, id, id, new SemanticVersion(1, 0, 0)); + } + + /// Create a temporary content pack to read files from a directory. Temporary content packs will not appear in the SMAPI log and update checks will not be performed. + /// The absolute directory path containing the content pack files. + /// The content pack's unique ID. + /// The content pack name. + /// The content pack description. + /// The content pack author's name. + /// The content pack version. + public IContentPack CreateTemporary(string directoryPath, string id, string name, string description, string author, ISemanticVersion version) + { + // validate + if (string.IsNullOrWhiteSpace(directoryPath)) + throw new ArgumentNullException(nameof(directoryPath)); + if (string.IsNullOrWhiteSpace(id)) + throw new ArgumentNullException(nameof(id)); + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + if (!Directory.Exists(directoryPath)) + throw new ArgumentException($"Can't create content pack for directory path '{directoryPath}' because no such directory exists."); + + // create manifest + IManifest manifest = new Manifest( + uniqueID: id, + name: name, + author: author, + description: description, + version: version, + contentPackFor: this.ModID + ); + + // create content pack + return this.CreateContentPack(directoryPath, manifest); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/DataHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/DataHelper.cs new file mode 100644 index 00000000..3b5c1752 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/DataHelper.cs @@ -0,0 +1,166 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for reading and storing local mod data. + internal class DataHelper : BaseHelper, IDataHelper + { + /********* + ** Fields + *********/ + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + /// The absolute path to the mod folder. + private readonly string ModFolderPath; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The absolute path to the mod folder. + /// The absolute path to the mod folder. + public DataHelper(string modID, string modFolderPath, JsonHelper jsonHelper) + : base(modID) + { + this.ModFolderPath = modFolderPath; + this.JsonHelper = jsonHelper; + } + + /**** + ** JSON file + ****/ + /// Read data from a JSON file in the mod's folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the mod folder. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The is not relative or contains directory climbing (../). + public TModel ReadJsonFile(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.NormalisePathSeparators(path)); + return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) + ? data + : null; + } + + /// Save data to a JSON file in the mod's folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the mod folder. + /// The arbitrary data to save. + /// The is not relative or contains directory climbing (../). + public void WriteJsonFile(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)."); + + path = Path.Combine(this.ModFolderPath, PathUtilities.NormalisePathSeparators(path)); + this.JsonHelper.WriteJsonFile(path, data); + } + + /**** + ** Save file + ****/ + /// Read arbitrary data stored in the current save slot. This is only possible if a save has been loaded. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// Returns the parsed data, or null if the entry doesn't exist or is empty. + /// The player hasn't loaded a save file yet or isn't the main player. + public TModel ReadSaveData(string key) where TModel : class + { + if (!Game1.hasLoadedGame) + throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} when a save file isn't loaded."); + if (!Game1.IsMasterGame) + throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); + + return Game1.CustomData.TryGetValue(this.GetSaveFileKey(key), out string value) + ? this.JsonHelper.Deserialise(value) + : null; + } + + /// 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. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// The arbitrary data to save. + /// The player hasn't loaded a save file yet or isn't the main player. + public void WriteSaveData(string key, TModel data) where TModel : class + { + if (!Game1.hasLoadedGame) + throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when a save file isn't loaded."); + if (!Game1.IsMasterGame) + throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); + + string internalKey = this.GetSaveFileKey(key); + if (data != null) + Game1.CustomData[internalKey] = this.JsonHelper.Serialise(data, Formatting.None); + else + Game1.CustomData.Remove(internalKey); + } + + /**** + ** Global app data + ****/ + /// Read arbitrary data stored on the local computer, synchronised by GOG/Steam if applicable. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// Returns the parsed data, or null if the entry doesn't exist or is empty. + public TModel ReadGlobalData(string key) where TModel : class + { + string path = this.GetGlobalDataPath(key); + return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) + ? data + : null; + } + + /// Save arbitrary data to the local computer, synchronised by GOG/Steam if applicable. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// The arbitrary data to save. + public void WriteGlobalData(string key, TModel data) where TModel : class + { + string path = this.GetGlobalDataPath(key); + if (data != null) + this.JsonHelper.WriteJsonFile(path, data); + else + File.Delete(path); + } + + + /********* + ** Public methods + *********/ + /// Get the unique key for a save file data entry. + /// The unique key identifying the data. + private string GetSaveFileKey(string key) + { + this.AssertSlug(key, nameof(key)); + return $"smapi/mod-data/{this.ModID}/{key}".ToLower(); + } + + /// Get the absolute path for a global data file. + /// The unique key identifying the data. + private string GetGlobalDataPath(string key) + { + this.AssertSlug(key, nameof(key)); + return Path.Combine(Constants.SavesPath, ".smapi", "mod-data", this.ModID.ToLower(), $"{key}.json".ToLower()); + } + + /// Assert that a key contains only characters that are safe in all contexts. + /// The key to check. + /// The argument name for any assertion error. + private void AssertSlug(string key, string paramName) + { + if (!PathUtilities.IsSlug(key)) + throw new ArgumentException("The data key is invalid (keys must only contain letters, numbers, underscores, periods, or hyphens).", paramName); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/InputHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/InputHelper.cs new file mode 100644 index 00000000..f4cd12b6 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/InputHelper.cs @@ -0,0 +1,54 @@ +using StardewModdingAPI.Framework.Input; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides an API for checking and changing input state. + internal class InputHelper : BaseHelper, IInputHelper + { + /********* + ** Accessors + *********/ + /// Manages the game's input state. + private readonly SInputState InputState; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// Manages the game's input state. + public InputHelper(string modID, SInputState inputState) + : base(modID) + { + this.InputState = inputState; + } + + /// Get the current cursor position. + public ICursorPosition GetCursorPosition() + { + return this.InputState.CursorPosition; + } + + /// Get whether a button is currently pressed. + /// The button. + public bool IsDown(SButton button) + { + return this.InputState.IsDown(button); + } + + /// Get whether a button is currently suppressed, so the game won't see it. + /// The button. + public bool IsSuppressed(SButton button) + { + return this.InputState.SuppressButtons.Contains(button); + } + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + public void Suppress(SButton button) + { + this.InputState.SuppressButtons.Add(button); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs new file mode 100644 index 00000000..6c9838c9 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.IO; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Input; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides simplified APIs for writing mods. + internal class ModHelper : BaseHelper, IModHelper, IDisposable + { + /********* + ** Accessors + *********/ + /// The full path to the mod's folder. + public string DirectoryPath { get; } + +#if !SMAPI_3_0_STRICT + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; +#endif + + /// Manages access to events raised by SMAPI, which let your mod react when something happens in the game. + public IModEvents Events { get; } + + /// An API for loading content assets. + public IContentHelper Content { get; } + + /// An API for managing content packs. + public IContentPackHelper ContentPacks { get; } + + /// An API for reading and writing persistent mod data. + public IDataHelper Data { get; } + + /// An API for checking and changing input state. + public IInputHelper Input { get; } + + /// An API for accessing private game code. + public IReflectionHelper Reflection { get; } + + /// an API for fetching metadata about loaded mods. + public IModRegistry ModRegistry { get; } + + /// An API for managing console commands. + public ICommandHelper ConsoleCommands { get; } + + /// Provides multiplayer utilities. + public IMultiplayerHelper Multiplayer { get; } + + /// An API for reading translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + public ITranslationHelper Translation { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's unique ID. + /// The full path to the mod's folder. + /// Encapsulate SMAPI's JSON parsing. + /// Manages the game's input state. + /// Manages access to events raised by SMAPI. + /// An API for loading content assets. + /// An API for managing content packs. + /// An API for managing console commands. + /// An API for reading and writing persistent mod data. + /// an API for fetching metadata about loaded mods. + /// An API for accessing private game code. + /// Provides multiplayer utilities. + /// An API for reading translations stored in the mod's i18n folder. + /// An argument is null or empty. + /// The path does not exist on disk. + public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, SInputState inputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper) + : base(modID) + { + // validate directory + if (string.IsNullOrWhiteSpace(modDirectory)) + throw new ArgumentNullException(nameof(modDirectory)); + if (!Directory.Exists(modDirectory)) + throw new InvalidOperationException("The specified mod directory does not exist."); + + // initialise + this.DirectoryPath = modDirectory; + this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); + this.ContentPacks = contentPackHelper ?? throw new ArgumentNullException(nameof(contentPackHelper)); + this.Data = dataHelper ?? throw new ArgumentNullException(nameof(dataHelper)); + this.Input = new InputHelper(modID, inputState); + this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); + this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); + this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); + this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer)); + this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); + this.Events = events; +#if !SMAPI_3_0_STRICT + this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper)); +#endif + } + + /**** + ** Mod config file + ****/ + /// Read the mod's configuration file (and create it if needed). + /// The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types. + public TConfig ReadConfig() + where TConfig : class, new() + { + TConfig config = this.Data.ReadJsonFile("config.json") ?? new TConfig(); + this.WriteConfig(config); // create file or fill in missing fields + return config; + } + + /// Save to the mod's configuration file. + /// The config class type. + /// The config settings to save. + public void WriteConfig(TConfig config) + where TConfig : class, new() + { + this.Data.WriteJsonFile("config.json", config); + } + +#if !SMAPI_3_0_STRICT + /**** + ** Generic JSON files + ****/ + /// Read a JSON file. + /// The model type. + /// The file path relative to the mod directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + [Obsolete("Use " + nameof(ModHelper.Data) + "." + nameof(IDataHelper.ReadJsonFile) + " instead")] + public TModel ReadJsonFile(string path) + where TModel : class + { + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); + return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) + ? data + : null; + } + + /// Save to a JSON file. + /// The model type. + /// The file path relative to the mod directory. + /// The model to save. + [Obsolete("Use " + nameof(ModHelper.Data) + "." + nameof(IDataHelper.WriteJsonFile) + " instead")] + public void WriteJsonFile(string path, TModel model) + where TModel : class + { + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); + this.JsonHelper.WriteJsonFile(path, model); + } +#endif + + /**** + ** Content packs + ****/ +#if !SMAPI_3_0_STRICT + /// Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI. + /// The absolute directory path containing the content pack files. + /// The content pack's unique ID. + /// The content pack name. + /// The content pack description. + /// The content pack author's name. + /// The content pack version. + [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ContentPacks) + "." + nameof(IContentPackHelper.CreateTemporary) + " instead")] + public IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version) + { + SCore.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.PendingRemoval); + return this.ContentPacks.CreateTemporary(directoryPath, id, name, description, author, version); + } + + /// Get all content packs loaded for this mod. + [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ContentPacks) + "." + nameof(IContentPackHelper.GetOwned) + " instead")] + public IEnumerable GetContentPacks() + { + return this.ContentPacks.GetOwned(); + } +#endif + + /**** + ** Disposal + ****/ + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + // nothing to dispose yet + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs new file mode 100644 index 00000000..8330e078 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Framework.Reflection; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides metadata about installed mods. + internal class ModRegistryHelper : BaseHelper, IModRegistry + { + /********* + ** Fields + *********/ + /// The underlying mod registry. + private readonly ModRegistry Registry; + + /// Encapsulates monitoring and logging for the mod. + private readonly IMonitor Monitor; + + /// The mod IDs for APIs accessed by this instanced. + private readonly HashSet AccessedModApis = new HashSet(); + + /// Generates proxy classes to access mod APIs through an arbitrary interface. + private readonly InterfaceProxyFactory ProxyFactory; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The underlying mod registry. + /// Generates proxy classes to access mod APIs through an arbitrary interface. + /// Encapsulates monitoring and logging for the mod. + public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor) + : base(modID) + { + this.Registry = registry; + this.ProxyFactory = proxyFactory; + this.Monitor = monitor; + } + + /// Get metadata for all loaded mods. + public IEnumerable GetAll() + { + return this.Registry.GetAll(); + } + + /// Get metadata for a loaded mod. + /// The mod's unique ID. + /// Returns the matching mod's metadata, or null if not found. + public IModInfo Get(string uniqueID) + { + return this.Registry.Get(uniqueID); + } + + /// Get whether a mod has been loaded. + /// The mod's unique ID. + public bool IsLoaded(string uniqueID) + { + return this.Registry.Get(uniqueID) != null; + } + + /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods. + public object GetApi(string 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}.", LogLevel.Trace); + return mod?.Api; + } + + /// 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 null. + /// The interface which matches the properties and methods you intend to access. + /// The mod's unique ID. + public TInterface GetApi(string uniqueID) where TInterface : class + { + // validate + if (!this.Registry.AreAllModsInitialised) + { + this.Monitor.Log("Tried to access a mod-provided API before all mods were initialised.", LogLevel.Error); + return null; + } + if (!typeof(TInterface).IsInterface) + { + this.Monitor.Log($"Tried to map a mod-provided API to class '{typeof(TInterface).FullName}'; must be a public interface.", LogLevel.Error); + return null; + } + if (!typeof(TInterface).IsPublic) + { + this.Monitor.Log($"Tried to map a mod-provided API to non-public interface '{typeof(TInterface).FullName}'; must be a public interface.", LogLevel.Error); + return null; + } + + // get raw API + object api = this.GetApi(uniqueID); + if (api == null) + return null; + + // get API of type + if (api is TInterface castApi) + return castApi; + return this.ProxyFactory.CreateProxy(api, this.ModID, uniqueID); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/MultiplayerHelper.cs new file mode 100644 index 00000000..c62dd121 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using StardewModdingAPI.Framework.Networking; +using StardewValley; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides multiplayer utilities. + internal class MultiplayerHelper : BaseHelper, IMultiplayerHelper + { + /********* + ** Fields + *********/ + /// SMAPI's core multiplayer utility. + private readonly SMultiplayer Multiplayer; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// SMAPI's core multiplayer utility. + public MultiplayerHelper(string modID, SMultiplayer multiplayer) + : base(modID) + { + this.Multiplayer = multiplayer; + } + + /// Get a new multiplayer ID. + public long GetNewID() + { + return this.Multiplayer.getNewID(); + } + + /// Get the locations which are being actively synced from the host. + public IEnumerable GetActiveLocations() + { + return this.Multiplayer.activeLocations(); + } + + /// Get a connected player. + /// The player's unique ID. + /// Returns the connected player, or null if no such player is connected. + public IMultiplayerPeer GetConnectedPlayer(long id) + { + return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer peer) + ? peer + : null; + } + + /// Get all connected players. + public IEnumerable GetConnectedPlayers() + { + return this.Multiplayer.Peers.Values; + } + + /// Send a message to mods installed by connected players. + /// The data type. This can be a class with a default constructor, or a value type. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + /// The or is null. + public void SendMessage(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null) + { + this.Multiplayer.BroadcastModMessage( + message: message, + messageType: messageType, + fromModID: this.ModID, + toModIDs: modIDs, + toPlayerIDs: playerIDs + ); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs new file mode 100644 index 00000000..0ce72a9e --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -0,0 +1,156 @@ +using System; +using System.Reflection; +using StardewModdingAPI.Framework.Reflection; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides helper methods for accessing private game code. + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + internal class ReflectionHelper : BaseHelper, IReflectionHelper + { + /********* + ** Fields + *********/ + /// The underlying reflection helper. + private readonly Reflector Reflector; + + /// The mod name for error messages. + private readonly string ModName; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The mod name for error messages. + /// The underlying reflection helper. + public ReflectionHelper(string modID, string modName, Reflector reflector) + : base(modID) + { + this.ModName = modName; + this.Reflector = reflector; + } + + /// Get an instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedField GetField(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetField(obj, name, required) + ); + } + + /// Get a static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedField GetField(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetField(type, name, required) + ); + } + + /// Get an instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetProperty(obj, name, required) + ); + } + + /// Get a static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetProperty(type, name, required) + ); + } + + /// Get an instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(object obj, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetMethod(obj, name, required) + ); + } + + /// Get a static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(Type type, string name, bool required = true) + { + return this.AssertAccessAllowed( + this.Reflector.GetMethod(type, name, required) + ); + } + + + /********* + ** Private methods + *********/ + /// Assert that mods can use the reflection helper to access the given member. + /// The field value type. + /// The field being accessed. + /// Returns the same field instance for convenience. + private IReflectedField AssertAccessAllowed(IReflectedField field) + { + this.AssertAccessAllowed(field?.FieldInfo); + return field; + } + + /// Assert that mods can use the reflection helper to access the given member. + /// The property value type. + /// The property being accessed. + /// Returns the same property instance for convenience. + private IReflectedProperty AssertAccessAllowed(IReflectedProperty property) + { + this.AssertAccessAllowed(property?.PropertyInfo); + return property; + } + + /// Assert that mods can use the reflection helper to access the given member. + /// The method being accessed. + /// Returns the same method instance for convenience. + private IReflectedMethod AssertAccessAllowed(IReflectedMethod method) + { + this.AssertAccessAllowed(method?.MethodInfo); + return method; + } + + /// Assert that mods can use the reflection helper to access the given member. + /// The member being accessed. + private void AssertAccessAllowed(MemberInfo member) + { + if (member == null) + return; + + // get type which defines the member + 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; + 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/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs new file mode 100644 index 00000000..3252e047 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + internal class TranslationHelper : BaseHelper, ITranslationHelper + { + /********* + ** Fields + *********/ + /// The name of the relevant mod for error messages. + private readonly string ModName; + + /// The translations for each locale. + private readonly IDictionary> All = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + + /// The translations for the current locale, with locale fallback taken into account. + private IDictionary ForLocale; + + + /********* + ** Accessors + *********/ + /// The current locale. + public string Locale { get; private set; } + + /// The game's current language code. + public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the relevant mod. + /// The name of the relevant mod for error messages. + /// The initial locale. + /// The game's current language code. + public TranslationHelper(string modID, string modName, string locale, LocalizedContentManager.LanguageCode languageCode) + : base(modID) + { + // save data + this.ModName = modName; + + // set locale + this.SetLocale(locale, languageCode); + } + + /// Get all translations for the current locale. + public IEnumerable GetTranslations() + { + return this.ForLocale.Values.ToArray(); + } + + /// Get a translation for the current locale. + /// The translation key. + public Translation Get(string key) + { + this.ForLocale.TryGetValue(key, out Translation translation); + return translation ?? new Translation(this.ModName, this.Locale, key, null); + } + + /// Get a translation for the current locale. + /// The translation key. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. + public Translation Get(string key, object tokens) + { + return this.Get(key).Tokens(tokens); + } + + /// Set the translations to use. + /// The translations to use. + internal TranslationHelper SetTranslations(IDictionary> translations) + { + // reset translations + this.All.Clear(); + foreach (var pair in translations) + this.All[pair.Key] = new Dictionary(pair.Value, StringComparer.InvariantCultureIgnoreCase); + + // rebuild cache + this.SetLocale(this.Locale, this.LocaleEnum); + + return this; + } + + /// Set the current locale and precache translations. + /// The current locale. + /// The game's current language code. + internal void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) + { + this.Locale = locale.ToLower().Trim(); + this.LocaleEnum = localeEnum; + + this.ForLocale = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (string next in this.GetRelevantLocales(this.Locale)) + { + // skip if locale not defined + if (!this.All.TryGetValue(next, out IDictionary translations)) + continue; + + // add missing translations + foreach (var pair in translations) + { + if (!this.ForLocale.ContainsKey(pair.Key)) + this.ForLocale.Add(pair.Key, new Translation(this.ModName, this.Locale, pair.Key, pair.Value)); + } + } + } + + + /********* + ** Private methods + *********/ + /// Get the locales which can provide translations for the given locale, in precedence order. + /// The locale for which to find valid locales. + private IEnumerable GetRelevantLocales(string locale) + { + // given locale + yield return locale; + + // broader locales (like pt-BR => pt) + while (true) + { + int dashIndex = locale.LastIndexOf('-'); + if (dashIndex <= 0) + break; + + locale = locale.Substring(0, dashIndex); + yield return locale; + } + + // default + if (locale != "default") + yield return "default"; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs new file mode 100644 index 00000000..5d629bf7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// A minimal assembly definition resolver which resolves references to known assemblies. + internal class AssemblyDefinitionResolver : DefaultAssemblyResolver + { + /********* + ** Fields + *********/ + /// The known assemblies. + private readonly IDictionary Lookup = new Dictionary(); + + /********* + ** Public methods + *********/ + /// Add known assemblies to the resolver. + /// The known assemblies. + public void Add(params AssemblyDefinition[] assemblies) + { + foreach (AssemblyDefinition assembly in assemblies) + { + this.RegisterAssembly(assembly); + this.Lookup[assembly.Name.Name] = assembly; + this.Lookup[assembly.Name.FullName] = assembly; + } + } + + /// Resolve an assembly reference. + /// The assembly name. + public override AssemblyDefinition Resolve(AssemblyNameReference name) => this.ResolveName(name.Name) ?? base.Resolve(name); + + + /// Resolve an assembly reference. + /// The assembly name. + /// The assembly reader parameters. + public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) => this.ResolveName(name.Name) ?? base.Resolve(name, parameters); + + + /********* + ** Private methods + *********/ + /// Resolve a known assembly definition based on its short or full name. + /// The assembly's short or full name. + private AssemblyDefinition ResolveName(string name) + { + return this.Lookup.TryGetValue(name, out AssemblyDefinition match) + ? match + : null; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs new file mode 100644 index 00000000..11be19fc --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates the result of an assembly load. + internal enum AssemblyLoadStatus + { + /// The assembly was loaded successfully. + Okay = 1, + + /// The assembly could not be loaded. + Failed = 2, + + /// The assembly is already loaded. + AlreadyLoaded = 3 + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs new file mode 100644 index 00000000..878b3148 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Metadata; +using StardewModdingAPI.Toolkit.Framework.ModData; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Preprocesses and loads mod assemblies. + internal class AssemblyLoader : IDisposable + { + /********* + ** Fields + *********/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// Whether to detect paranoid mode issues. + private readonly bool ParanoidMode; + + /// Metadata for mapping assemblies to the current platform. + private readonly PlatformAssemblyMap AssemblyMap; + + /// A type => assembly lookup for types which should be rewritten. + private readonly IDictionary TypeAssemblies; + + /// A minimal assembly definition resolver which resolves references to known loaded assemblies. + private readonly AssemblyDefinitionResolver AssemblyDefinitionResolver; + + /// The objects to dispose as part of this instance. + private readonly HashSet Disposables = new HashSet(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The current game platform. + /// Encapsulates monitoring and logging. + /// Whether to detect paranoid mode issues. + public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode) + { + this.Monitor = monitor; + this.ParanoidMode = paranoidMode; + this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); + this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); + this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); + this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.InternalFilesPath); + + // generate type => assembly lookup for types which should be rewritten + this.TypeAssemblies = new Dictionary(); + foreach (Assembly assembly in this.AssemblyMap.Targets) + { + ModuleDefinition module = this.AssemblyMap.TargetModules[assembly]; + foreach (TypeDefinition type in module.GetTypes()) + { + if (!type.IsPublic) + continue; // no need to rewrite + if (type.Namespace.Contains("<")) + continue; // ignore assembly metadata + this.TypeAssemblies[type.FullName] = assembly; + } + } + } + + /// Preprocess and load an assembly. + /// The mod for which the assembly is being loaded. + /// The assembly file path. + /// Assume the mod is compatible, even if incompatible code is detected. + /// Returns the rewrite metadata for the preprocessed assembly. + /// An incompatible CIL instruction was found while rewriting the assembly. + public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) + { + // get referenced local assemblies + AssemblyParseResult[] assemblies; + { + HashSet visitedAssemblyNames = new HashSet(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded + assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, this.AssemblyDefinitionResolver).ToArray(); + } + + // validate load + if (!assemblies.Any() || assemblies[0].Status == AssemblyLoadStatus.Failed) + { + throw new SAssemblyLoadFailedException(!File.Exists(assemblyPath) + ? $"Could not load '{assemblyPath}' because it doesn't exist." + : $"Could not load '{assemblyPath}'." + ); + } + if (assemblies.Last().Status == AssemblyLoadStatus.AlreadyLoaded) // mod assembly is last in dependency order + throw new SAssemblyLoadFailedException($"Could not load '{assemblyPath}' because it was already loaded. Do you have two copies of this mod?"); + + // rewrite & load assemblies in leaf-to-root order + bool oneAssembly = assemblies.Length == 1; + Assembly lastAssembly = null; + HashSet loggedMessages = new HashSet(); + foreach (AssemblyParseResult assembly in assemblies) + { + if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) + continue; + + // rewrite assembly + bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + + // detect broken assembly reference + foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) + { + if (!reference.Name.StartsWith("System.") && !this.IsAssemblyLoaded(reference)) + { + this.Monitor.LogOnce(loggedMessages, $" Broken code in {assembly.File.Name}: reference to missing assembly '{reference.FullName}'."); + if (!assumeCompatible) + throw new IncompatibleInstructionException($"assembly reference to {reference.FullName}", $"Found a reference to missing assembly '{reference.FullName}' while loading assembly {assembly.File.Name}."); + mod.SetWarning(ModWarning.BrokenCodeLoaded); + break; + } + } + + // load assembly + if (changed) + { + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); + using (MemoryStream outStream = new MemoryStream()) + { + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); + } + } + else + { + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); + lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); + } + + // track loaded assembly for definition resolution + this.AssemblyDefinitionResolver.Add(assembly.Definition); + } + + // last assembly loaded is the root + return lastAssembly; + } + + /// Get whether an assembly is loaded. + /// The assembly name reference. + public bool IsAssemblyLoaded(AssemblyNameReference reference) + { + try + { + return this.AssemblyDefinitionResolver.Resolve(reference) != null; + } + catch (AssemblyResolutionException) + { + return false; + } + } + + /// Resolve an assembly by its name. + /// The assembly name. + /// + /// This implementation returns the first loaded assembly which matches the short form of + /// the assembly name, to resolve assembly resolution issues when rewriting + /// assemblies (especially with Mono). Since this is meant to be called on , + /// the implicit assumption is that loading the exact assembly failed. + /// + public static Assembly ResolveAssembly(string name) + { + string shortName = name.Split(new[] { ',' }, 2).First(); // get simple name (without version and culture) + return AppDomain.CurrentDomain + .GetAssemblies() + .FirstOrDefault(p => p.GetName().Name == shortName); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + foreach (IDisposable instance in this.Disposables) + instance.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Track an object for disposal as part of the assembly loader. + /// The instance type. + /// The disposable instance. + private T TrackForDisposal(T instance) where T : IDisposable + { + this.Disposables.Add(instance); + return instance; + } + + /**** + ** Assembly parsing + ****/ + /// Get a list of referenced local assemblies starting from the mod assembly, ordered from leaf to root. + /// The assembly file to load. + /// The assembly names that should be skipped. + /// A resolver which resolves references to known assemblies. + /// Returns the rewrite metadata for the preprocessed assembly. + private IEnumerable GetReferencedLocalAssemblies(FileInfo file, HashSet visitedAssemblyNames, IAssemblyResolver assemblyResolver) + { + // validate + if (file.Directory == null) + throw new InvalidOperationException($"Could not get directory from file path '{file.FullName}'."); + if (!file.Exists) + yield break; // not a local assembly + + // read assembly + byte[] assemblyBytes = File.ReadAllBytes(file.FullName); + Stream readStream = this.TrackForDisposal(new MemoryStream(assemblyBytes)); + AssemblyDefinition assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true })); + + // skip if already visited + if (visitedAssemblyNames.Contains(assembly.Name.Name)) + { + yield return new AssemblyParseResult(file, null, AssemblyLoadStatus.AlreadyLoaded); + yield break; + } + visitedAssemblyNames.Add(assembly.Name.Name); + + // yield referenced assemblies + foreach (AssemblyNameReference dependency in assembly.MainModule.AssemblyReferences) + { + FileInfo dependencyFile = new FileInfo(Path.Combine(file.Directory.FullName, $"{dependency.Name}.dll")); + foreach (AssemblyParseResult result in this.GetReferencedLocalAssemblies(dependencyFile, visitedAssemblyNames, assemblyResolver)) + yield return result; + } + + // yield assembly + yield return new AssemblyParseResult(file, assembly, AssemblyLoadStatus.Okay); + } + + /**** + ** Assembly rewriting + ****/ + /// Rewrite the types referenced by an assembly. + /// The mod for which the assembly is being loaded. + /// The assembly to rewrite. + /// Assume the mod is compatible, even if incompatible code is detected. + /// The messages that have already been logged for this mod. + /// A string to prefix to log messages. + /// Returns whether the assembly was modified. + /// An incompatible CIL instruction was found while rewriting the assembly. + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) + { + ModuleDefinition module = assembly.MainModule; + string filename = $"{assembly.Name.Name}.dll"; + + // swap assembly references if needed (e.g. XNA => MonoGame) + bool platformChanged = false; + for (int i = 0; i < module.AssemblyReferences.Count; i++) + { + // remove old assembly reference + if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) + { + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); + platformChanged = true; + module.AssemblyReferences.RemoveAt(i); + i--; + } + } + if (platformChanged) + { + // add target assembly references + foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values) + module.AssemblyReferences.Add(target); + + // rewrite type scopes to use target assemblies + IEnumerable typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); + foreach (TypeReference type in typeReferences) + this.ChangeTypeScope(type); + } + + // find (and optionally rewrite) incompatible instructions + bool anyRewritten = false; + IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode).ToArray(); + foreach (MethodDefinition method in this.GetMethods(module)) + { + // check method definition + foreach (IInstructionHandler handler in handlers) + { + InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); + this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; + } + + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + var instructions = cil.Body.Instructions; + // ReSharper disable once ForCanBeConvertedToForeach -- deliberate access by index so each handler sees replacements from previous handlers + for (int offset = 0; offset < instructions.Count; offset++) + { + foreach (IInstructionHandler handler in handlers) + { + Instruction instruction = instructions[offset]; + InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); + this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; + } + } + } + + return platformChanged || anyRewritten; + } + + /// Process the result from an instruction handler. + /// The mod being analysed. + /// The instruction handler. + /// The result returned by the handler. + /// The messages already logged for the current mod. + /// Assume the mod is compatible, even if incompatible code is detected. + /// A string to prefix to log messages. + /// The assembly filename for log messages. + private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet loggedMessages, string logPrefix, bool assumeCompatible, string filename) + { + switch (result) + { + case InstructionHandleResult.Rewritten: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); + break; + + case InstructionHandleResult.NotCompatible: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Broken code in {filename}: {handler.NounPhrase}."); + if (!assumeCompatible) + throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); + mod.SetWarning(ModWarning.BrokenCodeLoaded); + break; + + case InstructionHandleResult.DetectedGamePatch: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.PatchesGame); + break; + + case InstructionHandleResult.DetectedSaveSerialiser: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.ChangesSaveSerialiser); + break; + + case InstructionHandleResult.DetectedUnvalidatedUpdateTick: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); + mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick); + break; + + case InstructionHandleResult.DetectedDynamic: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.UsesDynamic); + break; + + case InstructionHandleResult.DetectedFilesystemAccess: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected filesystem access ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.AccessesFilesystem); + break; + + case InstructionHandleResult.DetectedShellAccess: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected shell or process access ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.AccessesShell); + break; + + case InstructionHandleResult.None: + break; + + default: + throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); + } + } + + /// Get the correct reference to use for compatibility with the current platform. + /// The type reference to rewrite. + 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)) + return; + + // replace scope + AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; + type.Scope = assemblyRef; + } + + /// Get all methods in a module. + /// The module to search. + private IEnumerable GetMethods(ModuleDefinition module) + { + return ( + from type in module.GetTypes() + where type.HasMethods + from method in type.Methods + where method.HasBody + select method + ); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs new file mode 100644 index 00000000..b56a776c --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs @@ -0,0 +1,36 @@ +using System.IO; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata about a parsed assembly definition. + internal class AssemblyParseResult + { + /********* + ** Accessors + *********/ + /// The original assembly file. + public readonly FileInfo File; + + /// The assembly definition. + public readonly AssemblyDefinition Definition; + + /// The result of the assembly load. + public AssemblyLoadStatus Status; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The original assembly file. + /// The assembly definition. + /// The result of the assembly load. + public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly, AssemblyLoadStatus status) + { + this.File = file; + this.Definition = assembly; + this.Status = status; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs new file mode 100644 index 00000000..898bafb4 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -0,0 +1,82 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given event. + internal class EventFinder : IInstructionHandler + { + /********* + ** Fields + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The event name for which to find references. + private readonly string EventName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name for which to find references. + /// The event name for which to find references. + /// The result to return for matching instructions. + public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) + { + this.FullTypeName = fullTypeName; + this.EventName = eventName; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{eventName} event"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.FullTypeName + && (methodRef.Name == "add_" + this.EventName || methodRef.Name == "remove_" + this.EventName); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs new file mode 100644 index 00000000..606ca8b7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -0,0 +1,82 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given field. + internal class FieldFinder : IInstructionHandler + { + /********* + ** Fields + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The field name for which to find references. + private readonly string FieldName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name for which to find references. + /// The field name for which to find references. + /// The result to return for matching instructions. + public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) + { + this.FullTypeName = fullTypeName; + this.FieldName = fieldName; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{fieldName} field"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + return + fieldRef != null + && fieldRef.DeclaringType.FullName == this.FullTypeName + && fieldRef.Name == this.FieldName; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs new file mode 100644 index 00000000..9ca246ff --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -0,0 +1,82 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given method. + internal class MethodFinder : IInstructionHandler + { + /********* + ** Fields + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The method name for which to find references. + private readonly string MethodName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name for which to find references. + /// The method name for which to find references. + /// The result to return for matching instructions. + public MethodFinder(string fullTypeName, string methodName, InstructionHandleResult result) + { + this.FullTypeName = fullTypeName; + this.MethodName = methodName; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{methodName} method"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.FullTypeName + && methodRef.Name == this.MethodName; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs new file mode 100644 index 00000000..0677aa88 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -0,0 +1,82 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given property. + internal class PropertyFinder : IInstructionHandler + { + /********* + ** Fields + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The property name for which to find references. + private readonly string PropertyName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name for which to find references. + /// The property name for which to find references. + /// The result to return for matching instructions. + public PropertyFinder(string fullTypeName, string propertyName, InstructionHandleResult result) + { + this.FullTypeName = fullTypeName; + this.PropertyName = propertyName; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{propertyName} property"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.FullTypeName + && (methodRef.Name == "get_" + this.PropertyName || methodRef.Name == "set_" + this.PropertyName); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs new file mode 100644 index 00000000..82c4920a --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds references to a field, property, or method which returns a different type than the code expects. + /// This implementation is purely heuristic. It should never return a false positive, but won't detect all cases. + internal class ReferenceToMemberWithUnexpectedTypeFinder : IInstructionHandler + { + /********* + ** Fields + *********/ + /// The assembly names to which to heuristically detect broken references. + private readonly HashSet ValidateReferencesToAssemblies; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; private set; } = ""; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The assembly names to which to heuristically detect broken references. + public ReferenceToMemberWithUnexpectedTypeFinder(string[] validateReferencesToAssemblies) + { + this.ValidateReferencesToAssemblies = new HashSet(validateReferencesToAssemblies); + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + // field reference + 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); + if (targetField == null) + return InstructionHandleResult.None; + + // validate return type + if (!RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType)) + { + this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})"; + return InstructionHandleResult.NotCompatible; + } + } + + // method reference + 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(); + if (candidateMethods == null || !candidateMethods.Any()) + return InstructionHandleResult.None; + + // compare return types + MethodDefinition methodDef = methodReference.Resolve(); + if (methodDef == null) + { + this.NounPhrase = $"reference to {methodReference.DeclaringType.FullName}.{methodReference.Name} (no such method)"; + return InstructionHandleResult.NotCompatible; + } + + if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) + { + this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"; + return InstructionHandleResult.NotCompatible; + } + } + + return InstructionHandleResult.None; + } + + + /********* + ** Private methods + *********/ + /// Whether references to the given type should be validated. + /// The type reference. + private bool ShouldValidate(TypeReference type) + { + return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name); + } + + /// Get whether a method reference is a special case that's not currently supported (e.g. array methods). + /// The method reference. + private bool IsUnsupported(MethodReference method) + { + return + method.DeclaringType.Name.Contains("["); // array methods + } + + /// Get a shorter type name for display. + /// The type reference. + private string GetFriendlyTypeName(TypeReference type) + { + // most common built-in types + switch (type.FullName) + { + case "System.Boolean": + return "bool"; + case "System.Int32": + return "int"; + case "System.String": + return "string"; + } + + // most common unambiguous namespaces + foreach (string @namespace in new[] { "Microsoft.Xna.Framework", "Netcode", "System", "System.Collections.Generic" }) + { + if (type.Namespace == @namespace) + return type.Name; + } + + return type.FullName; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs new file mode 100644 index 00000000..44b531a5 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds references to a field, property, or method which no longer exists. + /// This implementation is purely heuristic. It should never return a false positive, but won't detect all cases. + internal class ReferenceToMissingMemberFinder : IInstructionHandler + { + /********* + ** Fields + *********/ + /// The assembly names to which to heuristically detect broken references. + private readonly HashSet ValidateReferencesToAssemblies; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; private set; } = ""; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The assembly names to which to heuristically detect broken references. + public ReferenceToMissingMemberFinder(string[] validateReferencesToAssemblies) + { + this.ValidateReferencesToAssemblies = new HashSet(validateReferencesToAssemblies); + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + // field reference + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) + { + FieldDefinition target = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); + if (target == null) + { + this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"; + return InstructionHandleResult.NotCompatible; + } + } + + // method reference + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef != null && this.ShouldValidate(methodRef.DeclaringType) && !this.IsUnsupported(methodRef)) + { + MethodDefinition target = methodRef.Resolve(); + if (target == null) + { + if (this.IsProperty(methodRef)) + this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; + else if (methodRef.Name == ".ctor") + this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)"; + else + this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; + return InstructionHandleResult.NotCompatible; + } + } + + return InstructionHandleResult.None; + } + + + /********* + ** Private methods + *********/ + /// Whether references to the given type should be validated. + /// The type reference. + private bool ShouldValidate(TypeReference type) + { + return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name); + } + + /// Get whether a method reference is a special case that's not currently supported (e.g. array methods). + /// The method reference. + private bool IsUnsupported(MethodReference method) + { + return + method.DeclaringType.Name.Contains("["); // array methods + } + + /// Get whether a method reference is a property getter or setter. + /// The method reference. + private bool IsProperty(MethodReference method) + { + return method.Name.StartsWith("get_") || method.Name.StartsWith("set_"); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs new file mode 100644 index 00000000..79045241 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -0,0 +1,139 @@ +using System; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given type. + internal class TypeFinder : IInstructionHandler + { + /********* + ** Accessors + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + /// A lambda which overrides a matched type. + protected readonly Func ShouldIgnore; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name to match. + /// The result to return for matching instructions. + /// A lambda which overrides a matched type. + public TypeFinder(string fullTypeName, InstructionHandleResult result, Func shouldIgnore = null) + { + this.FullTypeName = fullTypeName; + this.Result = result; + this.NounPhrase = $"{fullTypeName} type"; + this.ShouldIgnore = shouldIgnore ?? (p => false); + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(method) + ? this.Result + : InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The method deifnition. + protected bool IsMatch(MethodDefinition method) + { + if (this.IsMatch(method.ReturnType)) + return true; + + foreach (VariableDefinition variable in method.Body.Variables) + { + if (this.IsMatch(variable.VariableType)) + return true; + } + + return false; + } + + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + // field reference + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null) + { + return + this.IsMatch(fieldRef.DeclaringType) // field on target class + || this.IsMatch(fieldRef.FieldType); // field value is target class + } + + // method reference + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef != null) + { + return + this.IsMatch(methodRef.DeclaringType) // method on target class + || this.IsMatch(methodRef.ReturnType) // method returns target class + || methodRef.Parameters.Any(p => this.IsMatch(p.ParameterType)); // method parameters + } + + return false; + } + + /// Get whether a type reference matches the expected type. + /// The type to check. + protected bool IsMatch(TypeReference type) + { + // root type + if (type.FullName == this.FullTypeName && !this.ShouldIgnore(type)) + return true; + + // generic arguments + if (type is GenericInstanceType genericType) + { + if (genericType.GenericArguments.Any(this.IsMatch)) + return true; + } + + // generic parameters (e.g. constraints) + if (type.GenericParameters.Any(this.IsMatch)) + return true; + + return false; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs b/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs new file mode 100644 index 00000000..8830cc74 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs @@ -0,0 +1,34 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Performs predefined logic for detected CIL instructions. + internal interface IInstructionHandler + { + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the handler matches. + string NounPhrase { get; } + + + /********* + ** Methods + *********/ + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs b/src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs new file mode 100644 index 00000000..17ec24b1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs @@ -0,0 +1,35 @@ +using System; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// An exception raised when an incompatible instruction is found while loading a mod assembly. + internal class IncompatibleInstructionException : Exception + { + /********* + ** Accessors + *********/ + /// A brief noun phrase which describes the incompatible instruction that was found. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A brief noun phrase which describes the incompatible instruction that was found. + public IncompatibleInstructionException(string nounPhrase) + : base($"Found an incompatible CIL instruction ({nounPhrase}).") + { + this.NounPhrase = nounPhrase; + } + + /// Construct an instance. + /// A brief noun phrase which describes the incompatible instruction that was found. + /// A message which describes the error. + public IncompatibleInstructionException(string nounPhrase, string message) + : base(message) + { + this.NounPhrase = nounPhrase; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs new file mode 100644 index 00000000..6592760e --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -0,0 +1,35 @@ +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates how an instruction was handled. + internal enum InstructionHandleResult + { + /// No special handling is needed. + None, + + /// The instruction was successfully rewritten for compatibility. + Rewritten, + + /// The instruction is not compatible and can't be rewritten for compatibility. + NotCompatible, + + /// The instruction is compatible, but patches the game in a way that may impact stability. + DetectedGamePatch, + + /// The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod. + DetectedSaveSerialiser, + + /// The instruction is compatible, but uses the dynamic keyword which won't work on Linux/Mac. + DetectedDynamic, + + /// The instruction is compatible, but references or which may impact stability. + DetectedUnvalidatedUpdateTick, + + /// The instruction accesses the filesystem directly. + DetectedFilesystemAccess, + + /// The instruction accesses the OS shell or processes directly. + DetectedShellAccess + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs b/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs new file mode 100644 index 00000000..075e237a --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs @@ -0,0 +1,14 @@ +using System; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// An exception which indicates that something went seriously wrong while loading mods, and SMAPI should abort outright. + internal class InvalidModStateException : Exception + { + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public InvalidModStateException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs new file mode 100644 index 00000000..0774b487 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// The status of a given mod in the dependency-sorting algorithm. + internal enum ModDependencyStatus + { + /// The mod hasn't been visited yet. + Queued, + + /// The mod is currently being analysed as part of a dependency chain. + Checking, + + /// The mod has already been sorted. + Sorted, + + /// The mod couldn't be sorted due to a metadata issue (e.g. missing dependencies). + Failed + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs new file mode 100644 index 00000000..4ff021b7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata for a mod. + internal class ModMetadata : IModMetadata + { + /********* + ** Accessors + *********/ + /// The mod's display name. + public string DisplayName { get; } + + /// The mod's full directory path. + public string DirectoryPath { get; } + + /// The relative to the game's Mods folder. + public string RelativeDirectoryPath { get; } + + /// The mod manifest. + public IManifest Manifest { get; } + + /// Metadata about the mod from SMAPI's internal data (if any). + public ModDataRecordVersionedFields DataRecord { get; } + + /// The metadata resolution status. + public ModMetadataStatus Status { get; private set; } + + /// Indicates non-error issues with the mod. + public ModWarning Warnings { get; private set; } + + /// The reason the metadata is invalid, if any. + public string Error { get; private set; } + + /// Whether the mod folder should be ignored. This is true if it was found within a folder whose name starts with a dot. + public bool IsIgnored { get; } + + /// The mod instance (if loaded and is false). + public IMod Mod { get; private set; } + + /// The content pack instance (if loaded and is true). + public IContentPack ContentPack { get; private set; } + + /// Writes messages to the console and log file as this mod. + public IMonitor Monitor { get; private set; } + + /// The mod-provided API (if any). + public object Api { get; private set; } + + /// The update-check metadata for this mod (if any). + public ModEntryModel UpdateCheckData { get; private set; } + + /// Whether the mod is a content pack. + public bool IsContentPack => this.Manifest?.ContentPackFor != null; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's display name. + /// The mod's full directory path. + /// The relative to the game's Mods folder. + /// The mod manifest. + /// Metadata about the mod from SMAPI's internal data (if any). + /// Whether the mod folder should be ignored. This should be true if it was found within a folder whose name starts with a dot. + public ModMetadata(string displayName, string directoryPath, string relativeDirectoryPath, IManifest manifest, ModDataRecordVersionedFields dataRecord, bool isIgnored) + { + this.DisplayName = displayName; + this.DirectoryPath = directoryPath; + this.RelativeDirectoryPath = relativeDirectoryPath; + this.Manifest = manifest; + this.DataRecord = dataRecord; + this.IsIgnored = isIgnored; + } + + /// Set the mod status. + /// The metadata resolution status. + /// The reason the metadata is invalid, if any. + /// Return the instance for chaining. + public IModMetadata SetStatus(ModMetadataStatus status, string error = null) + { + this.Status = status; + this.Error = error; + return this; + } + + /// Set a warning flag for the mod. + /// The warning to set. + public IModMetadata SetWarning(ModWarning warning) + { + this.Warnings |= warning; + return this; + } + + /// Set the mod instance. + /// The mod instance to set. + public IModMetadata SetMod(IMod mod) + { + if (this.ContentPack != null) + throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); + + this.Mod = mod; + this.Monitor = mod.Monitor; + return this; + } + + /// Set the mod instance. + /// The contentPack instance to set. + /// Writes messages to the console and log file. + public IModMetadata SetMod(IContentPack contentPack, IMonitor monitor) + { + if (this.Mod != null) + throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); + + this.ContentPack = contentPack; + this.Monitor = monitor; + return this; + } + + /// Set the mod-provided API instance. + /// The mod-provided API. + public IModMetadata SetApi(object api) + { + this.Api = api; + return this; + } + + /// Set the update-check metadata for this mod. + /// The update-check metadata. + public IModMetadata SetUpdateData(ModEntryModel data) + { + this.UpdateCheckData = data; + return this; + } + + /// Whether the mod manifest was loaded (regardless of whether the mod itself was loaded). + public bool HasManifest() + { + return this.Manifest != null; + } + + /// Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded). + public bool HasID() + { + return + this.HasManifest() + && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID); + } + + /// Whether the mod has the given ID. + /// The mod ID to check. + public bool HasID(string id) + { + return + this.HasID() + && string.Equals(this.Manifest.UniqueID.Trim(), id?.Trim(), StringComparison.InvariantCultureIgnoreCase); + } + + /// Get the defined update keys. + /// Only return valid update keys. + public IEnumerable GetUpdateKeys(bool validOnly = false) + { + foreach (string rawKey in this.Manifest?.UpdateKeys ?? new string[0]) + { + UpdateKey updateKey = UpdateKey.Parse(rawKey); + if (updateKey.LooksValid || !validOnly) + yield return updateKey; + } + } + + /// Whether the mod has at least one valid update key set. + public bool HasValidUpdateKeys() + { + return this.GetUpdateKeys(validOnly: true).Any(); + } + + /// Get whether the mod has a given warning and it hasn't been suppressed in the . + /// The warning to check. + public bool HasUnsuppressWarning(ModWarning warning) + { + return + this.Warnings.HasFlag(warning) + && (this.DataRecord?.DataRecord == null || !this.DataRecord.DataRecord.SuppressWarnings.HasFlag(warning)); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs new file mode 100644 index 00000000..ab65f7b4 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates the status of a mod's metadata resolution. + internal enum ModMetadataStatus + { + /// The mod has been found, but hasn't been processed yet. + Found, + + /// The mod cannot be loaded. + Failed + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs new file mode 100644 index 00000000..75d3849d --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -0,0 +1,449 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.ModScanning; +using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Finds and processes mod metadata. + internal class ModResolver + { + /********* + ** Public methods + *********/ + /// Get manifest metadata for each folder in the given root path. + /// The mod toolkit. + /// The root path to search for mods. + /// Handles access to SMAPI's internal mod metadata list. + /// Returns the manifests by relative folder. + public IEnumerable ReadManifests(ModToolkit toolkit, string rootPath, ModDatabase modDatabase) + { + foreach (ModFolder folder in toolkit.GetModFolders(rootPath)) + { + Manifest manifest = folder.Manifest; + + // parse internal data record (if any) + ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest); + + // apply defaults + if (manifest != null && dataRecord != null) + { + if (dataRecord.UpdateKey != null) + manifest.UpdateKeys = new[] { dataRecord.UpdateKey }; + } + + // build metadata + ModMetadataStatus status = folder.ManifestParseError == null || !folder.ShouldBeLoaded + ? ModMetadataStatus.Found + : ModMetadataStatus.Failed; + string relativePath = PathUtilities.GetRelativePath(rootPath, folder.Directory.FullName); + + yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, relativePath, manifest, dataRecord, isIgnored: !folder.ShouldBeLoaded) + .SetStatus(status, !folder.ShouldBeLoaded ? "disabled by dot convention" : folder.ManifestParseError); + } + } + + /// Validate manifest metadata. + /// The mod manifests to validate. + /// The current SMAPI version. + /// Get an update URL for an update key (if valid). + public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion, Func getUpdateUrl) + { + mods = mods.ToArray(); + + // validate each manifest + foreach (IModMetadata mod in mods) + { + // skip if already failed + if (mod.Status == ModMetadataStatus.Failed) + continue; + + // validate compatibility from internal data + switch (mod.DataRecord?.Status) + { + case ModStatus.Obsolete: + mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); + continue; + + case ModStatus.AssumeBroken: + { + // get reason + string reasonPhrase = mod.DataRecord.StatusReasonPhrase ?? "it's no longer compatible"; + + // get update URLs + List updateUrls = new List(); + foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) + { + string url = getUpdateUrl(key); + if (url != null) + updateUrls.Add(url); + } + if (mod.DataRecord.AlternativeUrl != null) + updateUrls.Add(mod.DataRecord.AlternativeUrl); + + // default update URL + updateUrls.Add("https://mods.smapi.io"); + + // build error + string error = $"{reasonPhrase}. Please check for a "; + if (mod.DataRecord.StatusUpperVersion == null || mod.Manifest.Version.Equals(mod.DataRecord.StatusUpperVersion)) + error += "newer version"; + else + error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; + error += " at " + string.Join(" or ", updateUrls); + + mod.SetStatus(ModMetadataStatus.Failed, error); + } + continue; + } + + // validate SMAPI version + if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) + { + mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); + continue; + } + + // validate DLL / content pack fields + { + bool hasDll = !string.IsNullOrWhiteSpace(mod.Manifest.EntryDll); + bool isContentPack = mod.Manifest.ContentPackFor != null; + + // validate field presence + if (!hasDll && !isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); + continue; + } + if (hasDll && isContentPack) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); + continue; + } + + // validate DLL + if (hasDll) + { + // invalid filename format + if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + mod.SetStatus(ModMetadataStatus.Failed, $"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))) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + continue; + } + + // invalid capitalisation + string actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll).FirstOrDefault()?.Name; + if (actualFilename != mod.Manifest.EntryDll) + { +#if SMAPI_3_0_STRICT + mod.SetStatus(ModMetadataStatus.Failed, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalisation '{actualFilename}'. The capitalisation must match for crossplatform compatibility."); + continue; +#else + SCore.DeprecationManager.Warn(mod.DisplayName, $"{nameof(IManifest.EntryDll)} value with case-insensitive capitalisation", "2.11", DeprecationLevel.PendingRemoval); +#endif + } + } + + // validate content pack + else + { + // invalid content pack ID + if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); + continue; + } + } + } + + // validate required fields + { + List missingFields = new List(3); + + if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) + missingFields.Add(nameof(IManifest.Name)); + if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") + missingFields.Add(nameof(IManifest.Version)); + if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) + missingFields.Add(nameof(IManifest.UniqueID)); + + if (missingFields.Any()) + { + mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + continue; + } + } + + // validate ID format + if (!PathUtilities.IsSlug(mod.Manifest.UniqueID)) + mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); + } + + // validate IDs are unique + { + var duplicatesByID = mods + .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) + .Where(p => p.Count() > 1); + foreach (var group in duplicatesByID) + { + foreach (IModMetadata mod in group) + { + if (mod.Status == ModMetadataStatus.Failed) + continue; // don't replace metadata error + mod.SetStatus(ModMetadataStatus.Failed, $"you have multiple copies of this mod installed ({string.Join(", ", group.Select(p => p.RelativeDirectoryPath).OrderBy(p => p))})."); + } + } + } + } + + /// Sort the given mods by the order they should be loaded. + /// The mods to process. + /// Handles access to SMAPI's internal mod metadata list. + public IEnumerable ProcessDependencies(IEnumerable mods, ModDatabase modDatabase) + { + // initialise metadata + mods = mods.ToArray(); + var sortedMods = new Stack(); + var states = mods.ToDictionary(mod => mod, mod => ModDependencyStatus.Queued); + + // handle failed mods + foreach (IModMetadata mod in mods.Where(m => m.Status == ModMetadataStatus.Failed)) + { + states[mod] = ModDependencyStatus.Failed; + sortedMods.Push(mod); + } + + // sort mods + foreach (IModMetadata mod in mods) + this.ProcessDependencies(mods.ToArray(), modDatabase, mod, states, sortedMods, new List()); + + return sortedMods.Reverse(); + } + + + /********* + ** Private methods + *********/ + /// Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. + /// The full list of mods being validated. + /// Handles access to SMAPI's internal mod metadata list. + /// The mod whose dependencies to process. + /// The dependency state for each mod. + /// The list in which to save mods sorted by dependency order. + /// The current change of mod dependencies. + /// Returns the mod dependency status. + private ModDependencyStatus ProcessDependencies(IModMetadata[] mods, ModDatabase modDatabase, IModMetadata mod, IDictionary states, Stack sortedMods, ICollection currentChain) + { + // check if already visited + switch (states[mod]) + { + // already sorted or failed + case ModDependencyStatus.Sorted: + case ModDependencyStatus.Failed: + return states[mod]; + + // dependency loop + case ModDependencyStatus.Checking: + // This should never happen. The higher-level mod checks if the dependency is + // already being checked, so it can fail without visiting a mod twice. If this + // case is hit, that logic didn't catch the dependency loop for some reason. + throw new InvalidModStateException($"A dependency loop was not caught by the calling iteration ({string.Join(" => ", currentChain.Select(p => p.DisplayName))} => {mod.DisplayName}))."); + + // not visited yet, start processing + case ModDependencyStatus.Queued: + break; + + // sanity check + default: + throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); + } + + // collect dependencies + ModDependency[] dependencies = this.GetDependenciesFrom(mod.Manifest, mods).ToArray(); + + // mark sorted if no dependencies + if (!dependencies.Any()) + { + sortedMods.Push(mod); + return states[mod] = ModDependencyStatus.Sorted; + } + + // mark failed if missing dependencies + { + string[] failedModNames = ( + from entry in dependencies + where entry.IsRequired && entry.Mod == null + let displayName = modDatabase.Get(entry.ID)?.DisplayName ?? entry.ID + let modUrl = modDatabase.GetModPageUrlFor(entry.ID) + orderby displayName + select modUrl != null + ? $"{displayName}: {modUrl}" + : displayName + ).ToArray(); + if (failedModNames.Any()) + { + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); + return states[mod] = ModDependencyStatus.Failed; + } + } + + // dependency min version not met, mark failed + { + 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)" + ) + .ToArray(); + if (failedLabels.Any()) + { + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); + return states[mod] = ModDependencyStatus.Failed; + } + } + + // process dependencies + { + states[mod] = ModDependencyStatus.Checking; + + // recursively sort dependencies + foreach (var dependency in dependencies) + { + IModMetadata requiredMod = dependency.Mod; + var subchain = new List(currentChain) { mod }; + + // ignore missing optional dependency + if (!dependency.IsRequired && requiredMod == null) + continue; + + // detect dependency loop + if (states[requiredMod] == ModDependencyStatus.Checking) + { + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); + return states[mod] = ModDependencyStatus.Failed; + } + + // recursively process each dependency + var substatus = this.ProcessDependencies(mods, modDatabase, requiredMod, states, sortedMods, subchain); + switch (substatus) + { + // sorted successfully + case ModDependencyStatus.Sorted: + case ModDependencyStatus.Failed when !dependency.IsRequired: // ignore failed optional dependency + break; + + // failed, which means this mod can't be loaded either + case ModDependencyStatus.Failed: + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); + return states[mod] = ModDependencyStatus.Failed; + + // 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."); + + // sanity check + default: + throw new InvalidModStateException($"Unknown dependency status '{states[mod]}'."); + } + } + + // all requirements sorted successfully + sortedMods.Push(mod); + return states[mod] = ModDependencyStatus.Sorted; + } + } + + /// Get all mod folders in a root folder, passing through empty folders as needed. + /// The root folder path to search. + private IEnumerable GetModFolders(string rootPath) + { + foreach (string modRootPath in Directory.GetDirectories(rootPath)) + { + DirectoryInfo directory = new DirectoryInfo(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; + } + } + + /// Get the dependencies declared in a manifest. + /// The mod manifest. + /// The loaded mods. + private IEnumerable GetDependenciesFrom(IManifest manifest, IModMetadata[] loadedMods) + { + 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); + } + + // yield content pack parent + if (manifest.ContentPackFor != null) + yield return new ModDependency(manifest.ContentPackFor.UniqueID, manifest.ContentPackFor.MinimumVersion, FindMod(manifest.ContentPackFor.UniqueID), isRequired: true); + } + + + /********* + ** Private models + *********/ + /// Represents a dependency from one mod to another. + private readonly struct ModDependency + { + /********* + ** Accessors + *********/ + /// The unique ID of the required mod. + public string ID { get; } + + /// The minimum required version (if any). + public ISemanticVersion MinVersion { get; } + + /// Whether the mod shouldn't be loaded if the dependency isn't available. + public bool IsRequired { get; } + + /// The loaded mod that fulfills the dependency (if available). + public IModMetadata Mod { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique ID of the required mod. + /// The minimum required version (if any). + /// The loaded mod that fulfills the dependency (if available). + /// Whether the mod shouldn't be loaded if the dependency isn't available. + public ModDependency(string id, ISemanticVersion minVersion, IModMetadata mod, bool isRequired) + { + this.ID = id; + this.MinVersion = minVersion; + this.Mod = mod; + this.IsRequired = isRequired; + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs new file mode 100644 index 00000000..dd74aad7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using StardewModdingAPI.Internal; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Metadata for mapping assemblies to the current . + internal class PlatformAssemblyMap : IDisposable + { + /********* + ** Accessors + *********/ + /**** + ** Data + ****/ + /// The target game platform. + public readonly Platform TargetPlatform; + + /// The short assembly names to remove as assembly reference, and replace with the . These should be short names (like "Stardew Valley"). + public readonly string[] RemoveNames; + + /**** + ** Metadata + ****/ + /// The assemblies to target. Equivalent types should be rewritten to use these assemblies. + public readonly Assembly[] Targets; + + /// An assembly => reference cache. + public readonly IDictionary TargetReferences; + + /// An assembly => module cache. + public readonly IDictionary TargetModules; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The target game platform. + /// The assembly short names to remove (like Stardew Valley). + /// The assemblies to target. + public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies) + { + // save data + this.TargetPlatform = targetPlatform; + this.RemoveNames = removeAssemblyNames; + + // cache assembly metadata + this.Targets = targetAssemblies; + this.TargetReferences = this.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); + this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(Path.Combine(Constants.ExecutionPath, assembly.Modules.Single().FullyQualifiedName), new ReaderParameters { InMemory = true })); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + foreach (ModuleDefinition module in this.TargetModules.Values) + module.Dispose(); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs b/src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs new file mode 100644 index 00000000..7c27cb03 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Provides helper methods for field rewriters. + internal static class RewriteHelper + { + /********* + ** Fields + *********/ + /// The comparer which heuristically compares type definitions. + private static readonly TypeReferenceComparer TypeDefinitionComparer = new TypeReferenceComparer(); + + + /********* + ** Public methods + *********/ + /// Get the field reference from an instruction if it matches. + /// The IL 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 + : null; + } + + /// Get the method reference from an instruction if it matches. + /// The IL instruction. + public static MethodReference AsMethodReference(Instruction instruction) + { + return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt || instruction.OpCode == OpCodes.Newobj + ? (MethodReference)instruction.Operand + : null; + } + + /// Get whether a type matches a type reference. + /// The defined type. + /// The type reference. + public static bool IsSameType(Type type, TypeReference reference) + { + // same namespace & name + if (type.Namespace != reference.Namespace || type.Name != reference.Name) + return false; + + // same generic parameters + if (type.IsGenericType) + { + if (!reference.IsGenericInstance) + return false; + + Type[] defGenerics = type.GetGenericArguments(); + TypeReference[] refGenerics = ((GenericInstanceType)reference).GenericArguments.ToArray(); + if (defGenerics.Length != refGenerics.Length) + return false; + for (int i = 0; i < defGenerics.Length; i++) + { + if (!RewriteHelper.IsSameType(defGenerics[i], refGenerics[i])) + return false; + } + } + + return true; + } + + /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. + /// The type ID to compare. + /// The other type ID to compare. + /// true if the type IDs look like the same type, false if not. + public static bool LooksLikeSameType(TypeReference typeA, TypeReference typeB) + { + return RewriteHelper.TypeDefinitionComparer.Equals(typeA, typeB); + } + + /// Get whether a method definition matches the signature expected by a method reference. + /// The method definition. + /// The method reference. + public static bool HasMatchingSignature(MethodBase definition, MethodReference reference) + { + // same name + if (definition.Name != reference.Name) + return false; + + // same arguments + ParameterInfo[] definitionParameters = definition.GetParameters(); + ParameterDefinition[] referenceParameters = reference.Parameters.ToArray(); + if (referenceParameters.Length != definitionParameters.Length) + return false; + for (int i = 0; i < referenceParameters.Length; i++) + { + if (!RewriteHelper.IsSameType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType)) + return false; + } + return true; + } + + /// Get whether a type has a method whose signature matches the one expected by a method reference. + /// The type to check. + /// The method reference. + public static bool HasMatchingSignature(Type type, MethodReference reference) + { + return type + .GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Public) + .Any(method => RewriteHelper.HasMatchingSignature(method, reference)) + || + type + .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) + .Any(method => RewriteHelper.HasMatchingSignature(method, reference)); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs new file mode 100644 index 00000000..ff86c6e2 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Finders; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites references to one field with another. + internal class FieldReplaceRewriter : FieldFinder + { + /********* + ** Fields + *********/ + /// The new field to reference. + private readonly FieldInfo ToField; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type whose field to which references should be rewritten. + /// The field name to rewrite. + /// The new field name to reference. + public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) + : base(type.FullName, fromFieldName, InstructionHandleResult.None) + { + this.ToField = type.GetField(toFieldName); + if (this.ToField == null) + throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field."); + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + FieldReference newRef = module.ImportReference(this.ToField); + cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); + return InstructionHandleResult.Rewritten; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs new file mode 100644 index 00000000..d65f7a5b --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -0,0 +1,58 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Finders; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites field references into property references. + internal class FieldToPropertyRewriter : FieldFinder + { + /********* + ** Fields + *********/ + /// The type whose field to which references should be rewritten. + private readonly Type Type; + + /// The property name. + private readonly string PropertyName; + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type whose field to which references should be rewritten. + /// The field name to rewrite. + /// The property name (if different). + public FieldToPropertyRewriter(Type type, string fieldName, string propertyName) + : base(type.FullName, fieldName, InstructionHandleResult.None) + { + this.Type = type; + this.PropertyName = propertyName; + } + + /// Construct an instance. + /// The type whose field to which references should be rewritten. + /// The field name to rewrite. + public FieldToPropertyRewriter(Type type, string fieldName) + : this(type, fieldName, fieldName) { } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; + MethodReference propertyRef = module.ImportReference(this.Type.GetMethod($"{methodPrefix}_{this.PropertyName}")); + cil.Replace(instruction, cil.Create(OpCodes.Call, propertyRef)); + + return InstructionHandleResult.Rewritten; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs new file mode 100644 index 00000000..6b8c2de1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -0,0 +1,88 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites method references from one parent type to another if the signatures match. + internal class MethodParentRewriter : IInstructionHandler + { + /********* + ** Fields + *********/ + /// The type whose methods to remap. + private readonly Type FromType; + + /// The type with methods to map to. + private readonly Type ToType; + + /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. + private readonly bool OnlyIfPlatformChanged; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type whose methods to remap. + /// The type with methods to map to. + /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. + public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false) + { + this.FromType = fromType; + this.ToType = toType; + this.NounPhrase = $"{fromType.Name} methods"; + this.OnlyIfPlatformChanged = onlyIfPlatformChanged; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction, platformChanged)) + return InstructionHandleResult.None; + + MethodReference methodRef = (MethodReference)instruction.Operand; + methodRef.DeclaringType = module.ImportReference(this.ToType); + return InstructionHandleResult.Rewritten; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + /// Whether the mod was compiled on a different platform. + protected bool IsMatch(Instruction instruction, bool platformChanged) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && (platformChanged || !this.OnlyIfPlatformChanged) + && methodRef.DeclaringType.FullName == this.FromType.FullName + && RewriteHelper.HasMatchingSignature(this.ToType, methodRef); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs new file mode 100644 index 00000000..7e7c0efa --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs @@ -0,0 +1,63 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Finders; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites static field references into constant values. + /// The constant value type. + internal class StaticFieldToConstantRewriter : FieldFinder + { + /********* + ** Fields + *********/ + /// The constant value to replace with. + private readonly TValue Value; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type whose field to which references should be rewritten. + /// The field name to rewrite. + /// The constant value to replace with. + public StaticFieldToConstantRewriter(Type type, string fieldName, TValue value) + : base(type.FullName, fieldName, InstructionHandleResult.None) + { + this.Value = value; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + cil.Replace(instruction, this.CreateConstantInstruction(cil, this.Value)); + return InstructionHandleResult.Rewritten; + } + + + /********* + ** Private methods + *********/ + /// Create a CIL constant value instruction. + /// The CIL processor. + /// The constant value to set. + private Instruction CreateConstantInstruction(ILProcessor cil, object value) + { + if (typeof(TValue) == typeof(int)) + return cil.Create(OpCodes.Ldc_I4, (int)value); + if (typeof(TValue) == typeof(string)) + return cil.Create(OpCodes.Ldstr, (string)value); + throw new NotSupportedException($"Rewriting to constant values of type {typeof(TValue)} isn't currently supported."); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeFieldToAnotherTypeFieldRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeFieldToAnotherTypeFieldRewriter.cs new file mode 100644 index 00000000..d4df22fb --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeFieldToAnotherTypeFieldRewriter.cs @@ -0,0 +1,77 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Finders; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + internal class TypeFieldToAnotherTypeFieldRewriter : FieldFinder + { + /********* + ** Fields + *********/ + /// The type whose field to which references should be rewritten. + private readonly Type Type; + + /// The type whose field to which references should be rewritten to. + private readonly Type ToType; + + /// The property name. + private readonly string PropertyName; + + private readonly IMonitor Monitor; + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type whose field to which references should be rewritten. + /// The field name to rewrite. + /// The property name (if different). + public TypeFieldToAnotherTypeFieldRewriter(Type type, Type toType, string fieldName, string propertyName) + : base(type.FullName, fieldName, InstructionHandleResult.None) + { + //this.Monitor = monitor; + this.Type = type; + this.ToType = toType; + this.PropertyName = propertyName; + } + + /// Construct an instance. + /// The type whose field to which references should be rewritten. + /// The field name to rewrite. + public TypeFieldToAnotherTypeFieldRewriter(Type type, Type toType, string fieldName, IMonitor monitor) + : this(type, toType, fieldName, fieldName) { } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + //Instruction: IL_0025: ldsfld StardewValley.GameLocation StardewValley.Game1::currentLocation + string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; + try + { + //MethodReference propertyRef = module.ImportReference(this.ToType.GetMethod($"{methodPrefix}_{this.PropertyName}")); + + MethodReference method = module.ImportReference(this.ToType.GetMethod($"{methodPrefix}_{this.PropertyName}")); + this.Monitor.Log("Method Ref: " + method.ToString()); + + cil.Replace(instruction, cil.Create(OpCodes.Call, method)); + } + catch (Exception e) + { + //this.Monitor.Log(e.Message); + } + + + return InstructionHandleResult.Rewritten; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs new file mode 100644 index 00000000..fade082b --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -0,0 +1,152 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Finders; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites all references to a type. + internal class TypeReferenceRewriter : TypeFinder + { + /********* + ** Fields + *********/ + /// The full type name to which to find references. + private readonly string FromTypeName; + + /// The new type to reference. + private readonly Type ToType; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name to which to find references. + /// The new type to reference. + /// A lambda which overrides a matched type. + public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldIgnore = null) + : base(fromTypeFullName, InstructionHandleResult.None, shouldIgnore) + { + this.FromTypeName = fromTypeFullName; + this.ToType = toType; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + bool rewritten = false; + + // return type + if (this.IsMatch(method.ReturnType)) + { + this.RewriteIfNeeded(module, method.ReturnType, newType => method.ReturnType = newType); + rewritten = true; + } + + // parameters + foreach (ParameterDefinition parameter in method.Parameters) + { + if (this.IsMatch(parameter.ParameterType)) + { + this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); + rewritten = true; + } + } + + // generic parameters + for (int i = 0; i < method.GenericParameters.Count; i++) + { + var parameter = method.GenericParameters[i]; + if (this.IsMatch(parameter)) + { + this.RewriteIfNeeded(module, parameter, newType => method.GenericParameters[i] = new GenericParameter(parameter.Name, newType)); + rewritten = true; + } + } + + // local variables + foreach (VariableDefinition variable in method.Body.Variables) + { + if (this.IsMatch(variable.VariableType)) + { + this.RewriteIfNeeded(module, variable.VariableType, newType => variable.VariableType = newType); + rewritten = true; + } + } + + return rewritten + ? InstructionHandleResult.Rewritten + : InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + // field reference + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null) + { + this.RewriteIfNeeded(module, fieldRef.DeclaringType, newType => fieldRef.DeclaringType = newType); + this.RewriteIfNeeded(module, fieldRef.FieldType, newType => fieldRef.FieldType = newType); + } + + // method reference + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef != null) + { + this.RewriteIfNeeded(module, methodRef.DeclaringType, newType => methodRef.DeclaringType = newType); + this.RewriteIfNeeded(module, methodRef.ReturnType, newType => methodRef.ReturnType = newType); + foreach (var parameter in methodRef.Parameters) + this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); + } + + // type reference + if (instruction.Operand is TypeReference typeRef) + this.RewriteIfNeeded(module, typeRef, newType => cil.Replace(instruction, cil.Create(instruction.OpCode, newType))); + + return InstructionHandleResult.Rewritten; + } + + /********* + ** Private methods + *********/ + /// Change a type reference if needed. + /// The assembly module containing the instruction. + /// The type to replace if it matches. + /// Assign the new type reference. + private void RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action set) + { + // current type + if (type.FullName == this.FromTypeName) + { + if (!this.ShouldIgnore(type)) + set(module.ImportReference(this.ToType)); + return; + } + + // recurse into generic arguments + if (type is GenericInstanceType genericType) + { + for (int i = 0; i < genericType.GenericArguments.Count; i++) + this.RewriteIfNeeded(module, genericType.GenericArguments[i], typeRef => genericType.GenericArguments[i] = typeRef); + } + + // recurse into generic parameters (e.g. constraints) + for (int i = 0; i < type.GenericParameters.Count; i++) + this.RewriteIfNeeded(module, type.GenericParameters[i], typeRef => type.GenericParameters[i] = new GenericParameter(typeRef)); + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/TypeReferenceComparer.cs b/src/StardewModdingAPI/Framework/ModLoading/TypeReferenceComparer.cs new file mode 100644 index 00000000..f7497789 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/TypeReferenceComparer.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; + +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Performs heuristic equality checks for instances. + /// + /// This implementation compares instances to see if they likely + /// refer to the same type. While the implementation is obvious for types like System.Bool, + /// this class mainly exists to handle cases like System.Collections.Generic.Dictionary`2<!0,Netcode.NetRoot`1<!1>> + /// and System.Collections.Generic.Dictionary`2<TKey,Netcode.NetRoot`1<TValue>> + /// which are compatible, but not directly comparable. It does this by splitting each type name + /// into its component token types, and performing placeholder substitution (e.g. !0 to + /// TKey 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. + /// + internal class TypeReferenceComparer : IEqualityComparer + { + /********* + ** Public methods + *********/ + /// Get whether the specified objects are equal. + /// The first object to compare. + /// The second object to compare. + public bool Equals(TypeReference a, TypeReference b) + { + if (a == null || b == null) + return a == b; + + return + a == b + || a.FullName == b.FullName + || this.HeuristicallyEquals(a, b); + } + + /// Get a hash code for the specified object. + /// The object for which a hash code is to be returned. + /// The object type is a reference type and is null. + public int GetHashCode(TypeReference obj) + { + return obj.GetHashCode(); + } + + + /********* + ** Private methods + *********/ + /// Get whether two types are heuristically equal based on generic type token substitution. + /// The first type to compare. + /// The second type to compare. + private bool HeuristicallyEquals(TypeReference typeA, TypeReference typeB) + { + bool HeuristicallyEquals(string typeNameA, string typeNameB, IDictionary tokenMap) + { + // analyse type names + bool hasTokensA = typeNameA.Contains("!"); + bool hasTokensB = typeNameB.Contains("!"); + bool isTokenA = hasTokensA && typeNameA[0] == '!'; + bool isTokenB = hasTokensB && typeNameB[0] == '!'; + + // validate + if (!hasTokensA && !hasTokensB) + return typeNameA == typeNameB; // no substitution needed + if (hasTokensA && hasTokensB) + throw new InvalidOperationException("Can't compare two type names when both contain generic type tokens."); + + // perform substitution if applicable + if (isTokenA) + typeNameA = this.MapPlaceholder(placeholder: typeNameA, type: typeNameB, map: tokenMap); + if (isTokenB) + typeNameB = this.MapPlaceholder(placeholder: typeNameB, type: typeNameA, map: tokenMap); + + // compare inner tokens + string[] symbolsA = this.GetTypeSymbols(typeNameA).ToArray(); + string[] symbolsB = this.GetTypeSymbols(typeNameB).ToArray(); + if (symbolsA.Length != symbolsB.Length) + return false; + + for (int i = 0; i < symbolsA.Length; i++) + { + if (!HeuristicallyEquals(symbolsA[i], symbolsB[i], tokenMap)) + return false; + } + + return true; + } + + return HeuristicallyEquals(typeA.FullName, typeB.FullName, new Dictionary()); + } + + /// Map a generic type placeholder (like !0) to its actual type. + /// The token placeholder. + /// The actual type. + /// The map of token to map substitutions. + /// Returns the previously-mapped type if applicable, else the . + private string MapPlaceholder(string placeholder, string type, IDictionary map) + { + if (map.TryGetValue(placeholder, out string result)) + return result; + + map[placeholder] = type; + return type; + } + + /// Get the top-level type symbols in a type name (e.g. List and NetRef<T> in List<NetRef<T>>) + /// The full type name. + private IEnumerable GetTypeSymbols(string typeName) + { + int openGenerics = 0; + + Queue queue = new Queue(typeName); + string symbol = ""; + while (queue.Any()) + { + char ch = queue.Dequeue(); + switch (ch) + { + // skip `1 generic type identifiers + case '`': + while (int.TryParse(queue.Peek().ToString(), out int _)) + queue.Dequeue(); + break; + + // start generic args + case '<': + switch (openGenerics) + { + // start new generic symbol + case 0: + yield return symbol; + symbol = ""; + openGenerics++; + break; + + // continue accumulating nested type symbol + default: + symbol += ch; + openGenerics++; + break; + } + break; + + // generic args delimiter + case ',': + switch (openGenerics) + { + // invalid + case 0: + throw new InvalidOperationException($"Encountered unexpected comma in type name: {typeName}."); + + // start next generic symbol + case 1: + yield return symbol; + symbol = ""; + break; + + // continue accumulating nested type symbol + default: + symbol += ch; + break; + } + break; + + + // end generic args + case '>': + switch (openGenerics) + { + // invalid + case 0: + throw new InvalidOperationException($"Encountered unexpected closing generic in type name: {typeName}."); + + // end generic symbol + case 1: + yield return symbol; + symbol = ""; + openGenerics--; + break; + + // continue accumulating nested type symbol + default: + symbol += ch; + openGenerics--; + break; + } + break; + + // continue symbol + default: + symbol += ch; + break; + } + } + + if (symbol != "") + yield return symbol; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs new file mode 100644 index 00000000..5be33cb4 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModRegistry.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace StardewModdingAPI.Framework +{ + /// Tracks the installed mods. + internal class ModRegistry + { + /********* + ** Fields + *********/ + /// The registered mod data. + private readonly List Mods = new List(); + + /// An assembly full name => mod lookup. + private readonly IDictionary ModNamesByAssembly = new Dictionary(); + + /// Whether all mod assemblies have been loaded. + public bool AreAllModsLoaded { get; set; } + + /// Whether all mods have been initialised and their method called. + public bool AreAllModsInitialised { get; set; } + + + /********* + ** Public methods + *********/ + /// Register a mod. + /// The mod metadata. + public void Add(IModMetadata metadata) + { + this.Mods.Add(metadata); + } + + /// Track a mod's assembly for use via . + /// The mod metadata. + /// The mod assembly. + public void TrackAssemblies(IModMetadata metadata, Assembly modAssembly) + { + this.ModNamesByAssembly[modAssembly.FullName] = metadata; + } + + /// Get metadata for all loaded mods. + /// Whether to include SMAPI mods. + /// Whether to include content pack mods. + public IEnumerable GetAll(bool assemblyMods = true, bool contentPacks = true) + { + IEnumerable query = this.Mods; + if (!assemblyMods) + query = query.Where(p => p.IsContentPack); + if (!contentPacks) + query = query.Where(p => !p.IsContentPack); + + return query; + } + + /// Get metadata for a loaded mod. + /// The mod's unique ID. + /// Returns the matching mod's metadata, or null if not found. + public IModMetadata Get(string uniqueID) + { + // normalise search ID + if (string.IsNullOrWhiteSpace(uniqueID)) + return null; + uniqueID = uniqueID.Trim(); + + // find match + return this.GetAll().FirstOrDefault(p => p.HasID(uniqueID)); + } + + /// Get the mod metadata from one of its assemblies. + /// The type to check. + /// Returns the mod name, or null if the type isn't part of a known mod. + public IModMetadata GetFrom(Type type) + { + // null + if (type == null) + return null; + + // known type + string assemblyName = type.Assembly.FullName; + if (this.ModNamesByAssembly.ContainsKey(assemblyName)) + return this.ModNamesByAssembly[assemblyName]; + + // not found + return null; + } + + /// Get the friendly name for the closest assembly registered as a source of deprecation warnings. + /// Returns the source name, or null if no registered assemblies were found. + public IModMetadata GetFromStack() + { + // get stack frames + StackTrace stack = new StackTrace(); + StackFrame[] frames = stack.GetFrames(); + if (frames == null) + return null; + + // search stack for a source assembly + foreach (StackFrame frame in frames) + { + MethodBase method = frame.GetMethod(); + IModMetadata mod = this.GetFrom(method.ReflectedType); + if (mod != null) + return mod; + } + + // no known assembly found + return null; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Models/ModFolderExport.cs b/src/StardewModdingAPI/Framework/Models/ModFolderExport.cs new file mode 100644 index 00000000..3b8d451a --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/ModFolderExport.cs @@ -0,0 +1,21 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// Metadata exported to the mod folder. + internal class ModFolderExport + { + /// When the export was generated. + public string Exported { get; set; } + + /// The absolute path of the mod folder. + public string ModFolderPath { get; set; } + + /// The game version which last loaded the mods. + public string GameVersion { get; set; } + + /// The SMAPI version which last loaded the mods. + public string ApiVersion { get; set; } + + /// The detected mods. + public IModMetadata[] Mods { get; set; } + } +} diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs new file mode 100644 index 00000000..e2b33160 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs @@ -0,0 +1,46 @@ +using StardewModdingAPI.Internal.ConsoleWriting; + +namespace StardewModdingAPI.Framework.Models +{ + /// The SMAPI configuration settings. + internal class SConfig + { + /******** + ** Accessors + ********/ + /// Whether to enable development features. + public bool DeveloperMode { get; set; } + + /// Whether to check for newer versions of SMAPI and mods on startup. + public bool CheckForUpdates { get; set; } + + /// 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. + public bool ParanoidWarnings { get; set; } = +#if DEBUG + true; +#else + false; +#endif + + /// Whether to show beta versions as valid updates. + public bool UseBetaChannel { get; set; } = Constants.ApiVersion.IsPrerelease(); + + /// SMAPI's GitHub project name, used to perform update checks. + public string GitHubProjectName { get; set; } + + /// The base URL for SMAPI's web API, used to perform update checks. + public string WebApiBaseUrl { get; set; } + + /// Whether SMAPI should log more information about the game context. + public bool VerboseLogging { get; set; } + + /// Whether to generate a file in the mods folder with detailed metadata about the detected mods. + public bool DumpMetadata { get; set; } + + /// The console color scheme to use. + public MonitorColorScheme ColorScheme { get; set; } + + /// The mod IDs SMAPI should ignore when performing update checks or validating update keys. + public string[] SuppressUpdateChecks { get; set; } + } +} diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs new file mode 100644 index 00000000..47ebc2d7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -0,0 +1,163 @@ +using System; +using System.Linq; +using System.Threading; +using StardewModdingAPI.Framework.Logging; +using StardewModdingAPI.Internal.ConsoleWriting; + +namespace StardewModdingAPI.Framework +{ + /// Encapsulates monitoring and logic for a given module. + internal class Monitor : IMonitor + { + /********* + ** Fields + *********/ + /// The name of the module which logs messages using this instance. + private readonly string Source; + + /// Handles writing color-coded text to the console. + private readonly ColorfulConsoleWriter ConsoleWriter; + + /// Manages access to the console output. + private readonly ConsoleInterceptionManager ConsoleInterceptor; + + /// The log file to which to write messages. + private readonly LogFileManager LogFile; + + /// The maximum length of the values. + private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); + + /// Propagates notification that SMAPI should exit. + private readonly CancellationTokenSource ExitTokenSource; + + + /********* + ** Accessors + *********/ + /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. + public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; + + /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + public bool IsVerbose { get; } + + /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. + internal bool ShowFullStampInConsole { get; set; } + + /// Whether to show trace messages in the console. + internal bool ShowTraceInConsole { get; set; } + + /// Whether to write anything to the console. This should be disabled if no console is available. + internal bool WriteToConsole { get; set; } = true; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The name of the module which logs messages using this instance. + /// Intercepts access to the console output. + /// The log file to which to write messages. + /// Propagates notification that SMAPI should exit. + /// The console color scheme to use. + /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme, bool isVerbose) + { + // validate + if (string.IsNullOrWhiteSpace(source)) + throw new ArgumentException("The log source cannot be empty."); + + // initialise + this.Source = source; + this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); + this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorScheme); + this.ConsoleInterceptor = consoleInterceptor; + this.ExitTokenSource = exitTokenSource; + this.IsVerbose = isVerbose; + } + + /// Log a message for the player or developer. + /// The message to log. + /// The log severity level. + public void Log(string message, LogLevel level = LogLevel.Debug) + { + this.LogImpl(this.Source, message, (ConsoleLogLevel)level); + } + + /// Log a message that only appears when is enabled. + /// The message to log. + public void VerboseLog(string message) + { + if (this.IsVerbose) + this.Log(message, LogLevel.Trace); + } + + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. + /// The reason for the shutdown. + public void ExitGameImmediately(string reason) + { + this.LogFatal($"{this.Source} requested an immediate game shutdown: {reason}"); + this.ExitTokenSource.Cancel(); + } + + /// Write a newline to the console and log file. + internal void Newline() + { + if (this.WriteToConsole) + this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(Console.WriteLine); + this.LogFile.WriteLine(""); + } + + /// Log console input from the user. + /// The user input to log. + internal void LogUserInput(string input) + { + // user input already appears in the console, so just need to write to file + string prefix = this.GenerateMessagePrefix(this.Source, (ConsoleLogLevel)LogLevel.Info); + this.LogFile.WriteLine($"{prefix} $>{input}"); + } + + + /********* + ** Private methods + *********/ + /// Log a fatal error message. + /// The message to log. + private void LogFatal(string message) + { + this.LogImpl(this.Source, message, ConsoleLogLevel.Critical); + } + + /// Write a message line to the log. + /// The name of the mod logging the message. + /// The message to log. + /// The log level. + private void LogImpl(string source, string message, ConsoleLogLevel level) + { + // generate message + string prefix = this.GenerateMessagePrefix(source, level); + string fullMessage = $"{prefix} {message}"; + string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; + + // write to console + if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace)) + { + this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(() => + { + this.ConsoleWriter.WriteLine(consoleMessage, level); + }); + } + + // write to log file + this.LogFile.WriteLine(fullMessage); + } + + /// Generate a message prefix for the current time. + /// The name of the mod logging the message. + /// The log level. + private string GenerateMessagePrefix(string source, ConsoleLogLevel level) + { + string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); + return $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}]"; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Networking/MessageType.cs b/src/StardewModdingAPI/Framework/Networking/MessageType.cs new file mode 100644 index 00000000..bd9acfa9 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Networking/MessageType.cs @@ -0,0 +1,26 @@ +using StardewValley; + +namespace StardewModdingAPI.Framework.Networking +{ + /// Network message types recognised by SMAPI and Stardew Valley. + internal enum MessageType : byte + { + /********* + ** SMAPI + *********/ + /// A data message intended for mods to consume. + ModMessage = 254, + + /// Metadata context about a player synced by SMAPI. + ModContext = 255, + + /********* + ** Vanilla + *********/ + /// Metadata about the host server sent to a farmhand. + ServerIntroduction = Multiplayer.serverIntroduction, + + /// Metadata about a player sent to a farmhand or server. + PlayerIntroduction = Multiplayer.playerIntroduction + } +} diff --git a/src/StardewModdingAPI/Framework/Networking/ModMessageModel.cs b/src/StardewModdingAPI/Framework/Networking/ModMessageModel.cs new file mode 100644 index 00000000..7ee39863 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Networking/ModMessageModel.cs @@ -0,0 +1,72 @@ +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Framework.Networking +{ + /// The metadata for a mod message. + internal class ModMessageModel + { + /********* + ** Accessors + *********/ + /**** + ** Origin + ****/ + /// The unique ID of the player who broadcast the message. + public long FromPlayerID { get; set; } + + /// The unique ID of the mod which broadcast the message. + public string FromModID { get; set; } + + /**** + ** Destination + ****/ + /// The players who should receive the message, or null for all players. + public long[] ToPlayerIDs { get; set; } + + /// The mods which should receive the message, or null for all mods. + public string[] ToModIDs { get; set; } + + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + public string Type { get; set; } + + /// The custom mod data being broadcast. + public JToken Data { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModMessageModel() { } + + /// Construct an instance. + /// The unique ID of the player who broadcast the message. + /// The unique ID of the mod which broadcast the message. + /// The players who should receive the message, or null for all players. + /// The mods which should receive the message, or null for all mods. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The custom mod data being broadcast. + public ModMessageModel(long fromPlayerID, string fromModID, long[] toPlayerIDs, string[] toModIDs, string type, JToken data) + { + this.FromPlayerID = fromPlayerID; + this.FromModID = fromModID; + this.ToPlayerIDs = toPlayerIDs; + this.ToModIDs = toModIDs; + this.Type = type; + this.Data = data; + } + + /// Construct an instance. + /// The message to clone. + public ModMessageModel(ModMessageModel message) + { + this.FromPlayerID = message.FromPlayerID; + this.FromModID = message.FromModID; + this.ToPlayerIDs = message.ToPlayerIDs?.ToArray(); + this.ToModIDs = message.ToModIDs?.ToArray(); + this.Type = message.Type; + this.Data = message.Data; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Networking/MultiplayerPeer.cs b/src/StardewModdingAPI/Framework/Networking/MultiplayerPeer.cs new file mode 100644 index 00000000..b4e39379 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Networking/MultiplayerPeer.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewValley.Network; + +namespace StardewModdingAPI.Framework.Networking +{ + /// Metadata about a connected player. + internal class MultiplayerPeer : IMultiplayerPeer + { + /********* + ** Fields + *********/ + /// A method which sends a message to the peer. + private readonly Action SendMessageImpl; + + + /********* + ** Accessors + *********/ + /// The player's unique ID. + public long PlayerID { get; } + + /// Whether this is a connection to the host player. + public bool IsHost { get; } + + /// Whether the player has SMAPI installed. + public bool HasSmapi => this.ApiVersion != null; + + /// The player's OS platform, if is true. + public GamePlatform? Platform { get; } + + /// The installed version of Stardew Valley, if is true. + public ISemanticVersion GameVersion { get; } + + /// The installed version of SMAPI, if is true. + public ISemanticVersion ApiVersion { get; } + + /// The installed mods, if is true. + public IEnumerable Mods { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player's unique ID. + /// The metadata to copy. + /// A method which sends a message to the peer. + /// Whether this is a connection to the host player. + public MultiplayerPeer(long playerID, RemoteContextModel model, Action sendMessage, bool isHost) + { + this.PlayerID = playerID; + this.IsHost = isHost; + if (model != null) + { + this.Platform = model.Platform; + this.GameVersion = model.GameVersion; + this.ApiVersion = model.ApiVersion; + this.Mods = model.Mods.Select(mod => new MultiplayerPeerMod(mod)).ToArray(); + } + this.SendMessageImpl = sendMessage; + } + + /// Get metadata for a mod installed by the player. + /// The unique mod ID. + /// Returns the mod info, or null if the player doesn't have that mod. + public IMultiplayerPeerMod GetMod(string id) + { + if (string.IsNullOrWhiteSpace(id) || this.Mods == null || !this.Mods.Any()) + return null; + + id = id.Trim(); + return this.Mods.FirstOrDefault(mod => mod.ID != null && mod.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)); + } + + /// Send a message to the given peer, bypassing the game's normal validation to allow messages before the connection is approved. + /// The message to send. + public void SendMessage(OutgoingMessage message) + { + this.SendMessageImpl(message); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Networking/MultiplayerPeerMod.cs b/src/StardewModdingAPI/Framework/Networking/MultiplayerPeerMod.cs new file mode 100644 index 00000000..1b324bcd --- /dev/null +++ b/src/StardewModdingAPI/Framework/Networking/MultiplayerPeerMod.cs @@ -0,0 +1,30 @@ +namespace StardewModdingAPI.Framework.Networking +{ + internal class MultiplayerPeerMod : IMultiplayerPeerMod + { + /********* + ** Accessors + *********/ + /// The mod's display name. + public string Name { get; } + + /// The unique mod ID. + public string ID { get; } + + /// The mod version. + public ISemanticVersion Version { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod metadata. + public MultiplayerPeerMod(RemoteContextModModel mod) + { + this.Name = mod.Name; + this.ID = mod.ID?.Trim(); + this.Version = mod.Version; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Networking/RemoteContextModModel.cs b/src/StardewModdingAPI/Framework/Networking/RemoteContextModModel.cs new file mode 100644 index 00000000..9795d971 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Networking/RemoteContextModModel.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.Networking +{ + /// Metadata about an installed mod exchanged with connected computers. + public class RemoteContextModModel + { + /// The mod's display name. + public string Name { get; set; } + + /// The unique mod ID. + public string ID { get; set; } + + /// The mod version. + public ISemanticVersion Version { get; set; } + } +} diff --git a/src/StardewModdingAPI/Framework/Networking/RemoteContextModel.cs b/src/StardewModdingAPI/Framework/Networking/RemoteContextModel.cs new file mode 100644 index 00000000..7befb151 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Networking/RemoteContextModel.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Framework.Networking +{ + /// Metadata about the game, SMAPI, and installed mods exchanged with connected computers. + internal class RemoteContextModel + { + /********* + ** Accessors + *********/ + /// Whether this player is the host player. + public bool IsHost { get; set; } + + /// The game's platform version. + public GamePlatform Platform { get; set; } + + /// The installed version of Stardew Valley. + public ISemanticVersion GameVersion { get; set; } + + /// The installed version of SMAPI. + public ISemanticVersion ApiVersion { get; set; } + + /// The installed mods. + public RemoteContextModModel[] Mods { get; set; } + } +} diff --git a/src/StardewModdingAPI/Framework/Networking/SGalaxyNetClient.cs b/src/StardewModdingAPI/Framework/Networking/SGalaxyNetClient.cs new file mode 100644 index 00000000..01095c66 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Networking/SGalaxyNetClient.cs @@ -0,0 +1,52 @@ +using System; +using Galaxy.Api; +using StardewValley.Network; +using StardewValley.SDKs; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer client used to connect to a hosted server. This is an implementation of with callbacks for SMAPI functionality. + internal class SGalaxyNetClient : GalaxyNetClient + { + /********* + ** Fields + *********/ + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + private readonly Action, Action> OnSendingMessage; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The remote address being connected. + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + public SGalaxyNetClient(GalaxyID address, Action, Action> onProcessingMessage, Action, Action> onSendingMessage) + : base(address) + { + this.OnProcessingMessage = onProcessingMessage; + this.OnSendingMessage = onSendingMessage; + } + + /// Send a message to the connected peer. + public override void sendMessage(OutgoingMessage message) + { + this.OnSendingMessage(message, base.sendMessage, () => base.sendMessage(message)); + } + + + /********* + ** Protected methods + *********/ + /// Process an incoming network message. + /// The message to process. + protected override void processIncomingMessage(IncomingMessage message) + { + this.OnProcessingMessage(message, base.sendMessage, () => base.processIncomingMessage(message)); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Networking/SGalaxyNetServer.cs b/src/StardewModdingAPI/Framework/Networking/SGalaxyNetServer.cs new file mode 100644 index 00000000..bb67f70e --- /dev/null +++ b/src/StardewModdingAPI/Framework/Networking/SGalaxyNetServer.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Galaxy.Api; +using StardewValley.Network; +using StardewValley.SDKs; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer server used to connect to an incoming player. This is an implementation of that adds support for SMAPI's metadata context exchange. + internal class SGalaxyNetServer : GalaxyNetServer + { + /********* + ** Fields + *********/ + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// SMAPI's implementation of the game's core multiplayer logic. + private readonly SMultiplayer Multiplayer; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying game server. + /// SMAPI's implementation of the game's core multiplayer logic. + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + public SGalaxyNetServer(IGameServer gameServer, SMultiplayer multiplayer, Action, Action> onProcessingMessage) + : base(gameServer) + { + this.Multiplayer = multiplayer; + this.OnProcessingMessage = onProcessingMessage; + } + + + /********* + ** Protected methods + *********/ + /// Read and process a message from the client. + /// The Galaxy peer ID. + /// The data to process. + /// This reimplements , but adds a callback to . + [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] + protected override void onReceiveMessage(GalaxyID peer, Stream messageStream) + { + using (IncomingMessage message = new IncomingMessage()) + using (BinaryReader reader = new BinaryReader(messageStream)) + { + message.Read(reader); + ulong peerID = peer.ToUint64(); // note: GalaxyID instances get reused, so need to store the underlying ID instead + this.OnProcessingMessage(message, outgoing => this.SendMessageToPeerID(peerID, outgoing), () => + { + if (this.peers.ContainsLeft(message.FarmerID) && (long)this.peers[message.FarmerID] == (long)peerID) + this.gameServer.processIncomingMessage(message); + else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); + GalaxyID capturedPeer = new GalaxyID(peerID); + this.gameServer.checkFarmhandRequest(Convert.ToString(peerID), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); + } + }); + } + } + + /// Send a message to a remote peer. + /// The unique Galaxy ID, derived from . + /// The message to send. + private void SendMessageToPeerID(ulong peerID, OutgoingMessage message) + { + this.sendMessage(new GalaxyID(peerID), message); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Networking/SLidgrenClient.cs b/src/StardewModdingAPI/Framework/Networking/SLidgrenClient.cs new file mode 100644 index 00000000..39876744 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Networking/SLidgrenClient.cs @@ -0,0 +1,50 @@ +using System; +using StardewValley.Network; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer client used to connect to a hosted server. This is an implementation of with callbacks for SMAPI functionality. + internal class SLidgrenClient : LidgrenClient + { + /********* + ** Fields + *********/ + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + private readonly Action, Action> OnSendingMessage; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The remote address being connected. + /// A callback to raise when receiving a message. This receives the incoming message, a method to send an arbitrary message, and a callback to run the default logic. + /// A callback to raise when sending a message. This receives the outgoing message, a method to send an arbitrary message, and a callback to resume the default logic. + public SLidgrenClient(string address, Action, Action> onProcessingMessage, Action, Action> onSendingMessage) + : base(address) + { + this.OnProcessingMessage = onProcessingMessage; + this.OnSendingMessage = onSendingMessage; + } + + /// Send a message to the connected peer. + public override void sendMessage(OutgoingMessage message) + { + this.OnSendingMessage(message, base.sendMessage, () => base.sendMessage(message)); + } + + + /********* + ** Protected methods + *********/ + /// Process an incoming network message. + /// The message to process. + protected override void processIncomingMessage(IncomingMessage message) + { + this.OnProcessingMessage(message, base.sendMessage, () => base.processIncomingMessage(message)); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Networking/SLidgrenServer.cs b/src/StardewModdingAPI/Framework/Networking/SLidgrenServer.cs new file mode 100644 index 00000000..1bce47fe --- /dev/null +++ b/src/StardewModdingAPI/Framework/Networking/SLidgrenServer.cs @@ -0,0 +1,65 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Lidgren.Network; +using StardewValley.Network; + +namespace StardewModdingAPI.Framework.Networking +{ + /// A multiplayer server used to connect to an incoming player. This is an implementation of that adds support for SMAPI's metadata context exchange. + internal class SLidgrenServer : LidgrenServer + { + /********* + ** Fields + *********/ + /// SMAPI's implementation of the game's core multiplayer logic. + private readonly SMultiplayer Multiplayer; + + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + private readonly Action, Action> OnProcessingMessage; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// SMAPI's implementation of the game's core multiplayer logic. + /// The underlying game server. + /// A callback to raise when receiving a message. This receives the incoming message, a method to send a message, and a callback to run the default logic. + public SLidgrenServer(IGameServer gameServer, SMultiplayer multiplayer, Action, Action> onProcessingMessage) + : base(gameServer) + { + this.Multiplayer = multiplayer; + this.OnProcessingMessage = onProcessingMessage; + } + + /// Parse a data message from a client. + /// The raw network message to parse. + [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] + protected override void parseDataMessageFromClient(NetIncomingMessage rawMessage) + { + // add hook to call multiplayer core + NetConnection peer = rawMessage.SenderConnection; + using (IncomingMessage message = new IncomingMessage()) + using (Stream readStream = new NetBufferReadStream(rawMessage)) + using (BinaryReader reader = new BinaryReader(readStream)) + { + while (rawMessage.LengthBits - rawMessage.Position >= 8) + { + message.Read(reader); + NetConnection connection = rawMessage.SenderConnection; // don't pass rawMessage into context because it gets reused + this.OnProcessingMessage(message, outgoing => this.sendMessage(connection, outgoing), () => + { + if (this.peers.ContainsLeft(message.FarmerID) && this.peers[message.FarmerID] == peer) + this.gameServer.processIncomingMessage(message); + else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) + { + NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); + this.gameServer.checkFarmhandRequest("", farmer, msg => this.sendMessage(peer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = peer); + } + }); + } + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Patching/GamePatcher.cs b/src/StardewModdingAPI/Framework/Patching/GamePatcher.cs new file mode 100644 index 00000000..2ba485d1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Patching/GamePatcher.cs @@ -0,0 +1,48 @@ +using System; +using Harmony; +using MonoMod.RuntimeDetour; + +namespace StardewModdingAPI.Framework.Patching +{ + /// Encapsulates applying Harmony patches to the game. + internal class GamePatcher + { + /********* + ** Fields + *********/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging. + public GamePatcher(IMonitor monitor) + { + this.Monitor = monitor; + } + + /// Apply all loaded patches to the game. + /// The patches to apply. + public void Apply(params IHarmonyPatch[] patches) + { + HarmonyDetourBridge.Init(); + + HarmonyInstance harmony = HarmonyInstance.Create("io.smapi"); + foreach (IHarmonyPatch patch in patches) + { + try + { + patch.Apply(harmony); + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't apply runtime patch '{patch.Name}' to the game. Some SMAPI features may not work correctly. See log file for details.", LogLevel.Error); + this.Monitor.Log(ex.GetLogSummary(), LogLevel.Trace); + } + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Patching/IHarmonyPatch.cs b/src/StardewModdingAPI/Framework/Patching/IHarmonyPatch.cs new file mode 100644 index 00000000..cb42f40e --- /dev/null +++ b/src/StardewModdingAPI/Framework/Patching/IHarmonyPatch.cs @@ -0,0 +1,15 @@ +using Harmony; + +namespace StardewModdingAPI.Framework.Patching +{ + /// A Harmony patch to apply. + internal interface IHarmonyPatch + { + /// A unique name for this patch. + string Name { get; } + + /// Apply the Harmony patch. + /// The Harmony instance. + void Apply(HarmonyInstance harmony); + } +} diff --git a/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs b/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs new file mode 100644 index 00000000..912662e3 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs @@ -0,0 +1,30 @@ +using System.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// A cached member reflection result. + internal readonly struct CacheEntry + { + /********* + ** Accessors + *********/ + /// Whether the lookup found a valid match. + public bool IsValid { get; } + + /// The reflection data for this member (or null if invalid). + public MemberInfo MemberInfo { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Whether the lookup found a valid match. + /// The reflection data for this member (or null if invalid). + public CacheEntry(bool isValid, MemberInfo memberInfo) + { + this.IsValid = isValid; + this.MemberInfo = memberInfo; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/StardewModdingAPI/Framework/Reflection/InterfaceProxyBuilder.cs new file mode 100644 index 00000000..70ef81f8 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/InterfaceProxyBuilder.cs @@ -0,0 +1,118 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// Generates a proxy class to access a mod API through an arbitrary interface. + internal class InterfaceProxyBuilder + { + /********* + ** Fields + *********/ + /// The target class type. + private readonly Type TargetType; + + /// The generated proxy type. + private readonly Type ProxyType; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type name to generate. + /// The CLR module in which to create proxy classes. + /// The interface type to implement. + /// The target type. + public InterfaceProxyBuilder(string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType) + { + // validate + if (name == null) + throw new ArgumentNullException(nameof(name)); + if (targetType == null) + throw new ArgumentNullException(nameof(targetType)); + + // define proxy type + TypeBuilder proxyBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class); + proxyBuilder.AddInterfaceImplementation(interfaceType); + + // create field to store target instance + FieldBuilder targetField = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private); + + // create constructor which accepts target instance and sets field + { + ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType }); + ILGenerator il = constructor.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); // this + // ReSharper disable once AssignNullToNotNullAttribute -- never null + il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); // call base constructor + il.Emit(OpCodes.Ldarg_0); // this + il.Emit(OpCodes.Ldarg_1); // load argument + il.Emit(OpCodes.Stfld, targetField); // set field to loaded argument + il.Emit(OpCodes.Ret); + } + + // proxy methods + foreach (MethodInfo proxyMethod in interfaceType.GetMethods()) + { + var targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray()); + if (targetMethod == null) + throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API."); + + this.ProxyMethod(proxyBuilder, targetMethod, targetField); + } + + // save info + this.TargetType = targetType; + this.ProxyType = proxyBuilder.CreateType(); + } + + /// Create an instance of the proxy for a target instance. + /// The target instance. + public object CreateInstance(object targetInstance) + { + ConstructorInfo constructor = this.ProxyType.GetConstructor(new[] { this.TargetType }); + if (constructor == null) + throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{this.ProxyType.Name}'."); // should never happen + return constructor.Invoke(new[] { targetInstance }); + } + + + /********* + ** Private methods + *********/ + /// Define a method which proxies access to a method on the target. + /// The proxy type being generated. + /// The target method. + /// The proxy field containing the API instance. + private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField) + { + Type[] argTypes = target.GetParameters().Select(a => a.ParameterType).ToArray(); + + // create method + MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual); + methodBuilder.SetParameters(argTypes); + methodBuilder.SetReturnType(target.ReturnType); + + // create method body + { + ILGenerator il = methodBuilder.GetILGenerator(); + + // load target instance + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, instanceField); + + // invoke target method on instance + for (int i = 0; i < argTypes.Length; i++) + il.Emit(OpCodes.Ldarg, i + 1); + il.Emit(OpCodes.Call, target); + + // return result + il.Emit(OpCodes.Ret); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/StardewModdingAPI/Framework/Reflection/InterfaceProxyFactory.cs new file mode 100644 index 00000000..464367b6 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/InterfaceProxyFactory.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// Generates proxy classes to access mod APIs through an arbitrary interface. + internal class InterfaceProxyFactory + { + /********* + ** Fields + *********/ + /// The CLR module in which to create proxy classes. + private readonly ModuleBuilder ModuleBuilder; + + /// The generated proxy types. + private readonly IDictionary Builders = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public InterfaceProxyFactory() + { + AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run); + this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies"); + } + + /// Create an API proxy. + /// The interface through which to access the API. + /// The API instance to access. + /// The unique ID of the mod consuming the API. + /// The unique ID of the mod providing the API. + public TInterface CreateProxy(object instance, string sourceModID, string targetModID) + where TInterface : class + { + // validate + if (instance == null) + throw new InvalidOperationException("Can't proxy access to a null API."); + if (!typeof(TInterface).IsInterface) + throw new InvalidOperationException("The proxy type must be an interface, not a class."); + + // get proxy type + Type targetType = instance.GetType(); + string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>"; + if (!this.Builders.TryGetValue(proxyTypeName, out InterfaceProxyBuilder builder)) + { + builder = new InterfaceProxyBuilder(proxyTypeName, this.ModuleBuilder, typeof(TInterface), targetType); + this.Builders[proxyTypeName] = builder; + } + + // create instance + return (TInterface)builder.CreateInstance(instance); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Reflection/ReflectedField.cs b/src/StardewModdingAPI/Framework/Reflection/ReflectedField.cs new file mode 100644 index 00000000..d771422c --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/ReflectedField.cs @@ -0,0 +1,93 @@ +using System; +using System.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// A field obtained through reflection. + /// The field value type. + internal class ReflectedField : IReflectedField + { + /********* + ** Fields + *********/ + /// The type that has the field. + private readonly Type ParentType; + + /// The object that has the instance field (if applicable). + private readonly object Parent; + + /// The display name shown in error messages. + private string DisplayName => $"{this.ParentType.FullName}::{this.FieldInfo.Name}"; + + + /********* + ** Accessors + *********/ + /// The reflection metadata. + public FieldInfo FieldInfo { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type that has the field. + /// The object that has the instance field (if applicable). + /// The reflection metadata. + /// Whether the field is static. + /// The or is null. + /// The is null for a non-static field, or not null for a static field. + public ReflectedField(Type parentType, object obj, FieldInfo field, bool isStatic) + { + // validate + if (parentType == null) + throw new ArgumentNullException(nameof(parentType)); + if (field == null) + throw new ArgumentNullException(nameof(field)); + if (isStatic && obj != null) + throw new ArgumentException("A static field cannot have an object instance."); + if (!isStatic && obj == null) + throw new ArgumentException("A non-static field must have an object instance."); + + // save + this.ParentType = parentType; + this.Parent = obj; + this.FieldInfo = field; + } + + /// Get the field value. + public TValue GetValue() + { + try + { + return (TValue)this.FieldInfo.GetValue(this.Parent); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't convert the {this.DisplayName} field from {this.FieldInfo.FieldType.FullName} to {typeof(TValue).FullName}."); + } + catch (Exception ex) + { + throw new Exception($"Couldn't get the value of the {this.DisplayName} field", ex); + } + } + + /// Set the field value. + //// The value to set. + public void SetValue(TValue value) + { + try + { + this.FieldInfo.SetValue(this.Parent, value); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't assign the {this.DisplayName} field a {typeof(TValue).FullName} value, must be compatible with {this.FieldInfo.FieldType.FullName}."); + } + catch (Exception ex) + { + throw new Exception($"Couldn't set the value of the {this.DisplayName} field", ex); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Reflection/ReflectedMethod.cs b/src/StardewModdingAPI/Framework/Reflection/ReflectedMethod.cs new file mode 100644 index 00000000..039f27c3 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/ReflectedMethod.cs @@ -0,0 +1,99 @@ +using System; +using System.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// A method obtained through reflection. + internal class ReflectedMethod : IReflectedMethod + { + /********* + ** Fields + *********/ + /// The type that has the method. + private readonly Type ParentType; + + /// The object that has the instance method (if applicable). + private readonly object Parent; + + /// The display name shown in error messages. + private string DisplayName => $"{this.ParentType.FullName}::{this.MethodInfo.Name}"; + + + /********* + ** Accessors + *********/ + /// The reflection metadata. + public MethodInfo MethodInfo { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type that has the method. + /// The object that has the instance method(if applicable). + /// The reflection metadata. + /// Whether the method is static. + /// The or is null. + /// The is null for a non-static method, or not null for a static method. + public ReflectedMethod(Type parentType, object obj, MethodInfo method, bool isStatic) + { + // validate + if (parentType == null) + throw new ArgumentNullException(nameof(parentType)); + if (method == null) + throw new ArgumentNullException(nameof(method)); + if (isStatic && obj != null) + throw new ArgumentException("A static method cannot have an object instance."); + if (!isStatic && obj == null) + throw new ArgumentException("A non-static method must have an object instance."); + + // save + this.ParentType = parentType; + this.Parent = obj; + this.MethodInfo = method; + } + + /// Invoke the method. + /// The return type. + /// The method arguments to pass in. + public TValue Invoke(params object[] arguments) + { + // invoke method + object result; + try + { + result = this.MethodInfo.Invoke(this.Parent, arguments); + } + catch (Exception ex) + { + throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex); + } + + // cast return value + try + { + return (TValue)result; + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't convert the return value of the {this.DisplayName} method from {this.MethodInfo.ReturnType.FullName} to {typeof(TValue).FullName}."); + } + } + + /// Invoke the method. + /// The method arguments to pass in. + public void Invoke(params object[] arguments) + { + // invoke method + try + { + this.MethodInfo.Invoke(this.Parent, arguments); + } + catch (Exception ex) + { + throw new Exception($"Couldn't invoke the {this.DisplayName} method", ex); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Reflection/ReflectedProperty.cs b/src/StardewModdingAPI/Framework/Reflection/ReflectedProperty.cs new file mode 100644 index 00000000..8a10ff9a --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/ReflectedProperty.cs @@ -0,0 +1,105 @@ +using System; +using System.Reflection; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// A property obtained through reflection. + /// The property value type. + internal class ReflectedProperty : IReflectedProperty + { + /********* + ** Fields + *********/ + /// The display name shown in error messages. + private readonly string DisplayName; + + /// The underlying property getter. + private readonly Func GetMethod; + + /// The underlying property setter. + private readonly Action SetMethod; + + + /********* + ** Accessors + *********/ + /// The reflection metadata. + public PropertyInfo PropertyInfo { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The type that has the property. + /// The object that has the instance property (if applicable). + /// The reflection metadata. + /// Whether the property is static. + /// The or is null. + /// The is null for a non-static property, or not null for a static property. + public ReflectedProperty(Type parentType, object obj, PropertyInfo property, bool isStatic) + { + // validate input + if (parentType == null) + throw new ArgumentNullException(nameof(parentType)); + if (property == null) + throw new ArgumentNullException(nameof(property)); + + // validate static + if (isStatic && obj != null) + throw new ArgumentException("A static property cannot have an object instance."); + if (!isStatic && obj == null) + throw new ArgumentException("A non-static property must have an object instance."); + + + this.DisplayName = $"{parentType.FullName}::{property.Name}"; + this.PropertyInfo = property; + + if (this.PropertyInfo.GetMethod != null) + this.GetMethod = (Func)Delegate.CreateDelegate(typeof(Func), obj, this.PropertyInfo.GetMethod); + if (this.PropertyInfo.SetMethod != null) + this.SetMethod = (Action)Delegate.CreateDelegate(typeof(Action), obj, this.PropertyInfo.SetMethod); + } + + /// Get the property value. + public TValue GetValue() + { + if (this.GetMethod == null) + throw new InvalidOperationException($"The {this.DisplayName} property has no get method."); + + try + { + return this.GetMethod(); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't convert the {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}."); + } + catch (Exception ex) + { + throw new Exception($"Couldn't get the value of the {this.DisplayName} property", ex); + } + } + + /// Set the property value. + //// The value to set. + public void SetValue(TValue value) + { + if (this.SetMethod == null) + throw new InvalidOperationException($"The {this.DisplayName} property has no set method."); + + try + { + this.SetMethod(value); + } + catch (InvalidCastException) + { + throw new InvalidCastException($"Can't assign the {this.DisplayName} property a {typeof(TValue).FullName} value, must be compatible with {this.PropertyInfo.PropertyType.FullName}."); + } + catch (Exception ex) + { + throw new Exception($"Couldn't set the value of the {this.DisplayName} property", ex); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Reflection/Reflector.cs b/src/StardewModdingAPI/Framework/Reflection/Reflector.cs new file mode 100644 index 00000000..6747543d --- /dev/null +++ b/src/StardewModdingAPI/Framework/Reflection/Reflector.cs @@ -0,0 +1,276 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Reflection +{ + /// Provides helper methods for accessing inaccessible code. + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + internal class Reflector + { + /********* + ** Fields + *********/ + /// The cached fields and methods found via reflection. + private readonly Dictionary Cache = new Dictionary(); + + /// The sliding cache expiration time. + private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); + + + /********* + ** Public methods + *********/ + /**** + ** Fields + ****/ + /// Get a instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + /// Returns the field wrapper, or null if the field doesn't exist and is false. + public IReflectedField GetField(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a instance field from a null object."); + + // get field from hierarchy + IReflectedField field = this.GetFieldFromHierarchy(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; + } + + /// Get a static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedField GetField(Type type, string name, bool required = true) + { + // get field from hierarchy + IReflectedField field = this.GetFieldFromHierarchy(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; + } + + /**** + ** Properties + ****/ + /// Get a instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(object obj, string name, bool required = true) + { + // validate + if (obj == null) + throw new ArgumentNullException(nameof(obj), "Can't get a instance property from a null object."); + + // get property from hierarchy + IReflectedProperty property = this.GetPropertyFromHierarchy(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; + } + + /// Get a static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + public IReflectedProperty GetProperty(Type type, string name, bool required = true) + { + // get field from hierarchy + IReflectedProperty property = this.GetPropertyFromHierarchy(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; + } + + /**** + ** Methods + ****/ + /// Get a instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + public IReflectedMethod GetMethod(object obj, string name, bool required = true) + { + // validate + if (obj == null) + 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); + if (required && method == null) + throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method."); + return method; + } + + /// Get a static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + 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); + if (required && method == null) + throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method."); + return method; + } + + /**** + ** Methods by signature + ****/ + /// Get a instance method. + /// The object which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the field is not found. + 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; + } + + /// Get a static method. + /// The type which has the method. + /// The field name. + /// The argument types of the method signature to find. + /// Whether to throw an exception if the field is not found. + 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; + } + + + /********* + ** Private methods + *********/ + /// Get a field from the type hierarchy. + /// The expected field type. + /// The type which has the field. + /// The object which has the field. + /// The field name. + /// The reflection binding which flags which indicates what type of field to find. + private IReflectedField GetFieldFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + 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; + }); + + return field != null + ? new ReflectedField(type, obj, field, isStatic) + : null; + } + + /// Get a property from the type hierarchy. + /// The expected property type. + /// The type which has the property. + /// The object which has the property. + /// The property name. + /// The reflection binding which flags which indicates what type of property to find. + private IReflectedProperty GetPropertyFromHierarchy(Type type, object obj, string name, BindingFlags bindingFlags) + { + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + PropertyInfo property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () => + { + PropertyInfo propertyInfo = null; + for (; type != null && propertyInfo == null; type = type.BaseType) + propertyInfo = type.GetProperty(name, bindingFlags); + return propertyInfo; + }); + + return property != null + ? new ReflectedProperty(type, obj, property, isStatic) + : null; + } + + /// Get a method from the type hierarchy. + /// The type which has the method. + /// The object which has the method. + /// The method name. + /// The reflection binding which flags which indicates what type of method to find. + 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 methodInfo = null; + for (; type != null && methodInfo == null; type = type.BaseType) + methodInfo = type.GetMethod(name, bindingFlags); + return methodInfo; + }); + + return method != null + ? new ReflectedMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static)) + : null; + } + + /// Get a method from the type hierarchy. + /// The type which has the method. + /// The object which has the method. + /// The method name. + /// The reflection binding which flags which indicates what type of method to find. + /// The argument types of the method signature to find. + 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; + } + + /// Get a method or field through the cache. + /// The expected type. + /// The cache key. + /// Fetches a new value to cache. + private TMemberInfo GetCached(string key, Func fetch) where TMemberInfo : MemberInfo + { + // get from cache + if (this.Cache.ContainsKey(key)) + { + CacheEntry entry = (CacheEntry)this.Cache[key]; + return entry.IsValid + ? (TMemberInfo)entry.MemberInfo + : default(TMemberInfo); + } + + // fetch & cache new value + TMemberInfo result = fetch(); + CacheEntry cacheEntry = new CacheEntry(result != null, result); + this.Cache.Add(key, cacheEntry); + return result; + } + } +} diff --git a/src/StardewModdingAPI/Framework/RequestExitDelegate.cs b/src/StardewModdingAPI/Framework/RequestExitDelegate.cs new file mode 100644 index 00000000..12d0ea0c --- /dev/null +++ b/src/StardewModdingAPI/Framework/RequestExitDelegate.cs @@ -0,0 +1,7 @@ +namespace StardewModdingAPI.Framework +{ + /// 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. + /// The module which requested an immediate exit. + /// The reason provided for the shutdown. + internal delegate void RequestExitDelegate(string module, string reason); +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/RewriteFacades/FarmerMethods.cs b/src/StardewModdingAPI/Framework/RewriteFacades/FarmerMethods.cs new file mode 100644 index 00000000..0a8c4dad --- /dev/null +++ b/src/StardewModdingAPI/Framework/RewriteFacades/FarmerMethods.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Framework.RewriteFacades +{ + class FarmerMethods : Farmer + { + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new bool couldInventoryAcceptThisItem(Item item) + { + return base.couldInventoryAcceptThisItem(item, true); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new bool addItemToInventoryBool(Item item) + { + return base.addItemToInventoryBool(item, false); + } + } +} diff --git a/src/StardewModdingAPI/Framework/RewriteFacades/FarmerRenderMethods.cs b/src/StardewModdingAPI/Framework/RewriteFacades/FarmerRenderMethods.cs new file mode 100644 index 00000000..ccd63210 --- /dev/null +++ b/src/StardewModdingAPI/Framework/RewriteFacades/FarmerRenderMethods.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Framework.RewriteFacades +{ + public class FarmerRendererMethods : FarmerRenderer + { + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void drawMiniPortrat(SpriteBatch b, Vector2 position, float layerDepth, float scale, int facingDirection, Farmer who) + { + base.drawMiniPortrat(b, position, layerDepth, scale, facingDirection, who); + } + } +} diff --git a/src/StardewModdingAPI/Framework/RewriteFacades/Game1Methods.cs b/src/StardewModdingAPI/Framework/RewriteFacades/Game1Methods.cs new file mode 100644 index 00000000..92a54819 --- /dev/null +++ b/src/StardewModdingAPI/Framework/RewriteFacades/Game1Methods.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Framework.RewriteFacades +{ + public class Game1Methods : Game1 + { + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new string parseText(string text, SpriteFont whichFont, int width) + { + return parseText(text, whichFont, width, 1); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void warpFarmer(LocationRequest locationRequest, int tileX, int tileY, int facingDirectionAfterWarp) + { + warpFarmer(locationRequest, tileX, tileY, facingDirectionAfterWarp, true, false); + } + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void warpFarmer(string locationName, int tileX, int tileY, bool flip) + { + warpFarmer(locationName, tileX, tileY, flip ? ((player.FacingDirection + 2) % 4) : player.FacingDirection); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void warpFarmer(string locationName, int tileX, int tileY, int facingDirectionAfterWarp) + { + warpFarmer(locationName, tileX, tileY, facingDirectionAfterWarp, false, true, false); + } + } +} diff --git a/src/StardewModdingAPI/Framework/RewriteFacades/HUDMessageMethods.cs b/src/StardewModdingAPI/Framework/RewriteFacades/HUDMessageMethods.cs new file mode 100644 index 00000000..7bfb2fba --- /dev/null +++ b/src/StardewModdingAPI/Framework/RewriteFacades/HUDMessageMethods.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Framework.RewriteFacades +{ + public class HUDMessageMethods : HUDMessage + { + public HUDMessageMethods(string message, int whatType) + : base(message, whatType, -1) + { + } + + } +} diff --git a/src/StardewModdingAPI/Framework/RewriteFacades/IClickableMenuMethods.cs b/src/StardewModdingAPI/Framework/RewriteFacades/IClickableMenuMethods.cs new file mode 100644 index 00000000..70dddf08 --- /dev/null +++ b/src/StardewModdingAPI/Framework/RewriteFacades/IClickableMenuMethods.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.Framework.RewriteFacades +{ + public class IClickableMenuMethods : IClickableMenu + { + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void drawHoverText(SpriteBatch b, string text, SpriteFont font, int xOffset = 0, int yOffset = 0, int moneyAmounttoDisplayAtBottom = -1, string boldTitleText = null, int healAmountToDisplay = -1, string[] buffIconsToDsiplay = null, Item hoveredItem = null, int currencySymbol = 0, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, int overideX = -1, int overrideY = -1, float alpha = 1, CraftingRecipe craftingIngrediants = null) + { + drawHoverText(b, text, font, xOffset, yOffset, moneyAmounttoDisplayAtBottom, boldTitleText, healAmountToDisplay, buffIconsToDsiplay, hoveredItem, currencySymbol, extraItemToShowIndex, extraItemToShowAmount, overideX, overrideY, alpha, craftingIngrediants, -1, 80, -1); + } + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void drawTextureBox(SpriteBatch b, Texture2D texture, Microsoft.Xna.Framework.Rectangle sourceRect, int x, int y, int width, int height, Color color) + { + drawTextureBox(b, texture, sourceRect, x, y, width, height, color, 1, true, false); + } + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void drawTextureBox(SpriteBatch b, Texture2D texture, Microsoft.Xna.Framework.Rectangle sourceRect, int x, int y, int width, int height, Color color, float scale) + { + drawTextureBox(b, texture, sourceRect, x, y, width, height, color, scale, true, false); + } + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public static new void drawTextureBox(SpriteBatch b, Texture2D texture, Microsoft.Xna.Framework.Rectangle sourceRect, int x, int y, int width, int height, Color color, float scale, bool drawShadow) + { + drawTextureBox(b, texture, sourceRect, x, y, width, height, color, scale, drawShadow, false); + } + + } +} diff --git a/src/StardewModdingAPI/Framework/RewriteFacades/MapPageMethods.cs b/src/StardewModdingAPI/Framework/RewriteFacades/MapPageMethods.cs new file mode 100644 index 00000000..fbd323f6 --- /dev/null +++ b/src/StardewModdingAPI/Framework/RewriteFacades/MapPageMethods.cs @@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.Framework.RewriteFacades +{ + public class MapPageMethods : MapPage + { + public MapPageMethods(int x, int y, int width, int height) + : base(x, y, width, height, 1f, 1f) + { + } + + } +} diff --git a/src/StardewModdingAPI/Framework/RewriteFacades/SpriteBatchMethods.cs b/src/StardewModdingAPI/Framework/RewriteFacades/SpriteBatchMethods.cs new file mode 100644 index 00000000..26b22315 --- /dev/null +++ b/src/StardewModdingAPI/Framework/RewriteFacades/SpriteBatchMethods.cs @@ -0,0 +1,61 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +#pragma warning disable 1591 // missing documentation +namespace StardewModdingAPI.Framework.RewriteFacades +{ + /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + public class SpriteBatchMethods : SpriteBatch + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + + + /**** + ** MonoGame signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); + } + + /**** + ** XNA signatures + ****/ + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin() + { + base.Begin(); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState) + { + base.Begin(sortMode, blendState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); + } + + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); + } + } +} diff --git a/src/StardewModdingAPI/Framework/RewriteFacades/TextBoxMethods.cs b/src/StardewModdingAPI/Framework/RewriteFacades/TextBoxMethods.cs new file mode 100644 index 00000000..cd4aaf0b --- /dev/null +++ b/src/StardewModdingAPI/Framework/RewriteFacades/TextBoxMethods.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Menus; + +#pragma warning disable 1591 // missing documentation +namespace StardewModdingAPI.Framework.RewriteFacades +{ + public class TextBoxMethods : TextBox + { + public TextBoxMethods(Texture2D textboxTexture, Texture2D caretTexture, SpriteFont font, Color textColor) + : base(textboxTexture, caretTexture, font, textColor, true, false) + { + + } + } +} diff --git a/src/StardewModdingAPI/Framework/SCore.cs b/src/StardewModdingAPI/Framework/SCore.cs new file mode 100644 index 00000000..491a85fb --- /dev/null +++ b/src/StardewModdingAPI/Framework/SCore.cs @@ -0,0 +1,1412 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +#if SMAPI_FOR_WINDOWS +using System.Windows.Forms; +#endif +using Newtonsoft.Json; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Events; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Logging; +using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.ModHelpers; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Internal; +using StardewModdingAPI.Patches; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; +using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; +using Object = StardewValley.Object; +using ThreadState = System.Threading.ThreadState; + +namespace StardewModdingAPI.Framework +{ + /// The core class which initialises and manages SMAPI. + internal class SCore : IDisposable + { + /********* + ** Fields + *********/ + /// The log file to which to write messages. + private readonly LogFileManager LogFile; + + /// Manages console output interception. + private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); + + /// The core logger and monitor for SMAPI. + private readonly Monitor Monitor; + + /// The core logger and monitor on behalf of the game. + private readonly Monitor MonitorForGame; + + /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. + private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + + /// Simplifies access to private game code. + private readonly Reflector Reflection = new Reflector(); + + /// The SMAPI configuration settings. + private readonly SConfig Settings; + + /// The underlying game instance. + public SGame GameInstance; + + /// The underlying content manager. + private ContentCoordinator ContentCore => this.GameInstance.ContentCore; + + /// Tracks the installed mods. + /// This is initialised after the game starts. + private readonly ModRegistry ModRegistry = new ModRegistry(); + + /// Manages SMAPI events for mods. + private readonly EventManager EventManager; + + /// Whether the game is currently running. + private bool IsGameRunning; + + /// Whether the program has been disposed. + private bool IsDisposed; + + /// Regex patterns which match console messages to suppress from the console and log. + private readonly Regex[] SuppressConsolePatterns = + { + new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^DebugOutput:\s+(?:added CLOUD|added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant), + new Regex(@"^static SerializableDictionary<.+>\(\) called\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant), + }; + + /// Regex patterns which match console messages to show a more friendly error for. + private readonly Tuple[] ReplaceConsolePatterns = + { + Tuple.Create( + new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.", RegexOptions.Compiled | RegexOptions.CultureInvariant), +#if SMAPI_FOR_WINDOWS + "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that (see 'Part 2: Configure Steam' in the install guide for more info: https://smapi.io/install).", +#else + "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.", +#endif + LogLevel.Error + ) + }; + + /// The mod toolkit used for generic mod interactions. + private readonly ModToolkit Toolkit = new ModToolkit(); + + /// The path to search for mods. + private string ModsPath => Constants.ModsPath; + + + /********* + ** Accessors + *********/ + /// Manages deprecation warnings. + /// This is initialised after the game starts. This is accessed directly because it's not part of the normal class model. + internal static DeprecationManager DeprecationManager { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The path to search for mods. + /// Whether to output log messages to the console. + public SCore(string modsPath, bool writeToConsole) + { + // init paths + this.VerifyPath(modsPath); + this.VerifyPath(Constants.LogDir); + Constants.ModsPath = modsPath; + + // init log file + this.PurgeNormalLogs(); + string logPath = this.GetLogPath(); + + // init basics + this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); + this.LogFile = new LogFileManager(logPath); + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme, this.Settings.VerboseLogging) + { + WriteToConsole = writeToConsole, + ShowTraceInConsole = this.Settings.DeveloperMode, + ShowFullStampInConsole = this.Settings.DeveloperMode + }; + this.MonitorForGame = this.GetSecondaryMonitor("game"); + this.EventManager = new EventManager(this.Monitor, this.ModRegistry); + SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); + + // redirect direct console output + if (this.MonitorForGame.WriteToConsole) + this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); + + // init logging + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); + this.Monitor.Log($"Mods go here: {modsPath}"); + if (modsPath != Constants.DefaultModsPath) + this.Monitor.Log("(Using custom --mods-path argument.)", LogLevel.Trace); + this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); + + // validate platform +#if SMAPI_FOR_WINDOWS + if (Constants.Platform != Platform.Windows) + { + this.Monitor.Log("Oops! You're running Windows, but this version of SMAPI is for Linux or Mac. Please reinstall SMAPI to fix this.", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } +#else + if (Constants.Platform == Platform.Windows) + { + this.Monitor.Log("Oops! You're running {Constants.Platform}, but this version of SMAPI is for Windows. Please reinstall SMAPI to fix this.", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } +#endif + } + + /// Launch SMAPI. + [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions + public void RunInteractively() + { + // initialise SMAPI + try + { +#if !SMAPI_3_0_STRICT + // hook up events + ContentEvents.Init(this.EventManager); + ControlEvents.Init(this.EventManager); + GameEvents.Init(this.EventManager); + GraphicsEvents.Init(this.EventManager); + InputEvents.Init(this.EventManager); + LocationEvents.Init(this.EventManager); + MenuEvents.Init(this.EventManager); + MineEvents.Init(this.EventManager); + MultiplayerEvents.Init(this.EventManager); + PlayerEvents.Init(this.EventManager); + SaveEvents.Init(this.EventManager); + SpecialisedEvents.Init(this.EventManager); + TimeEvents.Init(this.EventManager); +#endif + + // init JSON parser + JsonConverter[] converters = { + new ColorConverter(), + new PointConverter(), + new RectangleConverter() + }; + foreach (JsonConverter converter in converters) + this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter); + + // add error handlers +#if SMAPI_FOR_WINDOWS + Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); + Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); +#endif + AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); + + // add more leniant assembly resolvers + AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); + + // override game + SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); + this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, SCore.DeprecationManager, this.OnLocaleChanged, this.InitialiseAfterGameStart, this.Dispose); + StardewValley.Program.gamePtr = this.GameInstance; + + // apply game patches + new GamePatcher(this.Monitor).Apply( + new DialogueErrorPatch(this.MonitorForGame, this.Reflection), + new ObjectErrorPatch(), + new LoadForNewGamePatch(this.Reflection, this.GameInstance.OnLoadStageChanged) + ); + + // add exit handler + new Thread(() => + { + this.CancellationTokenSource.Token.WaitHandle.WaitOne(); + if (this.IsGameRunning) + { + try + { + File.WriteAllText(Constants.FatalCrashMarker, string.Empty); + File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true); + } + catch (Exception ex) + { + this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}"); + } + + this.GameInstance.Exit(); + } + }).Start(); + + // set window titles + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; + //Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; +#if SMAPI_3_0_STRICT + this.GameInstance.Window.Title += " [SMAPI 3.0 strict mode]"; + Console.Title += " [SMAPI 3.0 strict mode]"; +#endif + } + catch (Exception ex) + { + this.Monitor.Log($"SMAPI failed to initialise: {ex.GetLogSummary()}", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } + + // check update marker + if (File.Exists(Constants.UpdateMarker)) + { + string rawUpdateFound = File.ReadAllText(Constants.UpdateMarker); + if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound)) + { + if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion)) + { + this.Monitor.Log("A new version of SMAPI was detected last time you played.", LogLevel.Error); + this.Monitor.Log($"You can update to {updateFound}: https://smapi.io.", LogLevel.Error); + this.Monitor.Log("Press any key to continue playing anyway. (This only appears when using a SMAPI beta.)", LogLevel.Info); + Console.ReadKey(); + } + } + File.Delete(Constants.UpdateMarker); + } + + // show details if game crashed during last session + if (File.Exists(Constants.FatalCrashMarker)) + { + this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: https://community.playstarbound.com/threads/108375/.", LogLevel.Error); + this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://log.smapi.io.", LogLevel.Error); + this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); + Console.ReadKey(); + File.Delete(Constants.FatalCrashLog); + File.Delete(Constants.FatalCrashMarker); + } + + // start game + this.Monitor.Log("Starting game...", LogLevel.Debug); + try + { + this.IsGameRunning = true; + StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window + //this.GameInstance.Run(); + } + catch (InvalidOperationException ex) when (ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor")) + { + this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error); + this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); + this.PressAnyKeyToExit(); + } + catch (FileNotFoundException ex) when (ex.Message == "Could not find file 'C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Stardew Valley\\Content\\XACT\\FarmerSounds.xgs'.") // path in error is hardcoded regardless of install path + { + this.Monitor.Log("The game can't find its Content\\XACT\\FarmerSounds.xgs file. You can usually fix this by resetting your content files (see https://smapi.io/troubleshoot#reset-content ), or by uninstalling and reinstalling the game.", LogLevel.Error); + this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); + this.PressAnyKeyToExit(); + } + catch (Exception ex) + { + this.MonitorForGame.Log($"The game failed to launch: {ex.GetLogSummary()}", LogLevel.Error); + this.PressAnyKeyToExit(); + } + finally + { + //this.Dispose(); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + // skip if already disposed + if (this.IsDisposed) + return; + this.IsDisposed = true; + this.Monitor.Log("Disposing...", LogLevel.Trace); + + // dispose mod data + foreach (IModMetadata mod in this.ModRegistry.GetAll()) + { + try + { + (mod.Mod as IDisposable)?.Dispose(); + } + catch (Exception ex) + { + mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn); + } + } + + // dispose core components + this.IsGameRunning = false; + this.ConsoleManager?.Dispose(); + this.ContentCore?.Dispose(); + this.CancellationTokenSource?.Dispose(); + this.GameInstance?.Dispose(); + this.LogFile?.Dispose(); + + // end game (moved from Game1.OnExiting to let us clean up first) + Process.GetCurrentProcess().Kill(); + } + + + /********* + ** Private methods + *********/ + /// Initialise SMAPI and mods after the game starts. + private void InitialiseAfterGameStart() + { + // add headers +#if SMAPI_3_0_STRICT + this.Monitor.Log($"You're running SMAPI 3.0 strict mode, so most mods won't work correctly. If that wasn't intended, install the normal version of SMAPI from https://smapi.io instead.", LogLevel.Warn); +#endif + if (this.Settings.DeveloperMode) + this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); + if (!this.Settings.CheckForUpdates) + this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); + if (!this.Monitor.WriteToConsole) + this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); + this.Monitor.VerboseLog("Verbose logging enabled."); + + // validate XNB integrity + if (!this.ValidateContentIntegrity()) + this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); + + // load mod data + ModToolkit toolkit = new ModToolkit(); + ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath); + + // load mods + { + this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); + ModResolver resolver = new ModResolver(); + + // load manifests + IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase).ToArray(); + + // filter out ignored mods + foreach (IModMetadata mod in mods.Where(p => p.IsIgnored)) + this.Monitor.Log($" Skipped {mod.RelativeDirectoryPath} (folder name starts with a dot).", LogLevel.Trace); + mods = mods.Where(p => !p.IsIgnored).ToArray(); + + // load mods + resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl); + mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); + this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase); + + // write metadata file + if (this.Settings.DumpMetadata) + { + ModFolderExport export = new ModFolderExport + { + Exported = DateTime.UtcNow.ToString("O"), + ApiVersion = Constants.ApiVersion.ToString(), + GameVersion = Constants.GameVersion.ToString(), + ModFolderPath = this.ModsPath, + Mods = mods + }; + this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}metadata-dump.json"), export); + } + + // check for updates + this.CheckForUpdatesAsync(mods); + } + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); + return; + } + + // update window titles + int modsLoaded = this.ModRegistry.GetAll().Count(); + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; + //Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; +#if SMAPI_3_0_STRICT + this.GameInstance.Window.Title += " [SMAPI 3.0 strict mode]"; + Console.Title += " [SMAPI 3.0 strict mode]"; +#endif + + + // start SMAPI console + new Thread(this.RunConsoleLoop).Start(); + } + + /// Handle the game changing locale. + private void OnLocaleChanged() + { + // get locale + string locale = this.ContentCore.GetLocale(); + LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language; + + // update mod translation helpers + foreach (IModMetadata mod in this.ModRegistry.GetAll(contentPacks: false)) + (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); + } + + /// Run a loop handling console input. + [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] + private void RunConsoleLoop() + { + // prepare console + this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); + this.GameInstance.CommandManager.Add(null, "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); + this.GameInstance.CommandManager.Add(null, "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); + + // start handling command line input + Thread inputThread = new Thread(() => + { + while (true) + { + // get input + string input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + continue; + + // handle command + this.Monitor.LogUserInput(input); + this.GameInstance.CommandQueue.Enqueue(input); + } + }); + inputThread.Start(); + + // keep console thread alive while the game is running + while (this.IsGameRunning && !this.Monitor.IsExiting) + Thread.Sleep(1000 / 10); + if (inputThread.ThreadState == ThreadState.Running) + inputThread.Abort(); + } + + /// Look for common issues with the game's XNB content, and log warnings if anything looks broken or outdated. + /// Returns whether all integrity checks passed. + private bool ValidateContentIntegrity() + { + this.Monitor.Log("Detecting common issues...", LogLevel.Trace); + bool issuesFound = false; + + // object format (commonly broken by outdated files) + { + // 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}).", LogLevel.Trace); + foreach (KeyValuePair entry in Game1.objectInformation) + { + // must not be empty + if (string.IsNullOrWhiteSpace(entry.Value)) + { + LogIssue(entry.Key, "entry is empty"); + hasObjectIssues = true; + continue; + } + + // require core fields + string[] fields = entry.Value.Split('/'); + if (fields.Length < Object.objectInfoDescriptionIndex + 1) + { + LogIssue(entry.Key, "too few fields for an object"); + hasObjectIssues = true; + continue; + } + + // check min length for specific types + switch (fields[Object.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0]) + { + case "Cooking": + if (fields.Length < Object.objectInfoBuffDurationIndex + 1) + { + LogIssue(entry.Key, "too few fields for a cooking item"); + hasObjectIssues = true; + } + break; + } + } + + // log error + if (hasObjectIssues) + { + issuesFound = true; + this.Monitor.Log(@"Your Content\Data\ObjectInformation.xnb file seems to be broken or outdated.", LogLevel.Warn); + } + } + + return !issuesFound; + } + + /// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available. + /// The mods to include in the update check (if eligible). + private void CheckForUpdatesAsync(IModMetadata[] mods) + { + if (!this.Settings.CheckForUpdates) + return; + + new Thread(() => + { + // create client + string url = this.Settings.WebApiBaseUrl; +#if !SMAPI_FOR_WINDOWS + url = url.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac +#endif + WebApiClient client = new WebApiClient(url, Constants.ApiVersion); + this.Monitor.Log("Checking for updates...", LogLevel.Trace); + + // check SMAPI version + ISemanticVersion updateFound = null; + try + { + ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value; + ISemanticVersion latestStable = response.Main?.Version; + ISemanticVersion latestBeta = response.Optional?.Version; + + if (latestStable == null && response.Errors.Any()) + { + this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); + } + else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) + { + updateFound = latestBeta; + this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert); + } + else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel)) + { + updateFound = latestStable; + this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert); + } + else + this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); + } + catch (Exception ex) + { + this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you won't be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log(ex is WebException && ex.InnerException == null + ? $"Error: {ex.Message}" + : $"Error: {ex.GetLogSummary()}" + ); + } + + // show update message on next launch + if (updateFound != null) + File.WriteAllText(Constants.UpdateMarker, updateFound.ToString()); + + // check mod versions + if (mods.Any()) + { + try + { + HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); + + // prepare search model + List searchMods = new List(); + foreach (IModMetadata mod in mods) + { + if (!mod.HasID() || suppressUpdateChecks.Contains(mod.Manifest.UniqueID)) + continue; + + string[] updateKeys = mod + .GetUpdateKeys(validOnly: true) + .Select(p => p.ToString()) + .ToArray(); + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.ToArray())); + } + + // fetch results + this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); + IDictionary results = client.GetModInfo(searchMods.ToArray()); + + // extract update alerts & errors + var updates = new List>(); + var errors = new StringBuilder(); + 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)) + continue; + mod.SetUpdateData(result); + + // handle errors + if (result.Errors != null && result.Errors.Any()) + { + errors.AppendLine(result.Errors.Length == 1 + ? $" {mod.DisplayName}: {result.Errors[0]}" + : $" {mod.DisplayName}:\n - {string.Join("\n - ", result.Errors)}" + ); + } + + // parse versions + bool useBetaInfo = result.HasBetaInfo && Constants.ApiVersion.IsPrerelease(); + ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; + ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version; + ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version; + ISemanticVersion unofficialVersion = useBetaInfo ? result.UnofficialForBeta?.Version : result.Unofficial?.Version; + + // show update alerts + if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) + updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url)); + else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) + updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url)); + else if (this.IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: mod.Status == ModMetadataStatus.Failed)) + updates.Add(Tuple.Create(mod, unofficialVersion, useBetaInfo ? result.UnofficialForBeta?.Url : result.Unofficial?.Url)); + } + + // show update errors + if (errors.Length != 0) + this.Monitor.Log("Got update-check errors for some mods:\n" + errors.ToString().TrimEnd(), LogLevel.Trace); + + // show update alerts + if (updates.Any()) + { + 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; + this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert); + } + } + else + this.Monitor.Log(" All mods up to date.", LogLevel.Trace); + } + catch (Exception ex) + { + this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn); + this.Monitor.Log(ex is WebException && ex.InnerException == null + ? ex.Message + : ex.ToString() + ); + } + } + }).Start(); + } + + /// Get whether a given version should be offered to the user as an update. + /// The current semantic version. + /// The target semantic version. + /// Whether the user enabled the beta channel and should be offered pre-release updates. + private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) + { + return + newVersion != null + && newVersion.IsNewerThan(currentVersion) + && (useBetaChannel || !newVersion.IsPrerelease()); + } + + /// Create a directory path if it doesn't exist. + /// The directory path. + private void VerifyPath(string path) + { + try + { + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + } + catch (Exception ex) + { + // note: this happens before this.Monitor is initialised + Console.WriteLine($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}"); + } + } + + /// Load and hook up the given mods. + /// The mods to load. + /// The JSON helper with which to read mods' JSON files. + /// The content manager to use for mod content. + /// Handles access to SMAPI's internal mod metadata list. + private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase) + { + this.Monitor.Log("Loading mods...", LogLevel.Trace); + + // load mods + IDictionary> skippedMods = new Dictionary>(); + using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings)) + { + // init + HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); + InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); + void LogSkip(IModMetadata mod, string errorPhrase, string errorDetails) + { + skippedMods[mod] = Tuple.Create(errorPhrase, errorDetails); + if (mod.Status != ModMetadataStatus.Failed) + mod.SetStatus(ModMetadataStatus.Failed, errorPhrase); + } + + // load mods + foreach (IModMetadata contentPack in mods) + { + if (!this.TryLoadMod(contentPack, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) + LogSkip(contentPack, errorPhrase, errorDetails); + } + } + IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); + IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray(); + + // unlock content packs + this.ModRegistry.AreAllModsLoaded = true; + + // log loaded mods + this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info); + foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log( + $" {metadata.DisplayName} {manifest.Version}" + + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), + LogLevel.Info + ); + } + this.Monitor.Newline(); + + // log loaded content packs + if (loadedContentPacks.Any()) + { + 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)) + { + IManifest manifest = metadata.Manifest; + this.Monitor.Log( + $" {metadata.DisplayName} {manifest.Version}" + + (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "") + + (metadata.IsContentPack ? $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}" : "") + + (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""), + LogLevel.Info + ); + } + this.Monitor.Newline(); + } + + // log mod warnings + this.LogModWarnings(this.ModRegistry.GetAll().ToArray(), skippedMods); + + // initialise translations + this.ReloadTranslations(loadedMods); + + // initialise loaded non-content-pack mods + foreach (IModMetadata metadata in loadedMods) + { + // add interceptors + if (metadata.Mod.Helper.Content is ContentHelper helper) + { + // ReSharper disable SuspiciousTypeConversion.Global + if (metadata.Mod is IAssetEditor editor) + helper.ObservableAssetEditors.Add(editor); + if (metadata.Mod is IAssetLoader loader) + helper.ObservableAssetLoaders.Add(loader); + // ReSharper restore SuspiciousTypeConversion.Global + + this.ContentCore.Editors[metadata] = helper.ObservableAssetEditors; + this.ContentCore.Loaders[metadata] = helper.ObservableAssetLoaders; + } + + // call entry method + try + { + IMod mod = metadata.Mod; + mod.Entry(mod.Helper); + } + catch (Exception ex) + { + metadata.LogAsMod($"Mod crashed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error); + } + + // get mod API + try + { + object api = metadata.Mod.GetApi(); + if (api != null && !api.GetType().IsPublic) + { + api = null; + this.Monitor.Log($"{metadata.DisplayName} provides an API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn); + } + + if (api != null) + this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace); + metadata.SetApi(api); + } + catch (Exception ex) + { + this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); + } + } + + // invalidate cache entries when needed + // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) + foreach (IModMetadata metadata in loadedMods) + { + if (metadata.Mod.Helper.Content is ContentHelper helper) + { + helper.ObservableAssetEditors.CollectionChanged += (sender, e) => + { + if (e.NewItems?.Count > 0) + { + this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); + this.ContentCore.InvalidateCacheFor(e.NewItems.Cast().ToArray(), new IAssetLoader[0]); + } + }; + helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => + { + if (e.NewItems?.Count > 0) + { + this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); + this.ContentCore.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast().ToArray()); + } + }; + } + } + + // reset cache now if any editors or loaders were added during entry + IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); + IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); + if (editors.Any() || loaders.Any()) + { + this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); + this.ContentCore.InvalidateCacheFor(editors, loaders); + } + + // unlock mod integrations + this.ModRegistry.AreAllModsInitialised = true; + } + + /// Load a given mod. + /// The mod to load. + /// The mods being loaded. + /// Preprocesses and loads mod assemblies + /// Generates proxy classes to access mod APIs through an arbitrary interface. + /// The JSON helper with which to read mods' JSON files. + /// The content manager to use for mod content. + /// Handles access to SMAPI's internal mod metadata list. + /// The mod IDs to ignore when validating update keys. + /// The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable). + /// More detailed details about the error intended for developers (if any). + /// Returns whether the mod was successfully loaded. + private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, out string errorReasonPhrase, out string errorDetails) + { + errorDetails = null; + + // log entry + { + string relativePath = PathUtilities.GetRelativePath(this.ModsPath, mod.DirectoryPath); + if (mod.IsContentPack) + this.Monitor.Log($" {mod.DisplayName} ({relativePath}) [content pack]...", LogLevel.Trace); + else if (mod.Manifest?.EntryDll != null) + this.Monitor.Log($" {mod.DisplayName} ({relativePath}{Path.DirectorySeparatorChar}{mod.Manifest.EntryDll})...", LogLevel.Trace); // don't use Path.Combine here, since EntryDLL might not be valid + else + this.Monitor.Log($" {mod.DisplayName} ({relativePath})...", LogLevel.Trace); + } + + // add warning for missing update key + 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.Error}", LogLevel.Trace); + errorReasonPhrase = mod.Error; + return false; + } + +#if !SMAPI_3_0_STRICT + // add deprecation warning for old version format + { + if (mod.Manifest?.Version is Toolkit.SemanticVersion version && version.IsLegacyFormat) + SCore.DeprecationManager.Warn(mod.DisplayName, "non-string manifest version", "2.8", DeprecationLevel.PendingRemoval); + } +#endif + + // validate dependencies + // Although dependences are validated before mods are loaded, a dependency may have failed to load. + if (mod.Manifest.Dependencies?.Any() == true) + { + foreach (IManifestDependency dependency in mod.Manifest.Dependencies.Where(p => p.IsRequired)) + { + if (this.ModRegistry.Get(dependency.UniqueID) == null) + { + string dependencyName = mods + .FirstOrDefault(otherMod => otherMod.HasID(dependency.UniqueID)) + ?.DisplayName ?? dependency.UniqueID; + errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded."; + return false; + } + } + } + + // load as content pack + if (mod.IsContentPack) + { + IManifest manifest = mod.Manifest; + IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName); + IContentHelper contentHelper = new ContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); + IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, contentHelper, jsonHelper); + mod.SetMod(contentPack, monitor); + this.ModRegistry.Add(mod); + + errorReasonPhrase = null; + return true; + } + + // load as mod + else + { + IManifest manifest = mod.Manifest; + + // load mod + string assemblyPath = manifest?.EntryDll != null + ? Path.Combine(mod.DirectoryPath, manifest.EntryDll) + : null; + Assembly modAssembly; + try + { + modAssembly = assemblyLoader.Load(mod, assemblyPath, true);// assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible); + this.ModRegistry.TrackAssemblies(mod, modAssembly); + } + catch (IncompatibleInstructionException) // details already in trace logs + { + string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://mods.smapi.io" }.Where(p => p != null).ToArray(); + errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}"; + return false; + } + catch (SAssemblyLoadFailedException ex) + { + errorReasonPhrase = $"it DLL couldn't be loaded: {ex.Message}"; + return false; + } + catch (Exception ex) + { + errorReasonPhrase = "its DLL couldn't be loaded."; + errorDetails = $"Error: {ex.GetLogSummary()}"; + return false; + } + + // initialise mod + try + { + // get mod instance + if (!this.TryLoadModEntry(modAssembly, out Mod modEntry, out errorReasonPhrase)) + return false; + + // get content packs + IContentPack[] GetContentPacks() + { + if (!this.ModRegistry.AreAllModsLoaded) + throw new InvalidOperationException("Can't access content packs before SMAPI finishes loading mods."); + + return this.ModRegistry + .GetAll(assemblyMods: false) + .Where(p => p.IsContentPack && mod.HasID(p.Manifest.ContentPackFor.UniqueID)) + .Select(p => p.ContentPack) + .ToArray(); + } + + // init mod helpers + IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName); + IModHelper modHelper; + { + IModEvents events = new ModEvents(mod, this.EventManager); + ICommandHelper commandHelper = new CommandHelper(mod, this.GameInstance.CommandManager); + IContentHelper contentHelper = new ContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); + IContentPackHelper contentPackHelper = new ContentPackHelper(manifest.UniqueID, new Lazy(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.GameInstance.Multiplayer); + ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); + + IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest) + { + IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); + IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); + } + + modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); + } + + // init mod + modEntry.ModManifest = manifest; + modEntry.Helper = modHelper; + modEntry.Monitor = monitor; + + // track mod + mod.SetMod(modEntry); + this.ModRegistry.Add(mod); + return true; + } + catch (Exception ex) + { + errorReasonPhrase = $"initialisation failed:\n{ex.GetLogSummary()}"; + return false; + } + } + } + + /// Write a summary of mod warnings to the console and log. + /// The loaded mods. + /// The mods which were skipped, along with the friendly and developer reasons. + private void LogModWarnings(IModMetadata[] mods, IDictionary> skippedMods) + { + // get mods with warnings + IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray(); + if (!modsWithWarnings.Any() && !skippedMods.Any()) + return; + + // log intro + { + int count = modsWithWarnings.Union(skippedMods.Keys).Count(); + this.Monitor.Log($"Found {count} mod{(count == 1 ? "" : "s")} with warnings:", LogLevel.Info); + } + + // log skipped mods + if (skippedMods.Any()) + { + this.Monitor.Log(" Skipped mods", LogLevel.Error); + this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error); + this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error); + this.Monitor.Newline(); + + HashSet logged = new HashSet(); + foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) + { + IModMetadata mod = pair.Key; + string errorReason = pair.Value.Item1; + string errorDetails = pair.Value.Item2; + string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {errorReason}"; + + if (!logged.Add($"{message}|{errorDetails}")) + continue; // skip duplicate messages (e.g. if multiple copies of the mod are installed) + + this.Monitor.Log(message, LogLevel.Error); + if (errorDetails != null) + this.Monitor.Log($" ({errorDetails})", LogLevel.Trace); + } + this.Monitor.Newline(); + } + + // log warnings + if (modsWithWarnings.Any()) + { + // issue block format logic + void LogWarningGroup(ModWarning warning, LogLevel logLevel, string heading, params string[] blurb) + { + IModMetadata[] matches = modsWithWarnings + .Where(mod => mod.HasUnsuppressWarning(warning)) + .ToArray(); + if (!matches.Any()) + return; + + this.Monitor.Log(" " + heading, logLevel); + this.Monitor.Log(" " + "".PadRight(50, '-'), logLevel); + foreach (string line in blurb) + this.Monitor.Log(" " + line, logLevel); + this.Monitor.Newline(); + foreach (IModMetadata match in matches) + this.Monitor.Log($" - {match.DisplayName}", logLevel); + this.Monitor.Newline(); + } + + // supported issues + LogWarningGroup(ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods", + "These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,", + "errors, or crashes in-game." + ); + LogWarningGroup(ModWarning.ChangesSaveSerialiser, LogLevel.Warn, "Changed save serialiser", + "These mods change the save serialiser. They may corrupt your save files, or make them unusable if", + "you uninstall these mods." + ); + if (this.Settings.ParanoidWarnings) + { + LogWarningGroup(ModWarning.AccessesFilesystem, LogLevel.Warn, "Accesses filesystem directly", + "These mods directly access the filesystem, and you enabled paranoid warnings. (Note that this may be", + "legitimate and innocent usage; this warning is meaningless without further investigation.)" + ); + LogWarningGroup(ModWarning.AccessesShell, LogLevel.Warn, "Accesses shell/process directly", + "These mods directly access the OS shell or processes, and you enabled paranoid warnings. (Note that", + "this may be legitimate and innocent usage; this warning is meaningless without further investigation.)" + ); + } + LogWarningGroup(ModWarning.PatchesGame, LogLevel.Info, "Patched game code", + "These mods directly change the game code. They're more likely to cause errors or bugs in-game; if", + "your game has issues, try removing these first. Otherwise you can ignore this warning." + ); + LogWarningGroup(ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks", + "These mods bypass SMAPI's normal safety checks, so they're more likely to cause errors or save", + "corruption. If your game has issues, try removing these first." + ); + LogWarningGroup(ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys", + "These mods have no update keys in their manifest. SMAPI may not notify you about updates for these", + "mods. Consider notifying the mod authors about this problem." + ); + LogWarningGroup(ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform", + "These mods use the 'dynamic' keyword, and won't work on Linux/Mac." + ); + } + } + + /// Load a mod's entry class. + /// The mod assembly. + /// The loaded instance. + /// The error indicating why loading failed (if applicable). + /// Returns whether the mod entry class was successfully loaded. + private bool TryLoadModEntry(Assembly modAssembly, out Mod mod, out string error) + { + mod = null; + + // find type + TypeInfo[] modEntries = modAssembly.DefinedTypes.Where(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray(); + if (modEntries.Length == 0) + { + error = $"its DLL has no '{nameof(Mod)}' subclass."; + return false; + } + if (modEntries.Length > 1) + { + error = $"its DLL contains multiple '{nameof(Mod)}' subclasses."; + return false; + } + + // get implementation + mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString()); + if (mod == null) + { + error = "its entry class couldn't be instantiated."; + return false; + } + + error = null; + return true; + } + + /// Reload translations for all mods. + /// The mods for which to reload translations. + private void ReloadTranslations(IEnumerable mods) + { + JsonHelper jsonHelper = this.Toolkit.JsonHelper; + foreach (IModMetadata metadata in mods) + { + if (metadata.IsContentPack) + throw new InvalidOperationException("Can't reload translations for a content pack."); + + // read translation files + IDictionary> translations = new Dictionary>(); + DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); + if (translationsDir.Exists) + { + foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) + { + string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim()); + try + { + if (jsonHelper.ReadJsonFileIfExists(file.FullName, out IDictionary data)) + translations[locale] = data; + else + metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed.", LogLevel.Warn); + } + catch (Exception ex) + { + metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed: {ex.GetLogSummary()}", LogLevel.Warn); + } + } + } + + // validate translations + foreach (string locale in translations.Keys.ToArray()) + { + // skip empty files + if (translations[locale] == null || !translations[locale].Keys.Any()) + { + metadata.LogAsMod($"Mod's i18n/{locale}.json is empty and will be ignored.", LogLevel.Warn); + translations.Remove(locale); + continue; + } + + // handle duplicates + HashSet keys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet duplicateKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + foreach (string key in translations[locale].Keys.ToArray()) + { + if (!keys.Add(key)) + { + duplicateKeys.Add(key); + translations[locale].Remove(key); + } + } + if (duplicateKeys.Any()) + metadata.LogAsMod($"Mod's i18n/{locale}.json has duplicate translation keys: [{string.Join(", ", duplicateKeys)}]. Keys are case-insensitive.", LogLevel.Warn); + } + + // update translation + TranslationHelper translationHelper = (TranslationHelper)metadata.Mod.Helper.Translation; + translationHelper.SetTranslations(translations); + } + } + + /// The method called when the user submits a core SMAPI command in the console. + /// The command name. + /// The command arguments. + private void HandleCommand(string name, string[] arguments) + { + switch (name) + { + case "help": + if (arguments.Any()) + { + Command result = this.GameInstance.CommandManager.Get(arguments[0]); + if (result == null) + this.Monitor.Log("There's no command with that name.", LogLevel.Error); + else + this.Monitor.Log($"{result.Name}: {result.Documentation}{(result.Mod != null ? $"\n(Added by {result.Mod.DisplayName}.)" : "")}", LogLevel.Info); + } + else + { + string message = "The following commands are registered:\n"; + IGrouping[] groups = (from command in this.GameInstance.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName).ToArray(); + foreach (var group in groups) + { + string modName = group.Key ?? "SMAPI"; + string[] commandNames = group.ToArray(); + message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; + } + message += "For more information about a command, type 'help command_name'."; + + this.Monitor.Log(message, LogLevel.Info); + } + break; + + case "reload_i18n": + this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); + this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); + break; + + default: + throw new NotSupportedException($"Unrecognise core SMAPI command '{name}'."); + } + } + + /// Redirect messages logged directly to the console to the given monitor. + /// The monitor with which to log messages as the game. + /// The message to log. + private void HandleConsoleMessage(IMonitor gameMonitor, string message) + { + // detect exception + LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; + + // ignore suppressed message + if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message))) + return; + + // show friendly error if applicable + foreach (var entry in this.ReplaceConsolePatterns) + { + if (entry.Item1.IsMatch(message)) + { + this.Monitor.Log(entry.Item2, entry.Item3); + gameMonitor.Log(message, LogLevel.Trace); + return; + } + } + + // forward to monitor + gameMonitor.Log(message, level); + } + + /// Show a 'press any key to exit' message, and exit when they press a key. + private void PressAnyKeyToExit() + { + this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); + this.PressAnyKeyToExit(showMessage: false); + } + + /// Show a 'press any key to exit' message, and exit when they press a key. + /// Whether to print a 'press any key to exit' message to the console. + private void PressAnyKeyToExit(bool showMessage) + { + if (showMessage) + Console.WriteLine("Game has ended. Press any key to exit."); + Thread.Sleep(100); + Console.ReadKey(); + Environment.Exit(0); + } + + /// Get a monitor instance derived from SMAPI's current settings. + /// The name of the module which will log messages with this instance. + private Monitor GetSecondaryMonitor(string name) + { + return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme, this.Settings.VerboseLogging) + { + WriteToConsole = this.Monitor.WriteToConsole, + ShowTraceInConsole = this.Settings.DeveloperMode, + ShowFullStampInConsole = this.Settings.DeveloperMode + }; + } + + /// Get the absolute path to the next available log file. + private string GetLogPath() + { + // default path + { + FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.{Constants.LogExtension}")); + if (!defaultFile.Exists) + return defaultFile.FullName; + } + + // get first disambiguated path + for (int i = 2; i < int.MaxValue; i++) + { + FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.player-{i}.{Constants.LogExtension}")); + if (!file.Exists) + return file.FullName; + } + + // should never happen + throw new InvalidOperationException("Could not find an available log path."); + } + + /// Delete normal (non-crash) log files created by SMAPI. + private void PurgeNormalLogs() + { + DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir); + if (!logsDir.Exists) + return; + + foreach (FileInfo logFile in logsDir.EnumerateFiles()) + { + // skip non-SMAPI file + if (!logFile.Name.StartsWith(Constants.LogNamePrefix, StringComparison.InvariantCultureIgnoreCase)) + continue; + + // skip crash log + if (logFile.FullName == Constants.FatalCrashLog) + continue; + + // delete file + try + { + FileUtilities.ForceDelete(logFile); + } + catch (IOException) + { + // ignore file if it's in use + } + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs new file mode 100644 index 00000000..5e43d6bb --- /dev/null +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -0,0 +1,1791 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +#if !SMAPI_3_0_STRICT +using Microsoft.Xna.Framework.Input; +#endif +using Netcode; +using StardewModdingAPI.Enums; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Events; +using StardewModdingAPI.Framework.Input; +using StardewModdingAPI.Framework.Networking; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.StateTracking; +using StardewModdingAPI.Framework.Utilities; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewValley; +using StardewValley.BellsAndWhistles; +using StardewValley.Buildings; +using StardewValley.Locations; +using StardewValley.Menus; +using StardewValley.Minigames; +using StardewValley.TerrainFeatures; +using StardewValley.Tools; +using xTile.Dimensions; +using xTile.Layers; +using SObject = StardewValley.Object; + +namespace StardewModdingAPI.Framework +{ + /// SMAPI's extension of the game's core , used to inject events. + internal class SGame : Game1 + { + /********* + ** Fields + *********/ + /**** + ** SMAPI state + ****/ + /// Encapsulates monitoring and logging for SMAPI. + private readonly IMonitor Monitor; + + /// Encapsulates monitoring and logging on the game's behalf. + private readonly IMonitor MonitorForGame; + + /// Manages SMAPI events for mods. + private readonly EventManager Events; + + /// Tracks the installed mods. + private readonly ModRegistry ModRegistry; + + /// Manages deprecation warnings. + private readonly DeprecationManager DeprecationManager; + + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. + private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second + + /// The maximum number of consecutive attempts SMAPI should make to recover from an update error. + private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second + + /// The number of ticks until SMAPI should notify mods that the game has loaded. + /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. + private readonly Countdown AfterLoadTimer = new Countdown(5); + + /// Whether the game is saving and SMAPI has already raised . + private bool IsBetweenSaveEvents; + + /// Whether the game is creating the save file and SMAPI has already raised . + private bool IsBetweenCreateEvents; + + /// A callback to invoke after the content language changes. + private readonly Action OnLocaleChanged; + + /// A callback to invoke after the game finishes initialising. + private readonly Action OnGameInitialised; + + /// A callback to invoke when the game exits. + private readonly Action OnGameExiting; + + /// Simplifies access to private game code. + private readonly Reflector Reflection; + + /**** + ** Game state + ****/ + /// Monitors the entire game state for changes. + private WatcherCore Watchers; + + /// Whether post-game-startup initialisation has been performed. + private bool IsInitialised; + + /// Whether the next content manager requested by the game will be for . + private bool NextContentManagerIsMain; + + + /********* + ** Accessors + *********/ + /// Static state to use while is initialising, which happens before the constructor runs. + internal static SGameConstructorHack ConstructorHack { get; set; } + + /// The number of update ticks which have already executed. This is similar to , but incremented more consistently for every tick. + internal static uint TicksElapsed { get; private set; } + + /// SMAPI's content manager. + public ContentCoordinator ContentCore { get; private set; } + + /// Manages console commands. + public CommandManager CommandManager { get; } = new CommandManager(); + + /// Manages input visible to the game. + public SInputState Input => (SInputState)Game1.input; + + /// The game's core multiplayer utility. + public SMultiplayer Multiplayer => (SMultiplayer)Game1.multiplayer; + + /// A list of queued commands to execute. + /// This property must be threadsafe, since it's accessed from a separate console input thread. + public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue(); + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging for SMAPI. + /// Encapsulates monitoring and logging on the game's behalf. + /// Simplifies access to private game code. + /// Manages SMAPI events for mods. + /// Encapsulates SMAPI's JSON file parsing. + /// Tracks the installed mods. + /// Manages deprecation warnings. + /// A callback to invoke after the content language changes. + /// A callback to invoke after the game finishes initialising. + /// A callback to invoke when the game exits. + internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onLocaleChanged, Action onGameInitialised, Action onGameExiting) + { + SGame.ConstructorHack = null; + + // check expectations + if (this.ContentCore == null) + throw new InvalidOperationException($"The game didn't initialise its first content manager before SMAPI's {nameof(SGame)} constructor. This indicates an incompatible lifecycle change."); + + // init XNA + Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + + // init SMAPI + this.Monitor = monitor; + this.MonitorForGame = monitorForGame; + this.Events = eventManager; + this.ModRegistry = modRegistry; + this.Reflection = reflection; + this.DeprecationManager = deprecationManager; + this.OnLocaleChanged = onLocaleChanged; + this.OnGameInitialised = onGameInitialised; + this.OnGameExiting = onGameExiting; + Game1.input = new SInputState(); + Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived); + Game1.hooks = new SModHooks(this.OnNewDayAfterFade); + + // init observables + Game1.locations = new ObservableCollection(); + } + + /// Initialise just before the game's first update tick. + private void InitialiseAfterGameStarted() + { + // set initial state + this.Input.TrueUpdate(); + + // init watchers + this.Watchers = new WatcherCore(this.Input); + + // raise callback + this.OnGameInitialised(); + } + + /// Perform cleanup logic when the game exits. + /// The event sender. + /// The event args. + /// This overrides the logic in to let SMAPI clean up before exit. + protected override void OnExiting(object sender, EventArgs args) + { + Game1.multiplayer.Disconnect(); + this.OnGameExiting?.Invoke(); + } + + /// A callback invoked before runs. + protected void OnNewDayAfterFade() + { + this.Events.DayEnding.RaiseEmpty(); + } + + /// A callback invoked when a mod message is received. + /// The message to deliver to applicable mods. + private void OnModMessageReceived(ModMessageModel message) + { + // raise events for applicable mods + HashSet modIDs = new HashSet(message.ToModIDs ?? this.ModRegistry.GetAll().Select(p => p.Manifest.UniqueID), StringComparer.InvariantCultureIgnoreCase); + this.Events.ModMessageReceived.RaiseForMods(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID)); + } + + /// A callback invoked when the game's low-level load stage changes. + /// The new load stage. + internal void OnLoadStageChanged(LoadStage newStage) + { + // nothing to do + if (newStage == Context.LoadStage) + return; + + // update data + LoadStage oldStage = Context.LoadStage; + Context.LoadStage = newStage; + if (newStage == LoadStage.None) + { + this.Monitor.Log("Context: returned to title", LogLevel.Trace); + this.Multiplayer.CleanupOnMultiplayerExit(); + } + this.Monitor.VerboseLog($"Context: load stage changed to {newStage}"); + + // raise events + this.Events.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage)); + if (newStage == LoadStage.None) + { + this.Events.ReturnedToTitle.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + this.Events.Legacy_AfterReturnToTitle.Raise(); +#endif + } + } + + /// Constructor a content manager to read XNB files. + /// The service provider to use to locate services. + /// The root directory to search for content. + protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) + { + // Game1._temporaryContent initialising from SGame constructor + // NOTE: this method is called before the SGame constructor runs. Don't depend on anything being initialised at this point. + if (this.ContentCore == null) + { + this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper); + this.NextContentManagerIsMain = true; + return this.ContentCore.CreateGameContentManager("Game1._temporaryContent"); + } + + // Game1.content initialising from LoadContent + if (this.NextContentManagerIsMain) + { + this.NextContentManagerIsMain = false; + return this.ContentCore.MainContentManager; + } + + // any other content manager + return this.ContentCore.CreateGameContentManager("(generated)"); + } + + /// The method called when the game is updating its state. This happens roughly 60 times per second. + /// A snapshot of the game timing state. + protected override void Update(GameTime gameTime) + { + var events = this.Events; + + try + { + this.DeprecationManager.PrintQueued(); + + /********* + ** Special cases + *********/ + // Perform first-tick initialisation. + if (!this.IsInitialised) + { + this.IsInitialised = true; + this.InitialiseAfterGameStarted(); + } + + // Abort if SMAPI is exiting. + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace); + return; + } + + // Run async tasks synchronously to avoid issues due to mod events triggering + // concurrently with game code. + //bool saveParsed = false; + //if (Game1.currentLoader != null) + //{ + // this.Monitor.Log("Game loader synchronising...", LogLevel.Trace); + // while (Game1.currentLoader?.MoveNext() == true) + // { + // // raise load stage changed + // switch (Game1.currentLoader.Current) + // { + // case 20 when (!saveParsed && SaveGame.loaded != null): + // saveParsed = true; + // this.OnLoadStageChanged(LoadStage.SaveParsed); + // break; + + // case 36: + // this.OnLoadStageChanged(LoadStage.SaveLoadedBasicInfo); + // break; + + // case 50: + // this.OnLoadStageChanged(LoadStage.SaveLoadedLocations); + // break; + + // default: + // if (Game1.gameMode == Game1.playingGameMode) + // this.OnLoadStageChanged(LoadStage.Preloaded); + // break; + // } + // } + + // Game1.currentLoader = null; + // this.Monitor.Log("Game loader done.", LogLevel.Trace); + //} + if (Game1._newDayTask?.Status == TaskStatus.Created) + { + this.Monitor.Log("New day task synchronising...", LogLevel.Trace); + Game1._newDayTask.RunSynchronously(); + this.Monitor.Log("New day task done.", LogLevel.Trace); + } + + // While a background task is in progress, the game may make changes to the game + // state while mods are running their code. This is risky, because data changes can + // conflict (e.g. collection changed during enumeration errors) and data may change + // unexpectedly from one mod instruction to the next. + // + // Therefore we can just run Game1.Update here without raising any SMAPI events. There's + // a small chance that the task will finish after we defer but before the game checks, + // which means technically events should be raised, but the effects of missing one + // update tick are neglible and not worth the complications of bypassing Game1.Update. + if (Game1._newDayTask != null || Game1.gameMode == Game1.loadingMode) + { + events.UnvalidatedUpdateTicking.RaiseEmpty(); + SGame.TicksElapsed++; + base.Update(gameTime); + events.UnvalidatedUpdateTicked.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_UnvalidatedUpdateTick.Raise(); +#endif + return; + } + + /********* + ** Execute commands + *********/ + while (this.CommandQueue.TryDequeue(out string rawInput)) + { + // parse command + string name; + string[] args; + Command command; + try + { + if (!this.CommandManager.TryParse(rawInput, out name, out args, out command)) + { + this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + continue; + } + } + catch (Exception ex) + { + this.Monitor.Log($"Failed parsing that command:\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // execute command + try + { + command.Callback.Invoke(name, args); + } + catch (Exception ex) + { + if (command.Mod != null) + command.Mod.LogAsMod($"Mod failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"Failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + + /********* + ** Update input + *********/ + // This should *always* run, even when suppressing mod events, since the game uses + // this too. For example, doing this after mod event suppression would prevent the + // user from doing anything on the overnight shipping screen. +#if !SMAPI_3_0_STRICT + SInputState previousInputState = this.Input.Clone(); +#endif + SInputState inputState = this.Input; + if (this.IsActive) + inputState.TrueUpdate(); + + /********* + ** Save events + suppress events during save + *********/ + // While the game is writing to the save file in the background, mods can unexpectedly + // fail since they don't have exclusive access to resources (e.g. collection changed + // during enumeration errors). To avoid problems, events are not invoked while a save + // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is + // opened (since the save hasn't started yet), but all other events should be suppressed. + if (Context.IsSaving) + { + // raise before-create + if (!Context.IsWorldReady && !this.IsBetweenCreateEvents) + { + this.IsBetweenCreateEvents = true; + this.Monitor.Log("Context: before save creation.", LogLevel.Trace); + events.SaveCreating.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_BeforeCreateSave.Raise(); +#endif + } + + // raise before-save + if (Context.IsWorldReady && !this.IsBetweenSaveEvents) + { + this.IsBetweenSaveEvents = true; + this.Monitor.Log("Context: before save.", LogLevel.Trace); + events.Saving.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_BeforeSave.Raise(); +#endif + } + + // suppress non-save events + events.UnvalidatedUpdateTicking.RaiseEmpty(); + SGame.TicksElapsed++; + base.Update(gameTime); + events.UnvalidatedUpdateTicked.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_UnvalidatedUpdateTick.Raise(); +#endif + return; + } + if (this.IsBetweenCreateEvents) + { + // raise after-create + this.IsBetweenCreateEvents = false; + this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + this.OnLoadStageChanged(LoadStage.CreatedSaveFile); + events.SaveCreated.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_AfterCreateSave.Raise(); +#endif + } + if (this.IsBetweenSaveEvents) + { + // raise after-save + this.IsBetweenSaveEvents = false; + this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + events.Saved.RaiseEmpty(); + events.DayStarted.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_AfterSave.Raise(); + events.Legacy_AfterDayStarted.Raise(); +#endif + } + + /********* + ** Update context + *********/ + bool wasWorldReady = Context.IsWorldReady; + if ((Context.IsWorldReady && !Context.IsSaveLoaded) || Game1.exitToTitle) + { + Context.IsWorldReady = false; + this.AfterLoadTimer.Reset(); + } + else if (Context.IsSaveLoaded && this.AfterLoadTimer.Current > 0 && Game1.currentLocation != null) + { + if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) + this.AfterLoadTimer.Decrement(); + Context.IsWorldReady = this.AfterLoadTimer.Current == 0; + } + + /********* + ** Update watchers + *********/ + this.Watchers.Update(); + + /********* + ** Locale changed events + *********/ + if (this.Watchers.LocaleWatcher.IsChanged) + { + var was = this.Watchers.LocaleWatcher.PreviousValue; + var now = this.Watchers.LocaleWatcher.CurrentValue; + + this.Monitor.Log($"Context: locale set to {now}.", LogLevel.Trace); + + this.OnLocaleChanged(); +#if !SMAPI_3_0_STRICT + events.Legacy_LocaleChanged.Raise(new EventArgsValueChanged(was.ToString(), now.ToString())); +#endif + + this.Watchers.LocaleWatcher.Reset(); + } + + /********* + ** Load / return-to-title events + *********/ + if (wasWorldReady && !Context.IsWorldReady) + this.OnLoadStageChanged(LoadStage.None); + else if (Context.IsWorldReady && Context.LoadStage != LoadStage.Ready) + { + // print context + string context = $"Context: loaded save '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}, locale set to {this.ContentCore.Language}."; + if (Context.IsMultiplayer) + { + int onlineCount = Game1.getOnlineFarmers().Count(); + context += $" {(Context.IsMainPlayer ? "Main player" : "Farmhand")} with {onlineCount} {(onlineCount == 1 ? "player" : "players")} online."; + } + else + context += " Single-player."; + this.Monitor.Log(context, LogLevel.Trace); + + // raise events + this.OnLoadStageChanged(LoadStage.Ready); + events.SaveLoaded.RaiseEmpty(); + events.DayStarted.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_AfterLoad.Raise(); + events.Legacy_AfterDayStarted.Raise(); +#endif + } + + /********* + ** Window events + *********/ + // Here we depend on the game's viewport instead of listening to the Window.Resize + // event because we need to notify mods after the game handles the resize, so the + // game's metadata (like Game1.viewport) are updated. That's a bit complicated + // since the game adds & removes its own handler on the fly. + if (this.Watchers.WindowSizeWatcher.IsChanged) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); + + Point oldSize = this.Watchers.WindowSizeWatcher.PreviousValue; + Point newSize = this.Watchers.WindowSizeWatcher.CurrentValue; + + events.WindowResized.Raise(new WindowResizedEventArgs(oldSize, newSize)); +#if !SMAPI_3_0_STRICT + events.Legacy_Resize.Raise(); +#endif + this.Watchers.WindowSizeWatcher.Reset(); + } + + /********* + ** Input events (if window has focus) + *********/ + if (this.IsActive) + { + // raise events + bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); + if (!isChatInput) + { + ICursorPosition cursor = this.Input.CursorPosition; + + // raise cursor moved event + if (this.Watchers.CursorWatcher.IsChanged) + { + if (events.CursorMoved.HasListeners()) + { + ICursorPosition was = this.Watchers.CursorWatcher.PreviousValue; + ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue; + this.Watchers.CursorWatcher.Reset(); + + events.CursorMoved.Raise(new CursorMovedEventArgs(was, now)); + } + else + this.Watchers.CursorWatcher.Reset(); + } + + // raise mouse wheel scrolled + if (this.Watchers.MouseWheelScrollWatcher.IsChanged) + { + if (events.MouseWheelScrolled.HasListeners() || this.Monitor.IsVerbose) + { + int was = this.Watchers.MouseWheelScrollWatcher.PreviousValue; + int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue; + this.Watchers.MouseWheelScrollWatcher.Reset(); + + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace); + events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, was, now)); + } + else + this.Watchers.MouseWheelScrollWatcher.Reset(); + } + + // raise input button events + foreach (var pair in inputState.ActiveButtons) + { + SButton button = pair.Key; + InputStatus status = pair.Value; + + if (status == InputStatus.Pressed) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); + + events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); + +#if !SMAPI_3_0_STRICT + // legacy events + events.Legacy_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); + if (button.TryGetKeyboard(out Keys key)) + { + if (key != Keys.None) + events.Legacy_KeyPressed.Raise(new EventArgsKeyPressed(key)); + } + else if (button.TryGetController(out Buttons controllerButton)) + { + if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) + events.Legacy_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); + else + events.Legacy_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); + } +#endif + } + else if (status == InputStatus.Released) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); + + events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); + +#if !SMAPI_3_0_STRICT + // legacy events + events.Legacy_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); + if (button.TryGetKeyboard(out Keys key)) + { + if (key != Keys.None) + events.Legacy_KeyReleased.Raise(new EventArgsKeyPressed(key)); + } + else if (button.TryGetController(out Buttons controllerButton)) + { + if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) + events.Legacy_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); + else + events.Legacy_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); + } +#endif + } + } + +#if !SMAPI_3_0_STRICT + // raise legacy state-changed events + if (inputState.RealKeyboard != previousInputState.RealKeyboard) + events.Legacy_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); + if (inputState.RealMouse != previousInputState.RealMouse) + events.Legacy_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, new Point((int)previousInputState.CursorPosition.ScreenPixels.X, (int)previousInputState.CursorPosition.ScreenPixels.Y), new Point((int)inputState.CursorPosition.ScreenPixels.X, (int)inputState.CursorPosition.ScreenPixels.Y))); +#endif + } + } + + /********* + ** Menu events + *********/ + if (this.Watchers.ActiveMenuWatcher.IsChanged) + { + IClickableMenu was = this.Watchers.ActiveMenuWatcher.PreviousValue; + IClickableMenu now = this.Watchers.ActiveMenuWatcher.CurrentValue; + this.Watchers.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards + + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace); + + // raise menu events + events.MenuChanged.Raise(new MenuChangedEventArgs(was, now)); +#if !SMAPI_3_0_STRICT + if (now != null) + events.Legacy_MenuChanged.Raise(new EventArgsClickableMenuChanged(was, now)); + else + events.Legacy_MenuClosed.Raise(new EventArgsClickableMenuClosed(was)); +#endif + } + + /********* + ** World & player events + *********/ + if (Context.IsWorldReady) + { + bool raiseWorldEvents = !this.Watchers.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded + + // raise location changes + if (this.Watchers.LocationsWatcher.IsChanged) + { + // location list changes + if (this.Watchers.LocationsWatcher.IsLocationListChanged) + { + GameLocation[] added = this.Watchers.LocationsWatcher.Added.ToArray(); + GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray(); + this.Watchers.LocationsWatcher.ResetLocationList(); + + if (this.Monitor.IsVerbose) + { + string addedText = this.Watchers.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; + string removedText = this.Watchers.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; + this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); + } + + events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); +#if !SMAPI_3_0_STRICT + events.Legacy_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); +#endif + } + + // raise location contents changed + if (raiseWorldEvents) + { + foreach (LocationTracker watcher in this.Watchers.LocationsWatcher.Locations) + { + // buildings changed + if (watcher.BuildingsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + Building[] added = watcher.BuildingsWatcher.Added.ToArray(); + Building[] removed = watcher.BuildingsWatcher.Removed.ToArray(); + watcher.BuildingsWatcher.Reset(); + + events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, added, removed)); +#if !SMAPI_3_0_STRICT + events.Legacy_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); +#endif + } + + // debris changed + if (watcher.DebrisWatcher.IsChanged) + { + GameLocation location = watcher.Location; + Debris[] added = watcher.DebrisWatcher.Added.ToArray(); + Debris[] removed = watcher.DebrisWatcher.Removed.ToArray(); + watcher.DebrisWatcher.Reset(); + + events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, added, removed)); + } + + // large terrain features changed + if (watcher.LargeTerrainFeaturesWatcher.IsChanged) + { + GameLocation location = watcher.Location; + LargeTerrainFeature[] added = watcher.LargeTerrainFeaturesWatcher.Added.ToArray(); + LargeTerrainFeature[] removed = watcher.LargeTerrainFeaturesWatcher.Removed.ToArray(); + watcher.LargeTerrainFeaturesWatcher.Reset(); + + events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, added, removed)); + } + + // NPCs changed + if (watcher.NpcsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + NPC[] added = watcher.NpcsWatcher.Added.ToArray(); + NPC[] removed = watcher.NpcsWatcher.Removed.ToArray(); + watcher.NpcsWatcher.Reset(); + + events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, added, removed)); + } + + // objects changed + if (watcher.ObjectsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + KeyValuePair[] added = watcher.ObjectsWatcher.Added.ToArray(); + KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); + watcher.ObjectsWatcher.Reset(); + + events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, added, removed)); +#if !SMAPI_3_0_STRICT + events.Legacy_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); +#endif + } + + // terrain features changed + if (watcher.TerrainFeaturesWatcher.IsChanged) + { + GameLocation location = watcher.Location; + KeyValuePair[] added = watcher.TerrainFeaturesWatcher.Added.ToArray(); + KeyValuePair[] removed = watcher.TerrainFeaturesWatcher.Removed.ToArray(); + watcher.TerrainFeaturesWatcher.Reset(); + + events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, added, removed)); + } + } + } + else + this.Watchers.LocationsWatcher.Reset(); + } + + // raise time changed + if (raiseWorldEvents && this.Watchers.TimeWatcher.IsChanged) + { + int was = this.Watchers.TimeWatcher.PreviousValue; + int now = this.Watchers.TimeWatcher.CurrentValue; + this.Watchers.TimeWatcher.Reset(); + + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); + + events.TimeChanged.Raise(new TimeChangedEventArgs(was, now)); +#if !SMAPI_3_0_STRICT + events.Legacy_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); +#endif + } + else + this.Watchers.TimeWatcher.Reset(); + + // raise player events + if (raiseWorldEvents) + { + PlayerTracker playerTracker = this.Watchers.CurrentPlayerTracker; + + // raise current location changed + if (playerTracker.TryGetNewLocation(out GameLocation newLocation)) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); + + GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue; + events.Warped.Raise(new WarpedEventArgs(playerTracker.Player, oldLocation, newLocation)); +#if !SMAPI_3_0_STRICT + events.Legacy_PlayerWarped.Raise(new EventArgsPlayerWarped(oldLocation, newLocation)); +#endif + } + + // raise player leveled up a skill + foreach (KeyValuePair> pair in playerTracker.GetChangedSkills()) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); + + events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue)); +#if !SMAPI_3_0_STRICT + events.Legacy_LeveledUp.Raise(new EventArgsLevelUp((EventArgsLevelUp.LevelType)pair.Key, pair.Value.CurrentValue)); +#endif + } + + // raise player inventory changed + ItemStackChange[] changedItems = playerTracker.GetInventoryChanges().ToArray(); + if (changedItems.Any()) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); + events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems)); +#if !SMAPI_3_0_STRICT + events.Legacy_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems)); +#endif + } + + // raise mine level changed + if (playerTracker.TryGetNewMineLevel(out int mineLevel)) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); +#if !SMAPI_3_0_STRICT + events.Legacy_MineLevelChanged.Raise(new EventArgsMineLevelChanged(playerTracker.MineLevelWatcher.PreviousValue, mineLevel)); +#endif + } + } + this.Watchers.CurrentPlayerTracker?.Reset(); + } + + // update save ID watcher + this.Watchers.SaveIdWatcher.Reset(); + + /********* + ** Game update + *********/ + // game launched + bool isFirstTick = SGame.TicksElapsed == 0; + if (isFirstTick) + events.GameLaunched.Raise(new GameLaunchedEventArgs()); + + // preloaded + if (Context.IsSaveLoaded && Context.LoadStage != LoadStage.Loaded && Context.LoadStage != LoadStage.Ready) + this.OnLoadStageChanged(LoadStage.Loaded); + + // update tick + bool isOneSecond = SGame.TicksElapsed % 60 == 0; + events.UnvalidatedUpdateTicking.RaiseEmpty(); + events.UpdateTicking.RaiseEmpty(); + if (isOneSecond) + events.OneSecondUpdateTicking.RaiseEmpty(); + try + { + this.Input.UpdateSuppression(); + SGame.TicksElapsed++; + base.Update(gameTime); + } + catch (Exception ex) + { + this.MonitorForGame.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); + } + events.UnvalidatedUpdateTicked.RaiseEmpty(); + events.UpdateTicked.RaiseEmpty(); + if (isOneSecond) + events.OneSecondUpdateTicked.RaiseEmpty(); + + /********* + ** Update events + *********/ +#if !SMAPI_3_0_STRICT + events.Legacy_UnvalidatedUpdateTick.Raise(); + if (isFirstTick) + events.Legacy_FirstUpdateTick.Raise(); + events.Legacy_UpdateTick.Raise(); + if (SGame.TicksElapsed % 2 == 0) + events.Legacy_SecondUpdateTick.Raise(); + if (SGame.TicksElapsed % 4 == 0) + events.Legacy_FourthUpdateTick.Raise(); + if (SGame.TicksElapsed % 8 == 0) + events.Legacy_EighthUpdateTick.Raise(); + if (SGame.TicksElapsed % 15 == 0) + events.Legacy_QuarterSecondTick.Raise(); + if (SGame.TicksElapsed % 30 == 0) + events.Legacy_HalfSecondTick.Raise(); + if (SGame.TicksElapsed % 60 == 0) + events.Legacy_OneSecondTick.Raise(); +#endif + + this.UpdateCrashTimer.Reset(); + } + catch (Exception ex) + { + // log error + this.Monitor.Log($"An error occured in the overridden update loop: {ex.GetLogSummary()}", LogLevel.Error); + + // exit if irrecoverable + if (!this.UpdateCrashTimer.Decrement()) + this.Monitor.ExitGameImmediately("the game crashed when updating, and SMAPI was unable to recover the game."); + } + } + + /// The method called to draw everything to the screen. + /// A snapshot of the game timing state. + protected override void Draw(GameTime gameTime) + { + Context.IsInDrawLoop = true; + try + { + this.DrawImpl(gameTime); + this.DrawCrashTimer.Reset(); + } + catch (Exception ex) + { + // log error + this.Monitor.Log($"An error occured in the overridden draw loop: {ex.GetLogSummary()}", LogLevel.Error); + + // exit if irrecoverable + if (!this.DrawCrashTimer.Decrement()) + { + this.Monitor.ExitGameImmediately("the game crashed when drawing, and SMAPI was unable to recover the game."); + return; + } + + // recover sprite batch + try + { + if (Game1.spriteBatch.IsOpen(this.Reflection)) + { + this.Monitor.Log("Recovering sprite batch from error...", LogLevel.Trace); + Game1.spriteBatch.End(); + } + } + catch (Exception innerEx) + { + this.Monitor.Log($"Could not recover sprite batch state: {innerEx.GetLogSummary()}", LogLevel.Error); + } + } + Context.IsInDrawLoop = false; + } + + /// Replicate the game's draw logic with some changes for SMAPI. + /// A snapshot of the game timing state. + /// This implementation is identical to , except for try..catch around menu draw code, private field references replaced by wrappers, and added events. + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "LocalVariableHidesMember", 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", "RedundantCast", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "RedundantExplicitNullableCreation", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] + [SuppressMessage("SMAPI.CommonErrors", "AvoidNetField", Justification = "copied from game code as-is")] + [SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")] + private void DrawImpl(GameTime gameTime) + { + var events = this.Events; + if (skipNextDrawCall) + { + skipNextDrawCall = false; + } + else + { + IReflectedField _drawActiveClickableMenu = this.Reflection.GetField(this, "_drawActiveClickableMenu"); + IReflectedField _drawHUD = this.Reflection.GetField(this, "_drawHUD"); + IReflectedField bgColor = this.Reflection.GetField(this, "bgColor"); + IReflectedField> _farmerShadows = this.Reflection.GetField>(this, "_farmerShadows"); + IReflectedField _debugStringBuilder = this.Reflection.GetField(typeof(Game1), "_debugStringBuilder"); + IReflectedField lightingBlend = this.Reflection.GetField(this, "lightingBlend"); + + IReflectedMethod SpriteBatchBegin = this.Reflection.GetMethod(this, "SpriteBatchBegin", new Type[] { typeof(float) }); + IReflectedMethod _spriteBatchBegin = this.Reflection.GetMethod(this, "_spriteBatchBegin", new Type[] { typeof(SpriteSortMode), typeof(BlendState), typeof(SamplerState), typeof(DepthStencilState), typeof(RasterizerState), typeof(Effect), typeof(Matrix) }); + IReflectedMethod _spriteBatchEnd = this.Reflection.GetMethod(this, "_spriteBatchEnd", new Type[] { }); + IReflectedMethod DrawLoadingDotDotDot = this.Reflection.GetMethod(this, "DrawLoadingDotDotDot", new Type[] { typeof(GameTime) }); + IReflectedMethod CheckToReloadGameLocationAfterDrawFail = this.Reflection.GetMethod(this, "CheckToReloadGameLocationAfterDrawFail", new Type[] { typeof(string), typeof(Exception) }); + IReflectedMethod DrawTapToMoveTarget = this.Reflection.GetMethod(this, "DrawTapToMoveTarget", new Type[] { }); + IReflectedMethod DrawDayTimeMoneyBox = this.Reflection.GetMethod(this, "DrawDayTimeMoneyBox", new Type[] { }); + IReflectedMethod DrawAfterMap = this.Reflection.GetMethod(this, "DrawAfterMap", new Type[] { }); + IReflectedMethod DrawToolbar = this.Reflection.GetMethod(this, "DrawToolbar", new Type[] { }); + IReflectedMethod DrawVirtualJoypad = this.Reflection.GetMethod(this, "DrawVirtualJoypad", new Type[] { }); + IReflectedMethod DrawFadeToBlackFullScreenRect = this.Reflection.GetMethod(this, "DrawFadeToBlackFullScreenRect", new Type[] { }); + IReflectedMethod DrawChatBox = this.Reflection.GetMethod(this, "DrawChatBox", new Type[] { }); + IReflectedMethod DrawDialogueBoxForPinchZoom = this.Reflection.GetMethod(this, "DrawDialogueBoxForPinchZoom", new Type[] { }); + IReflectedMethod DrawUnscaledActiveClickableMenuForPinchZoom = this.Reflection.GetMethod(this, "DrawUnscaledActiveClickableMenuForPinchZoom", new Type[] { }); + IReflectedMethod DrawNativeScaledActiveClickableMenuForPinchZoom = this.Reflection.GetMethod(this, "DrawNativeScaledActiveClickableMenuForPinchZoom", new Type[] { }); + IReflectedMethod DrawHUDMessages = this.Reflection.GetMethod(this, "DrawHUDMessages", new Type[] { }); + IReflectedMethod DrawTutorialUI = this.Reflection.GetMethod(this, "DrawTutorialUI", new Type[] { }); + IReflectedMethod DrawGreenPlacementBounds = this.Reflection.GetMethod(this, "DrawGreenPlacementBounds", new Type[] { }); + + _drawHUD.SetValue(false); + _drawActiveClickableMenu.SetValue(false); + if (_newDayTask != null) + { + base.GraphicsDevice.Clear(bgColor.GetValue()); + return; + } + if (options.zoomLevel != 1f) + { + base.GraphicsDevice.SetRenderTarget(screen); + } + if (IsSaving) + { + base.GraphicsDevice.Clear(bgColor.GetValue()); + renderScreenBuffer(BlendState.Opaque); + if (activeClickableMenu != null) + { + if (IsActiveClickableMenuNativeScaled) + { + BackupViewportAndZoom(divideByZoom: true); + SetSpriteBatchBeginNextID("A1"); + SpriteBatchBegin.Invoke(NativeZoomLevel); + events.Rendering.RaiseEmpty(); + try + { + events.RenderingActiveMenu.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPreRenderGuiEvent.Raise(); +#endif + Game1.activeClickableMenu.draw(Game1.spriteBatch); + events.RenderedActiveMenu.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderGuiEvent.Raise(); +#endif + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderEvent.Raise(); +#endif + _spriteBatchEnd.Invoke(); + RestoreViewportAndZoom(); + } + else + { + BackupViewportAndZoom(); + SetSpriteBatchBeginNextID("A2"); + SpriteBatchBegin.Invoke(1f); + events.Rendering.RaiseEmpty(); + try + { + events.RenderingActiveMenu.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPreRenderGuiEvent.Raise(); +#endif + Game1.activeClickableMenu.draw(Game1.spriteBatch); + events.RenderedActiveMenu.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderGuiEvent.Raise(); +#endif + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + events.Rendered.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderEvent.Raise(); +#endif + + _spriteBatchEnd.Invoke(); + RestoreViewportAndZoom(); + } + } + if (overlayMenu != null) + { + BackupViewportAndZoom(); + SetSpriteBatchBeginNextID("B"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + overlayMenu.draw(spriteBatch); + _spriteBatchEnd.Invoke(); + RestoreViewportAndZoom(); + } + return; + } + base.GraphicsDevice.Clear(bgColor.GetValue()); + if (activeClickableMenu != null && options.showMenuBackground && activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) + { + Matrix value = Matrix.CreateScale(1f); + SetSpriteBatchBeginNextID("C"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, value); + events.Rendering.RaiseEmpty(); + try + { + Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); + events.RenderingActiveMenu.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPreRenderGuiEvent.Raise(); +#endif + Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); + Game1.activeClickableMenu.draw(Game1.spriteBatch); + events.RenderedActiveMenu.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderGuiEvent.Raise(); +#endif + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + events.Rendered.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderEvent.Raise(); +#endif + + _spriteBatchEnd.Invoke(); + drawOverlays(spriteBatch); + renderScreenBuffer(BlendState.AlphaBlend); + if (overlayMenu != null) + { + SetSpriteBatchBeginNextID("D"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + overlayMenu.draw(spriteBatch); + _spriteBatchEnd.Invoke(); + } + return; + } + if (emergencyLoading) + { + if (!SeenConcernedApeLogo) + { + SetSpriteBatchBeginNextID("E"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + if (logoFadeTimer < 5000) + { + spriteBatch.Draw(staminaRect, new Microsoft.Xna.Framework.Rectangle(0, 0, viewport.Width, viewport.Height), Color.White); + } + if (logoFadeTimer > 4500) + { + float scale = System.Math.Min(1f, (float)(logoFadeTimer - 4500) / 500f); + spriteBatch.Draw(staminaRect, new Microsoft.Xna.Framework.Rectangle(0, 0, viewport.Width, viewport.Height), Color.Black * scale); + } + spriteBatch.Draw(titleButtonsTexture, new Vector2(viewport.Width / 2, viewport.Height / 2 - 90), new Microsoft.Xna.Framework.Rectangle(171 + ((logoFadeTimer / 100 % 2 == 0) ? 111 : 0), 311, 111, 60), Color.White * ((logoFadeTimer < 500) ? ((float)logoFadeTimer / 500f) : ((logoFadeTimer > 4500) ? (1f - (float)(logoFadeTimer - 4500) / 500f) : 1f)), 0f, Vector2.Zero, 3f, SpriteEffects.None, 0.2f); + spriteBatch.Draw(titleButtonsTexture, new Vector2(viewport.Width / 2 - 261, viewport.Height / 2 - 102), new Microsoft.Xna.Framework.Rectangle((logoFadeTimer / 100 % 2 == 0) ? 85 : 0, 306, 85, 69), Color.White * ((logoFadeTimer < 500) ? ((float)logoFadeTimer / 500f) : ((logoFadeTimer > 4500) ? (1f - (float)(logoFadeTimer - 4500) / 500f) : 1f)), 0f, Vector2.Zero, 3f, SpriteEffects.None, 0.2f); + _spriteBatchEnd.Invoke(); + } + logoFadeTimer -= gameTime.ElapsedGameTime.Milliseconds; + } + if (gameMode == 11) + { + SetSpriteBatchBeginNextID("F"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + events.Rendering.RaiseEmpty(); + spriteBatch.DrawString(dialogueFont, content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); + spriteBatch.DrawString(dialogueFont, content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, 255, 0)); + spriteBatch.DrawString(dialogueFont, parseText(errorMessage, dialogueFont, graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); + events.Rendered.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderEvent.Raise(); +#endif + + _spriteBatchEnd.Invoke(); + return; + } + if (currentMinigame != null) + { + currentMinigame.draw(spriteBatch); + if (globalFade && !menuUp && (!nameSelectUp || messagePause)) + { + SetSpriteBatchBeginNextID("G"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + spriteBatch.Draw(fadeToBlackRect, graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((gameMode == 0) ? (1f - fadeToBlackAlpha) : fadeToBlackAlpha)); + _spriteBatchEnd.Invoke(); + } + drawOverlays(spriteBatch); + renderScreenBuffer(BlendState.AlphaBlend); + if ((currentMinigame is FishingGame || currentMinigame is FantasyBoardGame) && activeClickableMenu != null) + { + SetSpriteBatchBeginNextID("A-A"); + SpriteBatchBegin.Invoke(IsActiveClickableMenuNativeScaled ? NativeZoomLevel : 1f); + activeClickableMenu.draw(spriteBatch); + _spriteBatchEnd.Invoke(); + drawOverlays(spriteBatch); + } + return; + } + if (showingEndOfNightStuff) + { + renderScreenBuffer(BlendState.Opaque); + BackupViewportAndZoom(divideByZoom: true); + SetSpriteBatchBeginNextID("A-B"); + SpriteBatchBegin.Invoke(IsActiveClickableMenuNativeScaled ? NativeZoomLevel : 1f); + events.Rendering.RaiseEmpty(); + if (activeClickableMenu != null) + { + try + { + events.RenderingActiveMenu.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPreRenderGuiEvent.Raise(); +#endif + Game1.activeClickableMenu.draw(Game1.spriteBatch); + events.RenderedActiveMenu.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderGuiEvent.Raise(); +#endif + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + } + + events.Rendered.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderEvent.Raise(); +#endif + + _spriteBatchEnd.Invoke(); + drawOverlays(spriteBatch); + RestoreViewportAndZoom(); + return; + } + if (gameMode == 6 || (gameMode == 3 && currentLocation == null)) + { + events.Rendering.RaiseEmpty(); + DrawLoadingDotDotDot.Invoke(gameTime); + events.Rendered.RaiseEmpty(); + +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderEvent.Raise(); +#endif + + drawOverlays(spriteBatch); + renderScreenBuffer(BlendState.AlphaBlend); + if (overlayMenu != null) + { + SetSpriteBatchBeginNextID("H"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + overlayMenu.draw(spriteBatch); + _spriteBatchEnd.Invoke(); + } + base.Draw(gameTime); + return; + } + if (gameMode == 0) + { + SetSpriteBatchBeginNextID("I"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + events.Rendering.RaiseEmpty(); + } + else if (!drawGame) + { + SetSpriteBatchBeginNextID("J"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null, null, null); + events.Rendering.RaiseEmpty(); + } + else if (drawGame) + { + if (drawLighting) + { + base.GraphicsDevice.SetRenderTarget(lightmap); + base.GraphicsDevice.Clear(Color.White * 0f); + SetSpriteBatchBeginNextID("K"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null, null, null); + events.Rendering.RaiseEmpty(); + spriteBatch.Draw(staminaRect, lightmap.Bounds, currentLocation.Name.StartsWith("UndergroundMine") ? mine.getLightingColor(gameTime) : ((!ambientLight.Equals(Color.White) && (!RainManager.Instance.isRaining || !currentLocation.isOutdoors)) ? ambientLight : outdoorLight)); + for (int i = 0; i < currentLightSources.Count; i++) + { + if (Utility.isOnScreen(currentLightSources.ElementAt(i).position, (int)((float)currentLightSources.ElementAt(i).radius * 64f * 4f))) + { + spriteBatch.Draw(currentLightSources.ElementAt(i).lightTexture, GlobalToLocal(viewport, currentLightSources.ElementAt(i).position) / (options.lightingQuality / 2), currentLightSources.ElementAt(i).lightTexture.Bounds, currentLightSources.ElementAt(i).color, 0f, new Vector2(currentLightSources.ElementAt(i).lightTexture.Bounds.Center.X, currentLightSources.ElementAt(i).lightTexture.Bounds.Center.Y), (float)currentLightSources.ElementAt(i).radius / (float)(options.lightingQuality / 2), SpriteEffects.None, 0.9f); + } + } + _spriteBatchEnd.Invoke(); + base.GraphicsDevice.SetRenderTarget((options.zoomLevel == 1f) ? null : screen); + } + if (bloomDay && bloom != null) + { + bloom.BeginDraw(); + } + base.GraphicsDevice.Clear(bgColor.GetValue()); + SetSpriteBatchBeginNextID("L"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + events.Rendering.RaiseEmpty(); + events.RenderingWorld.RaiseEmpty(); + if (background != null) + { + background.draw(spriteBatch); + } + mapDisplayDevice.BeginScene(spriteBatch); + try + { + currentLocation.Map.GetLayer("Back").Draw(mapDisplayDevice, viewport, Location.Origin, wrapAround: false, 4); + } + catch (KeyNotFoundException exception) + { + CheckToReloadGameLocationAfterDrawFail.Invoke("Back", exception); + } + currentLocation.drawWater(spriteBatch); + _farmerShadows.GetValue().Clear(); + if (currentLocation.currentEvent != null && !currentLocation.currentEvent.isFestival && currentLocation.currentEvent.farmerActors.Count > 0) + { + foreach (Farmer farmerActor in currentLocation.currentEvent.farmerActors) + { + if ((farmerActor.IsLocalPlayer && displayFarmer) || !farmerActor.hidden) + { + _farmerShadows.GetValue().Add(farmerActor); + } + } + } + else + { + foreach (Farmer farmer in currentLocation.farmers) + { + if ((farmer.IsLocalPlayer && displayFarmer) || !farmer.hidden) + { + _farmerShadows.GetValue().Add(farmer); + } + } + } + if (!currentLocation.shouldHideCharacters()) + { + if (CurrentEvent == null) + { + foreach (NPC character in currentLocation.characters) + { + if (!character.swimming && !character.HideShadow && !character.IsInvisible && !currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) + { + spriteBatch.Draw(shadowTexture, GlobalToLocal(viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, character.GetBoundingBox().Height + ((!character.IsMonster) ? 12 : 0))), shadowTexture.Bounds, Color.White, 0f, new Vector2(shadowTexture.Bounds.Center.X, shadowTexture.Bounds.Center.Y), (4f + (float)character.yJumpOffset / 40f) * (float)character.scale, SpriteEffects.None, System.Math.Max(0f, (float)character.getStandingY() / 10000f) - 1E-06f); + } + } + } + else + { + foreach (NPC actor in CurrentEvent.actors) + { + if (!actor.swimming && !actor.HideShadow && !currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) + { + spriteBatch.Draw(shadowTexture, GlobalToLocal(viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, actor.GetBoundingBox().Height + ((!actor.IsMonster) ? ((actor.Sprite.SpriteHeight <= 16) ? (-4) : 12) : 0))), shadowTexture.Bounds, Color.White, 0f, new Vector2(shadowTexture.Bounds.Center.X, shadowTexture.Bounds.Center.Y), (4f + (float)actor.yJumpOffset / 40f) * (float)actor.scale, SpriteEffects.None, System.Math.Max(0f, (float)actor.getStandingY() / 10000f) - 1E-06f); + } + } + } + foreach (Farmer farmerShadow in _farmerShadows.GetValue()) + { + if (!farmerShadow.swimming && !farmerShadow.isRidingHorse() && (currentLocation == null || !currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) + { + spriteBatch.Draw(shadowTexture, GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)), shadowTexture.Bounds, Color.White, 0f, new Vector2(shadowTexture.Bounds.Center.X, shadowTexture.Bounds.Center.Y), 4f - (((farmerShadow.running || farmerShadow.UsingTool) && farmerShadow.FarmerSprite.currentAnimationIndex > 1) ? ((float)System.Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, 0f); + } + } + } + try + { + currentLocation.Map.GetLayer("Buildings").Draw(mapDisplayDevice, viewport, Location.Origin, wrapAround: false, 4); + } + catch (KeyNotFoundException exception2) + { + CheckToReloadGameLocationAfterDrawFail.Invoke("Buildings", exception2); + } + mapDisplayDevice.EndScene(); + if (currentLocation.tapToMove.targetNPC != null) + { + spriteBatch.Draw(mouseCursors, GlobalToLocal(viewport, currentLocation.tapToMove.targetNPC.Position + new Vector2((float)(currentLocation.tapToMove.targetNPC.Sprite.SpriteWidth * 4) / 2f - 32f, currentLocation.tapToMove.targetNPC.GetBoundingBox().Height + ((!currentLocation.tapToMove.targetNPC.IsMonster) ? 12 : 0) - 32)), new Microsoft.Xna.Framework.Rectangle(194, 388, 16, 16), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 0.58f); + } + _spriteBatchEnd.Invoke(); + SetSpriteBatchBeginNextID("M"); + _spriteBatchBegin.Invoke(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + if (!currentLocation.shouldHideCharacters()) + { + if (CurrentEvent == null) + { + foreach (NPC character2 in currentLocation.characters) + { + if (!character2.swimming && !character2.HideShadow && currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character2.getTileLocation())) + { + spriteBatch.Draw(shadowTexture, GlobalToLocal(viewport, character2.Position + new Vector2((float)(character2.Sprite.SpriteWidth * 4) / 2f, character2.GetBoundingBox().Height + ((!character2.IsMonster) ? 12 : 0))), shadowTexture.Bounds, Color.White, 0f, new Vector2(shadowTexture.Bounds.Center.X, shadowTexture.Bounds.Center.Y), (4f + (float)character2.yJumpOffset / 40f) * (float)character2.scale, SpriteEffects.None, System.Math.Max(0f, (float)character2.getStandingY() / 10000f) - 1E-06f); + } + } + } + else + { + foreach (NPC actor2 in CurrentEvent.actors) + { + if (!actor2.swimming && !actor2.HideShadow && currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor2.getTileLocation())) + { + spriteBatch.Draw(shadowTexture, GlobalToLocal(viewport, actor2.Position + new Vector2((float)(actor2.Sprite.SpriteWidth * 4) / 2f, actor2.GetBoundingBox().Height + ((!actor2.IsMonster) ? 12 : 0))), shadowTexture.Bounds, Color.White, 0f, new Vector2(shadowTexture.Bounds.Center.X, shadowTexture.Bounds.Center.Y), (4f + (float)actor2.yJumpOffset / 40f) * (float)actor2.scale, SpriteEffects.None, System.Math.Max(0f, (float)actor2.getStandingY() / 10000f) - 1E-06f); + } + } + } + foreach (Farmer farmerShadow2 in _farmerShadows.GetValue()) + { + if (!farmerShadow2.swimming && !farmerShadow2.isRidingHorse() && currentLocation != null && currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow2.getTileLocation())) + { + spriteBatch.Draw(shadowTexture, GlobalToLocal(farmerShadow2.Position + new Vector2(32f, 24f)), shadowTexture.Bounds, Color.White, 0f, new Vector2(shadowTexture.Bounds.Center.X, shadowTexture.Bounds.Center.Y), 4f - (((farmerShadow2.running || farmerShadow2.UsingTool) && farmerShadow2.FarmerSprite.currentAnimationIndex > 1) ? ((float)System.Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow2.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, 0f); + } + } + } + if ((eventUp || killScreen) && !killScreen && currentLocation.currentEvent != null) + { + currentLocation.currentEvent.draw(spriteBatch); + } + if (player.currentUpgrade != null && player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && currentLocation.Name.Equals("Farm")) + { + spriteBatch.Draw(player.currentUpgrade.workerTexture, GlobalToLocal(viewport, player.currentUpgrade.positionOfCarpenter), player.currentUpgrade.getSourceRectangle(), Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, (player.currentUpgrade.positionOfCarpenter.Y + 48f) / 10000f); + } + currentLocation.draw(spriteBatch); + if (player.ActiveObject == null && (player.UsingTool || pickingTool) && player.CurrentTool != null && (!player.CurrentTool.Name.Equals("Seeds") || pickingTool)) + { + drawTool(player); + } + if (currentLocation.Name.Equals("Farm")) + { + drawFarmBuildings(); + } + if (tvStation >= 0) + { + spriteBatch.Draw(tvStationTexture, GlobalToLocal(viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle(tvStation * 24, 0, 24, 15), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); + } + if (panMode) + { + spriteBatch.Draw(fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)System.Math.Floor((double)(getOldMouseX() + viewport.X) / 64.0) * 64 - viewport.X, (int)System.Math.Floor((double)(getOldMouseY() + viewport.Y) / 64.0) * 64 - viewport.Y, 64, 64), Color.Lime * 0.75f); + foreach (Warp warp in currentLocation.warps) + { + spriteBatch.Draw(fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * 64 - viewport.X, warp.Y * 64 - viewport.Y, 64, 64), Color.Red * 0.75f); + } + } + mapDisplayDevice.BeginScene(spriteBatch); + try + { + currentLocation.Map.GetLayer("Front").Draw(mapDisplayDevice, viewport, Location.Origin, wrapAround: false, 4); + } + catch (KeyNotFoundException exception3) + { + CheckToReloadGameLocationAfterDrawFail.Invoke("Front", exception3); + } + mapDisplayDevice.EndScene(); + currentLocation.drawAboveFrontLayer(spriteBatch); + if (currentLocation.tapToMove.targetNPC == null && (displayHUD || eventUp) && currentBillboard == 0 && gameMode == 3 && !freezeControls && !panMode && !HostPaused) + { + DrawTapToMoveTarget.Invoke(); + } + _spriteBatchEnd.Invoke(); + SetSpriteBatchBeginNextID("N"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + if (displayFarmer && player.ActiveObject != null && (bool)player.ActiveObject.bigCraftable && checkBigCraftableBoundariesForFrontLayer() && currentLocation.Map.GetLayer("Front").PickTile(new Location(player.getStandingX(), player.getStandingY()), viewport.Size) == null) + { + drawPlayerHeldObject(player); + } + else if (displayFarmer && player.ActiveObject != null && ((currentLocation.Map.GetLayer("Front").PickTile(new Location((int)player.Position.X, (int)player.Position.Y - 38), viewport.Size) != null && !currentLocation.Map.GetLayer("Front").PickTile(new Location((int)player.Position.X, (int)player.Position.Y - 38), viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) || (currentLocation.Map.GetLayer("Front").PickTile(new Location(player.GetBoundingBox().Right, (int)player.Position.Y - 38), viewport.Size) != null && !currentLocation.Map.GetLayer("Front").PickTile(new Location(player.GetBoundingBox().Right, (int)player.Position.Y - 38), viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")))) + { + drawPlayerHeldObject(player); + } + if ((player.UsingTool || pickingTool) && player.CurrentTool != null && (!player.CurrentTool.Name.Equals("Seeds") || pickingTool) && currentLocation.Map.GetLayer("Front").PickTile(new Location(player.getStandingX(), (int)player.Position.Y - 38), viewport.Size) != null && currentLocation.Map.GetLayer("Front").PickTile(new Location(player.getStandingX(), player.getStandingY()), viewport.Size) == null) + { + drawTool(player); + } + if (currentLocation.Map.GetLayer("AlwaysFront") != null) + { + mapDisplayDevice.BeginScene(spriteBatch); + try + { + currentLocation.Map.GetLayer("AlwaysFront").Draw(mapDisplayDevice, viewport, Location.Origin, wrapAround: false, 4); + } + catch (KeyNotFoundException exception4) + { + CheckToReloadGameLocationAfterDrawFail.Invoke("AlwaysFront", exception4); + } + mapDisplayDevice.EndScene(); + } + if (toolHold > 400f && player.CurrentTool.UpgradeLevel >= 1 && player.canReleaseTool) + { + Color color = Color.White; + switch ((int)(toolHold / 600f)) + { + case -1: + color = Tool.copperColor; + break; + case 0: + color = Tool.steelColor; + break; + case 1: + color = Tool.goldColor; + break; + case 2: + color = Tool.iridiumColor; + break; + } + spriteBatch.Draw(littleEffect, new Microsoft.Xna.Framework.Rectangle((int)player.getLocalPosition(viewport).X - 2, (int)player.getLocalPosition(viewport).Y - ((!player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0) - 2, (int)(toolHold % 600f * 0.08f) + 4, 12), Color.Black); + spriteBatch.Draw(littleEffect, new Microsoft.Xna.Framework.Rectangle((int)player.getLocalPosition(viewport).X, (int)player.getLocalPosition(viewport).Y - ((!player.CurrentTool.Name.Equals("Watering Can")) ? 64 : 0), (int)(toolHold % 600f * 0.08f), 8), color); + } + if (WeatherDebrisManager.Instance.isDebrisWeather && currentLocation.IsOutdoors && !currentLocation.ignoreDebrisWeather && !currentLocation.Name.Equals("Desert")) + { + WeatherDebrisManager.Instance.Draw(spriteBatch); + } + if (farmEvent != null) + { + farmEvent.draw(spriteBatch); + } + if (currentLocation.LightLevel > 0f && timeOfDay < 2000) + { + spriteBatch.Draw(fadeToBlackRect, graphics.GraphicsDevice.Viewport.Bounds, Color.Black * currentLocation.LightLevel); + } + if (screenGlow) + { + spriteBatch.Draw(fadeToBlackRect, graphics.GraphicsDevice.Viewport.Bounds, screenGlowColor * screenGlowAlpha); + } + currentLocation.drawAboveAlwaysFrontLayer(spriteBatch); + if (player.CurrentTool != null && player.CurrentTool is FishingRod && ((player.CurrentTool as FishingRod).isTimingCast || (player.CurrentTool as FishingRod).castingChosenCountdown > 0f || (player.CurrentTool as FishingRod).fishCaught || (player.CurrentTool as FishingRod).showingTreasure)) + { + player.CurrentTool.draw(spriteBatch); + } + if (RainManager.Instance.isRaining && currentLocation.IsOutdoors && !currentLocation.Name.Equals("Desert") && !(currentLocation is Summit) && (!eventUp || currentLocation.isTileOnMap(new Vector2(viewport.X / 64, viewport.Y / 64)))) + { + RainManager.Instance.Draw(spriteBatch); + } + _spriteBatchEnd.Invoke(); + SetSpriteBatchBeginNextID("O"); + _spriteBatchBegin.Invoke(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + if (eventUp && currentLocation.currentEvent != null) + { + currentLocation.currentEvent.drawAboveAlwaysFrontLayer(spriteBatch); + foreach (NPC actor3 in currentLocation.currentEvent.actors) + { + if (actor3.isEmoting) + { + Vector2 localPosition = actor3.getLocalPosition(viewport); + localPosition.Y -= 140f; + if (actor3.Age == 2) + { + localPosition.Y += 32f; + } + else if (actor3.Gender == 1) + { + localPosition.Y += 10f; + } + spriteBatch.Draw(emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle(actor3.CurrentEmoteIndex * 16 % emoteSpriteSheet.Width, actor3.CurrentEmoteIndex * 16 / emoteSpriteSheet.Width * 16, 16, 16), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor3.getStandingY() / 10000f); + } + } + } + _spriteBatchEnd.Invoke(); + if (drawLighting) + { + SetSpriteBatchBeginNextID("P"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, lightingBlend.GetValue(), SamplerState.LinearClamp, null, null, null, null); + spriteBatch.Draw(lightmap, Vector2.Zero, lightmap.Bounds, Color.White, 0f, Vector2.Zero, options.lightingQuality / 2, SpriteEffects.None, 1f); + if (RainManager.Instance.isRaining && (bool)currentLocation.isOutdoors && !(currentLocation is Desert)) + { + spriteBatch.Draw(staminaRect, graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); + } + _spriteBatchEnd.Invoke(); + } + SetSpriteBatchBeginNextID("Q"); + _spriteBatchBegin.Invoke(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null, null); + events.RenderedWorld.RaiseEmpty(); + if (drawGrid) + { + int num = -viewport.X % 64; + float num2 = -viewport.Y % 64; + for (int j = num; j < graphics.GraphicsDevice.Viewport.Width; j += 64) + { + spriteBatch.Draw(staminaRect, new Microsoft.Xna.Framework.Rectangle(j, (int)num2, 1, graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); + } + for (float num3 = num2; num3 < (float)graphics.GraphicsDevice.Viewport.Height; num3 += 64f) + { + spriteBatch.Draw(staminaRect, new Microsoft.Xna.Framework.Rectangle(num, (int)num3, graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); + } + } + if ((displayHUD || eventUp) && currentBillboard == 0 && gameMode == 3 && !freezeControls && !panMode && !HostPaused) + { + _drawHUD.SetValue(true); + if (isOutdoorMapSmallerThanViewport()) + { + spriteBatch.Draw(fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(0, 0, -System.Math.Min(viewport.X, 4096), graphics.GraphicsDevice.Viewport.Height), Color.Black); + spriteBatch.Draw(fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(-viewport.X + currentLocation.map.Layers[0].LayerWidth * 64, 0, System.Math.Min(4096, graphics.GraphicsDevice.Viewport.Width - (-viewport.X + currentLocation.map.Layers[0].LayerWidth * 64)), graphics.GraphicsDevice.Viewport.Height), Color.Black); + } + DrawGreenPlacementBounds.Invoke(); + } + } + if (farmEvent != null) + { + farmEvent.draw(spriteBatch); + } + if (dialogueUp && !nameSelectUp && !messagePause && (activeClickableMenu == null || !(activeClickableMenu is DialogueBox))) + { + drawDialogueBox(); + } + if (progressBar) + { + spriteBatch.Draw(fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - dialogueWidth) / 2, graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, dialogueWidth, 32), Color.LightGray); + spriteBatch.Draw(staminaRect, new Microsoft.Xna.Framework.Rectangle((graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - dialogueWidth) / 2, graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)(pauseAccumulator / pauseTime * (float)dialogueWidth), 32), Color.DimGray); + } + if (RainManager.Instance.isRaining && currentLocation != null && (bool)currentLocation.isOutdoors && !(currentLocation is Desert)) + { + spriteBatch.Draw(staminaRect, graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f); + } + if ((messagePause || globalFade) && dialogueUp) + { + drawDialogueBox(); + } + foreach (TemporaryAnimatedSprite screenOverlayTempSprite in screenOverlayTempSprites) + { + screenOverlayTempSprite.draw(spriteBatch, localPosition: true); + } + if (debugMode) + { + System.Text.StringBuilder debugStringBuilder = _debugStringBuilder.GetValue(); + debugStringBuilder.Clear(); + if (panMode) + { + debugStringBuilder.Append((getOldMouseX() + viewport.X) / 64); + debugStringBuilder.Append(","); + debugStringBuilder.Append((getOldMouseY() + viewport.Y) / 64); + } + else + { + debugStringBuilder.Append("player: "); + debugStringBuilder.Append(player.getStandingX() / 64); + debugStringBuilder.Append(", "); + debugStringBuilder.Append(player.getStandingY() / 64); + } + debugStringBuilder.Append(" mouseTransparency: "); + debugStringBuilder.Append(mouseCursorTransparency); + debugStringBuilder.Append(" mousePosition: "); + debugStringBuilder.Append(getMouseX()); + debugStringBuilder.Append(","); + debugStringBuilder.Append(getMouseY()); + debugStringBuilder.Append(System.Environment.NewLine); + debugStringBuilder.Append("debugOutput: "); + debugStringBuilder.Append(debugOutput); + spriteBatch.DrawString(smallFont, debugStringBuilder, new Vector2(base.GraphicsDevice.Viewport.GetTitleSafeArea().X, base.GraphicsDevice.Viewport.GetTitleSafeArea().Y + smallFont.LineSpacing * 8), Color.Red, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.09999999f); + } + if (showKeyHelp) + { + spriteBatch.DrawString(smallFont, keyHelpString, new Vector2(64f, (float)(viewport.Height - 64 - (dialogueUp ? (192 + (isQuestion ? (questionChoices.Count * 64) : 0)) : 0)) - smallFont.MeasureString(keyHelpString).Y), Color.LightGray, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + } + if (activeClickableMenu != null) + { + _drawActiveClickableMenu.SetValue(true); + events.RenderingActiveMenu.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPreRenderGuiEvent.Raise(); +#endif + if (activeClickableMenu is CarpenterMenu) + { + ((CarpenterMenu)activeClickableMenu).DrawPlacementSquares(spriteBatch); + } + else if (activeClickableMenu is MuseumMenu) + { + ((MuseumMenu)activeClickableMenu).DrawPlacementGrid(spriteBatch); + } + if (!IsActiveClickableMenuUnscaled && !IsActiveClickableMenuNativeScaled) + { + activeClickableMenu.draw(spriteBatch); + } + events.RenderedActiveMenu.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderGuiEvent.Raise(); +#endif + } + else if (farmEvent != null) + { + farmEvent.drawAboveEverything(spriteBatch); + } + if (HostPaused) + { + string s = content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); + SpriteText.drawStringWithScrollBackground(spriteBatch, s, 96, 32); + } + events.Rendered.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderEvent.Raise(); +#endif + _spriteBatchEnd.Invoke(); + drawOverlays(spriteBatch); + renderScreenBuffer(BlendState.Opaque); + if (_drawHUD.GetValue()) + { + DrawDayTimeMoneyBox.Invoke(); + SetSpriteBatchBeginNextID("A-C"); + SpriteBatchBegin.Invoke(1f); + events.RenderingHud.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPreRenderHudEvent.Raise(); +#endif + DrawHUD(); + if (currentLocation != null && !(activeClickableMenu is GameMenu) && !(activeClickableMenu is QuestLog)) + { + currentLocation.drawAboveAlwaysFrontLayerText(spriteBatch); + } + DrawAfterMap.Invoke(); + events.RenderedHud.RaiseEmpty(); +#if !SMAPI_3_0_STRICT + events.Legacy_OnPostRenderHudEvent.Raise(); +#endif + _spriteBatchEnd.Invoke(); + if (tutorialManager != null) + { + SetSpriteBatchBeginNextID("A-D"); + SpriteBatchBegin.Invoke(options.zoomLevel); + tutorialManager.draw(spriteBatch); + _spriteBatchEnd.Invoke(); + } + DrawToolbar.Invoke(); + DrawVirtualJoypad.Invoke(); + } + DrawFadeToBlackFullScreenRect.Invoke(); + SetSpriteBatchBeginNextID("A-E"); + SpriteBatchBegin.Invoke(1f); + DrawChatBox.Invoke(); + _spriteBatchEnd.Invoke(); + if (_drawActiveClickableMenu.GetValue()) + { + DrawDialogueBoxForPinchZoom.Invoke(); + DrawUnscaledActiveClickableMenuForPinchZoom.Invoke(); + DrawNativeScaledActiveClickableMenuForPinchZoom.Invoke(); + } + if (_drawHUD.GetValue() && hudMessages.Count > 0 && (!eventUp || isFestival())) + { + SetSpriteBatchBeginNextID("A-F"); + SpriteBatchBegin.Invoke(NativeZoomLevel); + DrawHUDMessages.Invoke(); + _spriteBatchEnd.Invoke(); + } + if (CurrentEvent != null && CurrentEvent.skippable && (activeClickableMenu == null || (activeClickableMenu != null && !(activeClickableMenu is MenuWithInventory)))) + { + SetSpriteBatchBeginNextID("A-G"); + SpriteBatchBegin.Invoke(NativeZoomLevel); + CurrentEvent.DrawSkipButton(spriteBatch); + _spriteBatchEnd.Invoke(); + } + DrawTutorialUI.Invoke(); + } + } + + /**** + ** Methods + ****/ +#if !SMAPI_3_0_STRICT + /// Raise the if there are any listeners. + /// Whether to create a new sprite batch. + private void RaisePostRender(bool needsNewBatch = false) + { + if (this.Events.Legacy_OnPostRenderEvent.HasListeners()) + { + if (needsNewBatch) + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + this.Events.Legacy_OnPostRenderEvent.Raise(); + if (needsNewBatch) + Game1.spriteBatch.End(); + } + } +#endif + } +} diff --git a/src/StardewModdingAPI/Framework/SGameConstructorHack.cs b/src/StardewModdingAPI/Framework/SGameConstructorHack.cs new file mode 100644 index 00000000..494bab99 --- /dev/null +++ b/src/StardewModdingAPI/Framework/SGameConstructorHack.cs @@ -0,0 +1,37 @@ +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// The static state to use while is initialising, which happens before the constructor runs. + internal class SGameConstructorHack + { + /********* + ** Accessors + *********/ + /// Encapsulates monitoring and logging. + public IMonitor Monitor { get; } + + /// Simplifies access to private game code. + public Reflector Reflection { get; } + + /// Encapsulates SMAPI's JSON file parsing. + public JsonHelper JsonHelper { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging. + /// Simplifies access to private game code. + /// Encapsulates SMAPI's JSON file parsing. + public SGameConstructorHack(IMonitor monitor, Reflector reflection, JsonHelper jsonHelper) + { + this.Monitor = monitor; + this.Reflection = reflection; + this.JsonHelper = jsonHelper; + } + } +} diff --git a/src/StardewModdingAPI/Framework/SModHooks.cs b/src/StardewModdingAPI/Framework/SModHooks.cs new file mode 100644 index 00000000..7dafc746 --- /dev/null +++ b/src/StardewModdingAPI/Framework/SModHooks.cs @@ -0,0 +1,34 @@ +using System; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// Invokes callbacks for mod hooks provided by the game. + internal class SModHooks : ModHooks + { + /********* + ** Fields + *********/ + /// A callback to invoke before runs. + private readonly Action BeforeNewDayAfterFade; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A callback to invoke before runs. + public SModHooks(Action beforeNewDayAfterFade) + { + this.BeforeNewDayAfterFade = beforeNewDayAfterFade; + } + + /// A hook invoked when is called. + /// The vanilla logic. + public override void OnGame1_NewDayAfterFade(Action action) + { + this.BeforeNewDayAfterFade?.Invoke(); + action(); + } + } +} diff --git a/src/StardewModdingAPI/Framework/SMultiplayer.cs b/src/StardewModdingAPI/Framework/SMultiplayer.cs new file mode 100644 index 00000000..a3ff0e3b --- /dev/null +++ b/src/StardewModdingAPI/Framework/SMultiplayer.cs @@ -0,0 +1,532 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +//using Galaxy.Api; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Events; +using StardewModdingAPI.Framework.Networking; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewValley; +using StardewValley.Network; +using StardewValley.SDKs; + +namespace StardewModdingAPI.Framework +{ + /// SMAPI's implementation of the game's core multiplayer logic. + /// + /// SMAPI syncs mod context to all players through the host as such: + /// 1. Farmhand sends ModContext + PlayerIntro. + /// 2. If host receives ModContext: it stores the context, replies with known contexts, and forwards it to other farmhands. + /// 3. If host receives PlayerIntro before ModContext: it stores a 'vanilla player' context, and forwards it to other farmhands. + /// 4. If farmhand receives ModContext: it stores it. + /// 5. If farmhand receives ServerIntro without a preceding ModContext: it stores a 'vanilla host' context. + /// 6. If farmhand receives PlayerIntro without a preceding ModContext AND it's not the host peer: it stores a 'vanilla player' context. + /// + /// Once a farmhand/server stored a context, messages can be sent to that player through the SMAPI APIs. + /// + internal class SMultiplayer : Multiplayer + { + /********* + ** Fields + *********/ + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + /// Tracks the installed mods. + private readonly ModRegistry ModRegistry; + + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; + + /// Simplifies access to private code. + private readonly Reflector Reflection; + + /// Manages SMAPI events. + private readonly EventManager EventManager; + + /// A callback to invoke when a mod message is received. + private readonly Action OnModMessageReceived; + + + /********* + ** Accessors + *********/ + /// The metadata for each connected peer. + public IDictionary Peers { get; } = new Dictionary(); + + /// The metadata for the host player, if the current player is a farmhand. + public MultiplayerPeer HostPeer; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Encapsulates monitoring and logging. + /// Manages SMAPI events. + /// Encapsulates SMAPI's JSON file parsing. + /// Tracks the installed mods. + /// Simplifies access to private code. + /// A callback to invoke when a mod message is received. + public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, Action onModMessageReceived) + { + this.Monitor = monitor; + this.EventManager = eventManager; + this.JsonHelper = jsonHelper; + this.ModRegistry = modRegistry; + this.Reflection = reflection; + this.OnModMessageReceived = onModMessageReceived; + } + + /// Perform cleanup needed when a multiplayer session ends. + public void CleanupOnMultiplayerExit() + { + this.Peers.Clear(); + this.HostPeer = null; + } + +#if !SMAPI_3_0_STRICT + /// Handle sync messages from other players and perform other initial sync logic. + public override void UpdateEarly() + { + this.EventManager.Legacy_BeforeMainSync.Raise(); + base.UpdateEarly(); + this.EventManager.Legacy_AfterMainSync.Raise(); + } + + /// Broadcast sync messages to other players and perform other final sync logic. + public override void UpdateLate(bool forceSync = false) + { + this.EventManager.Legacy_BeforeMainBroadcast.Raise(); + base.UpdateLate(forceSync); + this.EventManager.Legacy_AfterMainBroadcast.Raise(); + } +#endif + + /// Initialise a client before the game connects to a remote server. + /// The client to initialise. + public override Client InitClient(Client client) + { + switch (client) + { + //case LidgrenClient _: + // { + // string address = this.Reflection.GetField(client, "address").GetValue(); + // return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); + // } + + //case GalaxyNetClient _: + // { + // GalaxyID address = this.Reflection.GetField(client, "lobbyId").GetValue(); + // return new SGalaxyNetClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage); + // } + + default: + this.Monitor.Log($"Unknown multiplayer client type: {client.GetType().AssemblyQualifiedName}", LogLevel.Trace); + return client; + } + } + + /// Initialise a server before the game connects to an incoming player. + /// The server to initialise. + public override Server InitServer(Server server) + { + switch (server) + { + //case LidgrenServer _: + // { + // IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + // return new SLidgrenServer(gameServer, this, this.OnServerProcessingMessage); + // } + + //case GalaxyNetServer _: + // { + // IGameServer gameServer = this.Reflection.GetField(server, "gameServer").GetValue(); + // return new SGalaxyNetServer(gameServer, this, this.OnServerProcessingMessage); + // } + + default: + this.Monitor.Log($"Unknown multiplayer server type: {server.GetType().AssemblyQualifiedName}", LogLevel.Trace); + return server; + } + } + + /// A callback raised when sending a message as a farmhand. + /// The message being sent. + /// Send an arbitrary message through the client. + /// Resume sending the underlying message. + protected void OnClientSendingMessage(OutgoingMessage message, Action sendMessage, Action resume) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + + switch (message.MessageType) + { + // sync mod context (step 1) + case (byte)MessageType.PlayerIntroduction: + sendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); + resume(); + break; + + // run default logic + default: + resume(); + break; + } + } + + /// Process an incoming network message as the host player. + /// The message to process. + /// A method which sends the given message to the client. + /// Process the message using the game's default logic. + public void OnServerProcessingMessage(IncomingMessage message, Action sendMessage, Action resume) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + + switch (message.MessageType) + { + // sync mod context (step 2) + case (byte)MessageType.ModContext: + { + // parse message + 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")}.", LogLevel.Trace); + + // store peer + MultiplayerPeer newPeer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: false); + if (this.Peers.ContainsKey(message.FarmerID)) + { + this.Monitor.Log($"Rejected mod context from farmhand {message.FarmerID}: already received context for that player.", LogLevel.Error); + return; + } + this.AddPeer(newPeer, canBeHost: false, raiseEvent: false); + + // reply with own context + this.Monitor.VerboseLog(" Replying with host context..."); + newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields())); + + // reply with other players' context + foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + { + this.Monitor.VerboseLog($" Replying with context for player {otherPeer.PlayerID}..."); + newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer))); + } + + // forward to other peers + if (this.Peers.Count > 1) + { + object[] fields = this.GetContextSyncMessageFields(newPeer); + foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID)) + { + this.Monitor.VerboseLog($" Forwarding context to player {otherPeer.PlayerID}..."); + otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields)); + } + } + + // raise event + this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(newPeer)); + } + break; + + // handle player intro + case (byte)MessageType.PlayerIntroduction: + // store peer if new + if (!this.Peers.ContainsKey(message.FarmerID)) + { + this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace); + MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: false); + this.AddPeer(peer, canBeHost: false); + } + + resume(); + break; + + // handle mod message + case (byte)MessageType.ModMessage: + this.ReceiveModMessage(message); + break; + + default: + resume(); + break; + } + } + + /// Process an incoming network message as a farmhand. + /// The message to process. + /// Send an arbitrary message through the client. + /// Resume processing the message using the game's default logic. + /// Returns whether the message was handled. + public void OnClientProcessingMessage(IncomingMessage message, Action sendMessage, Action resume) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace); + + switch (message.MessageType) + { + // mod context sync (step 4) + case (byte)MessageType.ModContext: + { + // parse message + 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")}.", LogLevel.Trace); + + // store peer + MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: model?.IsHost ?? this.HostPeer == null); + if (peer.IsHost && this.HostPeer != null) + { + this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error); + return; + } + this.AddPeer(peer, canBeHost: true); + } + break; + + // handle server intro + case (byte)MessageType.ServerIntroduction: + { + // store peer + if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null) + { + this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace); + this.AddPeer(new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: true), canBeHost: false); + } + resume(); + break; + } + + // handle player intro + case (byte)MessageType.PlayerIntroduction: + { + // store peer + if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer)) + { + peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: this.HostPeer == null); + this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace); + this.AddPeer(peer, canBeHost: true); + } + + resume(); + break; + } + + // handle mod message + case (byte)MessageType.ModMessage: + this.ReceiveModMessage(message); + break; + + default: + resume(); + break; + } + } + + /// Remove players who are disconnecting. + protected override void removeDisconnectedFarmers() + { + //foreach (long playerID in this.disconnectingFarmers) + //{ + // if (this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) + // { + // this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace); + // this.Peers.Remove(playerID); + // this.EventManager.PeerDisconnected.Raise(new PeerDisconnectedEventArgs(peer)); + // } + //} + + base.removeDisconnectedFarmers(); + } + + /// Broadcast a mod message to matching players. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The unique ID of the mod sending the message. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + public void BroadcastModMessage(TMessage message, string messageType, string fromModID, string[] toModIDs, long[] toPlayerIDs) + { + // validate + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (string.IsNullOrWhiteSpace(messageType)) + throw new ArgumentNullException(nameof(messageType)); + if (string.IsNullOrWhiteSpace(fromModID)) + throw new ArgumentNullException(nameof(fromModID)); + if (!this.Peers.Any()) + { + this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players."); + return; + } + + // filter player IDs + HashSet playerIDs = null; + if (toPlayerIDs != null && toPlayerIDs.Any()) + { + playerIDs = new HashSet(toPlayerIDs); + playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id)); + if (!playerIDs.Any()) + { + this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected."); + return; + } + } + + // get data to send + ModMessageModel model = new ModMessageModel( + fromPlayerID: Game1.player.UniqueMultiplayerID, + fromModID: fromModID, + toModIDs: toModIDs, + toPlayerIDs: playerIDs?.ToArray(), + type: messageType, + data: JToken.FromObject(message) + ); + string data = JsonConvert.SerializeObject(model, Formatting.None); + + // log message + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace); + + // send message + if (Context.IsMainPlayer) + { + foreach (MultiplayerPeer peer in this.Peers.Values) + { + if (playerIDs == null || playerIDs.Contains(peer.PlayerID)) + { + model.ToPlayerIDs = new[] { peer.PlayerID }; + peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data)); + } + } + } + else if (this.HostPeer != null && this.HostPeer.HasSmapi) + this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data)); + else + this.Monitor.VerboseLog(" Can't send message because no valid connections were found."); + + } + + + /********* + ** Private methods + *********/ + /// Save a received peer. + /// The peer to add. + /// Whether to track the peer as the host if applicable. + /// Whether to raise the event. + private void AddPeer(MultiplayerPeer peer, bool canBeHost, bool raiseEvent = true) + { + // store + this.Peers[peer.PlayerID] = peer; + if (canBeHost && peer.IsHost) + this.HostPeer = peer; + + // raise event + if (raiseEvent) + this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(peer)); + } + + /// Read the metadata context for a player. + /// The stream reader. + private RemoteContextModel ReadContext(BinaryReader reader) + { + string data = reader.ReadString(); + RemoteContextModel model = this.JsonHelper.Deserialise(data); + return model.ApiVersion != null + ? model + : null; // no data available for unmodded players + } + + /// Receive a mod message sent from another player's mods. + /// The raw message to parse. + private void ReceiveModMessage(IncomingMessage message) + { + // parse message + string json = message.Reader.ReadString(); + ModMessageModel model = this.JsonHelper.Deserialise(json); + HashSet playerIDs = new HashSet(model.ToPlayerIDs ?? this.GetKnownPlayerIDs()); + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Received message: {json}."); + + // notify local mods + if (playerIDs.Contains(Game1.player.UniqueMultiplayerID)) + this.OnModMessageReceived(model); + + // forward to other players + if (Context.IsMainPlayer && playerIDs.Any(p => p != Game1.player.UniqueMultiplayerID)) + { + ModMessageModel newModel = new ModMessageModel(model); + foreach (long playerID in playerIDs) + { + if (playerID != Game1.player.UniqueMultiplayerID && playerID != model.FromPlayerID && this.Peers.TryGetValue(playerID, out MultiplayerPeer peer)) + { + newModel.ToPlayerIDs = new[] { peer.PlayerID }; + this.Monitor.VerboseLog($" Forwarding message to player {peer.PlayerID}."); + peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None))); + } + } + } + } + + /// Get all connected player IDs, including the current player. + private IEnumerable GetKnownPlayerIDs() + { + yield return Game1.player.UniqueMultiplayerID; + foreach (long peerID in this.Peers.Keys) + yield return peerID; + } + + /// Get the fields to include in a context sync message sent to other players. + private object[] GetContextSyncMessageFields() + { + RemoteContextModel model = new RemoteContextModel + { + 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 + }) + .ToArray() + }; + + return new object[] { this.JsonHelper.Serialise(model, Formatting.None) }; + } + + /// Get the fields to include in a context sync message sent to other players. + /// The peer whose data to represent. + private object[] GetContextSyncMessageFields(IMultiplayerPeer peer) + { + if (!peer.HasSmapi) + return new object[] { "{}" }; + + RemoteContextModel model = new RemoteContextModel + { + 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.Serialise(model, Formatting.None) }; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Serialisation/ColorConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ColorConverter.cs new file mode 100644 index 00000000..c27065bf --- /dev/null +++ b/src/StardewModdingAPI/Framework/Serialisation/ColorConverter.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "B": 76, "G": 51, "R": 25, "A": 102 } + /// - Windows format: "26, 51, 76, 102" + /// + internal class ColorConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Color ReadObject(JObject obj, string path) + { + int r = obj.ValueIgnoreCase(nameof(Color.R)); + int g = obj.ValueIgnoreCase(nameof(Color.G)); + int b = obj.ValueIgnoreCase(nameof(Color.B)); + int a = obj.ValueIgnoreCase(nameof(Color.A)); + return new Color(r, g, b, a); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Color ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 4) + throw new SParseException($"Can't parse {typeof(Color).Name} from invalid value '{str}' (path: {path})."); + + int r = Convert.ToInt32(parts[0]); + int g = Convert.ToInt32(parts[1]); + int b = Convert.ToInt32(parts[2]); + int a = Convert.ToInt32(parts[3]); + return new Color(r, g, b, a); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Serialisation/PointConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/PointConverter.cs new file mode 100644 index 00000000..fbc857d2 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Serialisation/PointConverter.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "X": 1, "Y": 2 } + /// - Windows format: "1, 2" + /// + internal class PointConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Point ReadObject(JObject obj, string path) + { + int x = obj.ValueIgnoreCase(nameof(Point.X)); + int y = obj.ValueIgnoreCase(nameof(Point.Y)); + return new Point(x, y); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Point ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 2) + throw new SParseException($"Can't parse {typeof(Point).Name} from invalid value '{str}' (path: {path})."); + + int x = Convert.ToInt32(parts[0]); + int y = Convert.ToInt32(parts[1]); + return new Point(x, y); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Serialisation/RectangleConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/RectangleConverter.cs new file mode 100644 index 00000000..4f55cc32 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Serialisation/RectangleConverter.cs @@ -0,0 +1,52 @@ +using System; +using System.Text.RegularExpressions; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Handles deserialisation of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "X": 1, "Y": 2, "Width": 3, "Height": 4 } + /// - Windows format: "{X:1 Y:2 Width:3 Height:4}" + /// + internal class RectangleConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Rectangle ReadObject(JObject obj, string path) + { + int x = obj.ValueIgnoreCase(nameof(Rectangle.X)); + int y = obj.ValueIgnoreCase(nameof(Rectangle.Y)); + int width = obj.ValueIgnoreCase(nameof(Rectangle.Width)); + int height = obj.ValueIgnoreCase(nameof(Rectangle.Height)); + return new Rectangle(x, y, width, height); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Rectangle ReadString(string str, string path) + { + if (string.IsNullOrWhiteSpace(str)) + return Rectangle.Empty; + + var match = Regex.Match(str, @"^\{X:(?\d+) Y:(?\d+) Width:(?\d+) Height:(?\d+)\}$", RegexOptions.IgnoreCase); + if (!match.Success) + throw new SParseException($"Can't parse {typeof(Rectangle).Name} from invalid value '{str}' (path: {path})."); + + int x = Convert.ToInt32(match.Groups["x"].Value); + int y = Convert.ToInt32(match.Groups["y"].Value); + int width = Convert.ToInt32(match.Groups["width"].Value); + int height = Convert.ToInt32(match.Groups["height"].Value); + + return new Rectangle(x, y, width, height); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Singleton.cs b/src/StardewModdingAPI/Framework/Singleton.cs new file mode 100644 index 00000000..399a8bf0 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Singleton.cs @@ -0,0 +1,10 @@ +namespace StardewModdingAPI.Framework +{ + /// Provides singleton instances of a given type. + /// The instance type. + internal static class Singleton where T : new() + { + /// The singleton instance. + public static T Instance { get; } = new T(); + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/Comparers/EquatableComparer.cs b/src/StardewModdingAPI/Framework/StateTracking/Comparers/EquatableComparer.cs new file mode 100644 index 00000000..a96ffdb6 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/Comparers/EquatableComparer.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.StateTracking.Comparers +{ + /// Compares instances using . + /// The value type. + internal class EquatableComparer : IEqualityComparer where T : IEquatable + { + /********* + ** Public methods + *********/ + /// Determines whether the specified objects are equal. + /// true if the specified objects are equal; otherwise, false. + /// The first object to compare. + /// The second object to compare. + public bool Equals(T x, T y) + { + if (x == null) + return y == null; + return x.Equals(y); + } + + /// Get a hash code for the specified object. + /// The value. + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs b/src/StardewModdingAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs new file mode 100644 index 00000000..cc1d6553 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/Comparers/GenericEqualsComparer.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.StateTracking.Comparers +{ + /// Compares values using their method. This should only be used when won't work, since this doesn't validate whether they're comparable. + /// The value type. + internal class GenericEqualsComparer : IEqualityComparer + { + /********* + ** Public methods + *********/ + /// Determines whether the specified objects are equal. + /// true if the specified objects are equal; otherwise, false. + /// The first object to compare. + /// The second object to compare. + public bool Equals(T x, T y) + { + if (x == null) + return y == null; + return x.Equals(y); + } + + /// Get a hash code for the specified object. + /// The value. + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs b/src/StardewModdingAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs new file mode 100644 index 00000000..ef9adafb --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/Comparers/ObjectReferenceComparer.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace StardewModdingAPI.Framework.StateTracking.Comparers +{ + /// A comparer which considers two references equal if they point to the same instance. + /// The value type. + internal class ObjectReferenceComparer : IEqualityComparer + { + /********* + ** Public methods + *********/ + /// Determines whether the specified objects are equal. + /// true if the specified objects are equal; otherwise, false. + /// The first object to compare. + /// The second object to compare. + public bool Equals(T x, T y) + { + return object.ReferenceEquals(x, y); + } + + /// Get a hash code for the specified object. + /// The value. + public int GetHashCode(T obj) + { + return RuntimeHelpers.GetHashCode(obj); + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs new file mode 100644 index 00000000..60006c51 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/BaseDisposableWatcher.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// The base implementation for a disposable watcher. + internal abstract class BaseDisposableWatcher : IDisposable + { + /********* + ** Fields + *********/ + /// Whether the watcher has been disposed. + protected bool IsDisposed { get; private set; } + + + /********* + ** Public methods + *********/ + /// Stop watching the field and release all references. + public virtual void Dispose() + { + this.IsDisposed = true; + } + + + /********* + ** Protected methods + *********/ + /// Throw an exception if the watcher is disposed. + /// The watcher is disposed. + protected void AssertNotDisposed() + { + if (this.IsDisposed) + throw new ObjectDisposedException(this.GetType().Name); + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs new file mode 100644 index 00000000..6550f950 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a collection of values using a specified instance. + /// The value type within the collection. + internal class ComparableListWatcher : BaseDisposableWatcher, ICollectionWatcher + { + /********* + ** Fields + *********/ + /// The collection to watch. + private readonly ICollection CurrentValues; + + /// The values during the previous update. + private HashSet LastValues; + + /// The pairs added since the last reset. + private readonly List AddedImpl = new List(); + + /// The pairs removed since the last reset. + private readonly List RemovedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// The values added since the last reset. + public IEnumerable Added => this.AddedImpl; + + /// The values removed since the last reset. + public IEnumerable Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection to watch. + /// The equality comparer which indicates whether two values are the same. + public ComparableListWatcher(ICollection values, IEqualityComparer comparer) + { + this.CurrentValues = values; + this.LastValues = new HashSet(comparer); + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + + // optimise for zero items + if (this.CurrentValues.Count == 0) + { + if (this.LastValues.Count > 0) + { + this.AddedImpl.AddRange(this.LastValues); + this.LastValues.Clear(); + } + return; + } + + // detect changes + HashSet curValues = new HashSet(this.CurrentValues, this.LastValues.Comparer); + this.RemovedImpl.AddRange(from value in this.LastValues where !curValues.Contains(value) select value); + this.AddedImpl.AddRange(from value in curValues where !this.LastValues.Contains(value) select value); + this.LastValues = curValues; + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs new file mode 100644 index 00000000..5ca4b9f4 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a value using a specified instance. + /// The comparable value type. + internal class ComparableWatcher : IValueWatcher + { + /********* + ** Fields + *********/ + /// Get the current value. + private readonly Func GetValue; + + /// The equality comparer. + private readonly IEqualityComparer Comparer; + + + /********* + ** Accessors + *********/ + /// The field value at the last reset. + public TValue PreviousValue { get; private set; } + + /// The latest value. + public TValue CurrentValue { get; private set; } + + /// Whether the value changed since the last reset. + public bool IsChanged { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Get the current value. + /// The equality comparer which indicates whether two values are the same. + public ComparableWatcher(Func getValue, IEqualityComparer comparer) + { + this.GetValue = getValue; + this.Comparer = comparer; + this.PreviousValue = getValue(); + } + + /// Update the current value if needed. + public void Update() + { + this.CurrentValue = this.GetValue(); + this.IsChanged = !this.Comparer.Equals(this.PreviousValue, this.CurrentValue); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.PreviousValue = this.CurrentValue; + this.IsChanged = false; + } + + /// Release any references if needed when the field is no longer needed. + public void Dispose() { } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs new file mode 100644 index 00000000..21e84c47 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a Netcode collection. + /// The value type within the collection. + internal class NetCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher + where TValue : class, INetObject + { + /********* + ** Fields + *********/ + /// The field being watched. + private readonly NetCollection Field; + + /// The pairs added since the last reset. + private readonly List AddedImpl = new List(); + + /// The pairs removed since the last reset. + private readonly List RemovedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// The values added since the last reset. + public IEnumerable Added => this.AddedImpl; + + /// The values removed since the last reset. + public IEnumerable Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetCollectionWatcher(NetCollection field) + { + this.Field = field; + field.OnValueAdded += this.OnValueAdded; + field.OnValueRemoved += this.OnValueRemoved; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnValueAdded -= this.OnValueAdded; + this.Field.OnValueRemoved -= this.OnValueRemoved; + } + + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added to the collection. + /// The added value. + private void OnValueAdded(TValue value) + { + this.AddedImpl.Add(value); + } + + /// A callback invoked when an entry is removed from the collection. + /// The added value. + private void OnValueRemoved(TValue value) + { + this.RemovedImpl.Add(value); + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs new file mode 100644 index 00000000..e6882f7e --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a net dictionary field. + /// The dictionary key type. + /// The dictionary value type. + /// The net type equivalent to . + /// The serializable dictionary type that can store the keys and values. + /// The net field instance type. + internal class NetDictionaryWatcher : BaseDisposableWatcher, IDictionaryWatcher + where TField : class, INetObject, new() + where TSerialDict : IDictionary, new() + where TSelf : NetDictionary + { + /********* + ** Fields + *********/ + /// The pairs added since the last reset. + private readonly IDictionary PairsAdded = new Dictionary(); + + /// The pairs removed since the last reset. + private readonly IDictionary PairsRemoved = new Dictionary(); + + /// The field being watched. + private readonly NetDictionary Field; + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.PairsAdded.Count > 0 || this.PairsRemoved.Count > 0; + + /// The values added since the last reset. + public IEnumerable> Added => this.PairsAdded; + + /// The values removed since the last reset. + public IEnumerable> Removed => this.PairsRemoved; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetDictionaryWatcher(NetDictionary field) + { + this.Field = field; + + field.OnValueAdded += this.OnValueAdded; + field.OnValueRemoved += this.OnValueRemoved; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.PairsAdded.Clear(); + this.PairsRemoved.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.OnValueAdded -= this.OnValueAdded; + this.Field.OnValueRemoved -= this.OnValueRemoved; + } + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added to the dictionary. + /// The entry key. + /// The entry value. + private void OnValueAdded(TKey key, TValue value) + { + this.PairsAdded[key] = value; + } + + /// A callback invoked when an entry is removed from the dictionary. + /// The entry key. + /// The entry value. + private void OnValueRemoved(TKey key, TValue value) + { + if (!this.PairsRemoved.ContainsKey(key)) + this.PairsRemoved[key] = value; + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs new file mode 100644 index 00000000..48d5d681 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs @@ -0,0 +1,85 @@ +using Netcode; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to a net value field. + /// The value type wrapped by the net field. + /// The net field type. + internal class NetValueWatcher : BaseDisposableWatcher, IValueWatcher where TNetField : NetFieldBase + { + /********* + ** Fields + *********/ + /// The field being watched. + private readonly NetFieldBase Field; + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + public bool IsChanged { get; private set; } + + /// The field value at the last reset. + public TValue PreviousValue { get; private set; } + + /// The latest value. + public TValue CurrentValue { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public NetValueWatcher(NetFieldBase field) + { + this.Field = field; + this.PreviousValue = field.Value; + this.CurrentValue = field.Value; + + field.fieldChangeVisibleEvent += this.OnValueChanged; + field.fieldChangeEvent += this.OnValueChanged; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.PreviousValue = this.CurrentValue; + this.IsChanged = false; + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + { + this.Field.fieldChangeEvent -= this.OnValueChanged; + this.Field.fieldChangeVisibleEvent -= this.OnValueChanged; + } + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when the field's value changes. + /// The field being watched. + /// The old field value. + /// The new field value. + private void OnValueChanged(TNetField field, TValue oldValue, TValue newValue) + { + this.CurrentValue = newValue; + this.IsChanged = true; + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs new file mode 100644 index 00000000..883b1023 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// A watcher which detects changes to an observable collection. + /// The value type within the collection. + internal class ObservableCollectionWatcher : BaseDisposableWatcher, ICollectionWatcher + { + /********* + ** Fields + *********/ + /// The field being watched. + private readonly ObservableCollection Field; + + /// The pairs added since the last reset. + private readonly List AddedImpl = new List(); + + /// The pairs removed since the last reset. + private readonly List RemovedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the collection changed since the last reset. + public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0; + + /// The values added since the last reset. + public IEnumerable Added => this.AddedImpl; + + /// The values removed since the last reset. + public IEnumerable Removed => this.RemovedImpl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field to watch. + public ObservableCollectionWatcher(ObservableCollection field) + { + this.Field = field; + field.CollectionChanged += this.OnCollectionChanged; + } + + /// Update the current value if needed. + public void Update() + { + this.AssertNotDisposed(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.AssertNotDisposed(); + + this.AddedImpl.Clear(); + this.RemovedImpl.Clear(); + } + + /// Stop watching the field and release all references. + public override void Dispose() + { + if (!this.IsDisposed) + this.Field.CollectionChanged -= this.OnCollectionChanged; + base.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// A callback invoked when an entry is added or removed from the collection. + /// The event sender. + /// The event arguments. + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems != null) + this.AddedImpl.AddRange(e.NewItems.Cast()); + if (e.OldItems != null) + this.RemovedImpl.AddRange(e.OldItems.Cast()); + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs new file mode 100644 index 00000000..8301351e --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Netcode; +using StardewModdingAPI.Framework.StateTracking.Comparers; + +namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers +{ + /// Provides convenience wrappers for creating watchers. + internal static class WatcherFactory + { + /********* + ** Public methods + *********/ + /// Get a watcher which compares values using their method. This method should only be used when won't work, since this doesn't validate whether they're comparable. + /// The value type. + /// Get the current value. + public static ComparableWatcher ForGenericEquality(Func getValue) where T : struct + { + return new ComparableWatcher(getValue, new GenericEqualsComparer()); + } + + /// Get a watcher for an value. + /// The value type. + /// Get the current value. + public static ComparableWatcher ForEquatable(Func getValue) where T : IEquatable + { + return new ComparableWatcher(getValue, new EquatableComparer()); + } + + /// Get a watcher which detects when an object reference changes. + /// The value type. + /// Get the current value. + public static ComparableWatcher ForReference(Func getValue) + { + return new ComparableWatcher(getValue, new ObjectReferenceComparer()); + } + + /// Get a watcher which detects when an object reference in a collection changes. + /// The value type. + /// The observable collection. + public static ComparableListWatcher ForReferenceList(ICollection collection) + { + return new ComparableListWatcher(collection, new ObjectReferenceComparer()); + } + + /// Get a watcher for an observable collection. + /// The value type. + /// The observable collection. + public static ObservableCollectionWatcher ForObservableCollection(ObservableCollection collection) + { + return new ObservableCollectionWatcher(collection); + } + + /// Get a watcher for a net collection. + /// The value type. + /// The net field instance type. + /// The net collection. + public static NetValueWatcher ForNetValue(NetFieldBase field) where TSelf : NetFieldBase + { + return new NetValueWatcher(field); + } + + /// Get a watcher for a net collection. + /// The value type. + /// The net collection. + public static NetCollectionWatcher ForNetCollection(NetCollection collection) where T : class, INetObject + { + return new NetCollectionWatcher(collection); + } + + /// Get a watcher for a net dictionary. + /// The dictionary key type. + /// The dictionary value type. + /// The net type equivalent to . + /// The serializable dictionary type that can store the keys and values. + /// The net field instance type. + /// The net field. + public static NetDictionaryWatcher ForNetDictionary(NetDictionary field) + where TField : class, INetObject, new() + where TSerialDict : IDictionary, new() + where TSelf : NetDictionary + { + return new NetDictionaryWatcher(field); + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/ICollectionWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/ICollectionWatcher.cs new file mode 100644 index 00000000..7a7759e3 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/ICollectionWatcher.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a collection. + internal interface ICollectionWatcher : IWatcher + { + /********* + ** Accessors + *********/ + /// The values added since the last reset. + IEnumerable Added { get; } + + /// The values removed since the last reset. + IEnumerable Removed { get; } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/IDictionaryWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/IDictionaryWatcher.cs new file mode 100644 index 00000000..691ed377 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/IDictionaryWatcher.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a dictionary. + internal interface IDictionaryWatcher : ICollectionWatcher> { } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/IValueWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/IValueWatcher.cs new file mode 100644 index 00000000..4afca972 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/IValueWatcher.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which tracks changes to a value. + internal interface IValueWatcher : IWatcher + { + /********* + ** Accessors + *********/ + /// The field value at the last reset. + T PreviousValue { get; } + + /// The latest value. + T CurrentValue { get; } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/IWatcher.cs b/src/StardewModdingAPI/Framework/StateTracking/IWatcher.cs new file mode 100644 index 00000000..8c7fa51c --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/IWatcher.cs @@ -0,0 +1,24 @@ +using System; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// A watcher which detects changes to something. + internal interface IWatcher : IDisposable + { + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + bool IsChanged { get; } + + + /********* + ** Methods + *********/ + /// Update the current value if needed. + void Update(); + + /// Set the current value as the baseline. + void Reset(); + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/LocationTracker.cs b/src/StardewModdingAPI/Framework/StateTracking/LocationTracker.cs new file mode 100644 index 00000000..8a2ac1b7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/LocationTracker.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Buildings; +using StardewValley.Locations; +using StardewValley.TerrainFeatures; +using Object = StardewValley.Object; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Tracks changes to a location's data. + internal class LocationTracker : IWatcher + { + /********* + ** Fields + *********/ + /// The underlying watchers. + private readonly List Watchers = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last reset. + public bool IsChanged => this.Watchers.Any(p => p.IsChanged); + + /// The tracked location. + public GameLocation Location { get; } + + /// Tracks added or removed buildings. + public ICollectionWatcher BuildingsWatcher { get; } + + /// Tracks added or removed debris. + public ICollectionWatcher DebrisWatcher { get; } + + /// Tracks added or removed large terrain features. + public ICollectionWatcher LargeTerrainFeaturesWatcher { get; } + + /// Tracks added or removed NPCs. + public ICollectionWatcher NpcsWatcher { get; } + + /// Tracks added or removed objects. + public IDictionaryWatcher ObjectsWatcher { get; } + + /// Tracks added or removed terrain features. + public IDictionaryWatcher TerrainFeaturesWatcher { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The location to track. + public LocationTracker(GameLocation location) + { + this.Location = location; + + // init watchers + this.BuildingsWatcher = location is BuildableGameLocation buildableLocation + ? WatcherFactory.ForNetCollection(buildableLocation.buildings) + : (ICollectionWatcher)WatcherFactory.ForObservableCollection(new ObservableCollection()); + this.DebrisWatcher = WatcherFactory.ForNetCollection(location.debris.debrisNetCollection); + this.LargeTerrainFeaturesWatcher = WatcherFactory.ForNetCollection(location.largeTerrainFeatures); + this.NpcsWatcher = WatcherFactory.ForNetCollection(location.characters); + this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects); + this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures); + + this.Watchers.AddRange(new IWatcher[] + { + this.BuildingsWatcher, + this.DebrisWatcher, + this.LargeTerrainFeaturesWatcher, + this.NpcsWatcher, + this.ObjectsWatcher, + this.TerrainFeaturesWatcher + }); + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Dispose(); + } + + /// Update the current value if needed. + public void Update() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + } + + /// Set the current value as the baseline. + public void Reset() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Reset(); + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/PlayerTracker.cs b/src/StardewModdingAPI/Framework/StateTracking/PlayerTracker.cs new file mode 100644 index 00000000..abb4fa24 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/PlayerTracker.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Enums; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Locations; +using ChangeType = StardewModdingAPI.Events.ChangeType; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Tracks changes to a player's data. + internal class PlayerTracker : IDisposable + { + /********* + ** Fields + *********/ + /// The player's inventory as of the last reset. + private IDictionary PreviousInventory; + + /// The player's inventory change as of the last update. + private IDictionary CurrentInventory; + + /// The player's last valid location. + private GameLocation LastValidLocation; + + /// The underlying watchers. + private readonly List Watchers = new List(); + + + /********* + ** Accessors + *********/ + /// The player being tracked. + public Farmer Player { get; } + + /// The player's current location. + public IValueWatcher LocationWatcher { get; } + + /// The player's current mine level. + public IValueWatcher MineLevelWatcher { get; } + + /// Tracks changes to the player's skill levels. + public IDictionary> SkillWatchers { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player to track. + public PlayerTracker(Farmer player) + { + // init player data + this.Player = player; + this.PreviousInventory = this.GetInventory(); + + // init trackers + this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); + this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); + this.SkillWatchers = new Dictionary> + { + [SkillType.Combat] = WatcherFactory.ForNetValue(player.combatLevel), + [SkillType.Farming] = WatcherFactory.ForNetValue(player.farmingLevel), + [SkillType.Fishing] = WatcherFactory.ForNetValue(player.fishingLevel), + [SkillType.Foraging] = WatcherFactory.ForNetValue(player.foragingLevel), + [SkillType.Luck] = WatcherFactory.ForNetValue(player.luckLevel), + [SkillType.Mining] = WatcherFactory.ForNetValue(player.miningLevel) + }; + + // track watchers for convenience + this.Watchers.AddRange(new IWatcher[] + { + this.LocationWatcher, + this.MineLevelWatcher + }); + this.Watchers.AddRange(this.SkillWatchers.Values); + } + + /// Update the current values if needed. + public void Update() + { + // update valid location + this.LastValidLocation = this.GetCurrentLocation(); + + // update watchers + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + + // update inventory + this.CurrentInventory = this.GetInventory(); + } + + /// Reset all trackers so their current values are the baseline. + public void Reset() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Reset(); + + this.PreviousInventory = this.CurrentInventory; + } + + /// Get the player's current location, ignoring temporary null values. + /// The game will set 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. + public GameLocation GetCurrentLocation() + { + return this.Player.currentLocation ?? this.LastValidLocation; + } + + /// Get the player inventory changes between two states. + public IEnumerable GetInventoryChanges() + { + IDictionary previous = this.PreviousInventory; + IDictionary current = this.GetInventory(); + foreach (Item item in previous.Keys.Union(current.Keys)) + { + if (!previous.TryGetValue(item, out int prevStack)) + yield return new ItemStackChange { Item = item, StackChange = item.Stack, ChangeType = ChangeType.Added }; + else if (!current.TryGetValue(item, out int newStack)) + yield return new ItemStackChange { Item = item, StackChange = -item.Stack, ChangeType = ChangeType.Removed }; + else if (prevStack != newStack) + yield return new ItemStackChange { Item = item, StackChange = newStack - prevStack, ChangeType = ChangeType.StackChange }; + } + } + + /// Get the player skill levels which changed. + public IEnumerable>> GetChangedSkills() + { + return this.SkillWatchers.Where(p => p.Value.IsChanged); + } + + /// Get the player's new location if it changed. + /// The player's current location. + /// Returns whether it changed. + public bool TryGetNewLocation(out GameLocation location) + { + location = this.LocationWatcher.CurrentValue; + return this.LocationWatcher.IsChanged; + } + + /// Get the player's new mine level if it changed. + /// The player's current mine level. + /// Returns whether it changed. + public bool TryGetNewMineLevel(out int mineLevel) + { + mineLevel = this.MineLevelWatcher.CurrentValue; + return this.MineLevelWatcher.IsChanged; + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Get the player's current inventory. + private IDictionary GetInventory() + { + return this.Player.Items + .Where(n => n != null) + .Distinct() + .ToDictionary(n => n, n => n.Stack); + } + } +} diff --git a/src/StardewModdingAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/StardewModdingAPI/Framework/StateTracking/WorldLocationsTracker.cs new file mode 100644 index 00000000..f09c69c1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/StateTracking/WorldLocationsTracker.cs @@ -0,0 +1,243 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using StardewModdingAPI.Framework.StateTracking.Comparers; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Buildings; +using StardewValley.Locations; + +namespace StardewModdingAPI.Framework.StateTracking +{ + /// Detects changes to the game's locations. + internal class WorldLocationsTracker : IWatcher + { + /********* + ** Fields + *********/ + /// Tracks changes to the location list. + private readonly ICollectionWatcher LocationListWatcher; + + /// Tracks changes to the list of active mine locations. + private readonly ICollectionWatcher MineLocationListWatcher; + + /// A lookup of the tracked locations. + private IDictionary LocationDict { get; } = new Dictionary(new ObjectReferenceComparer()); + + /// A lookup of registered buildings and their indoor location. + private readonly IDictionary BuildingIndoors = new Dictionary(new ObjectReferenceComparer()); + + + /********* + ** Accessors + *********/ + /// Whether locations were added or removed since the last reset. + public bool IsLocationListChanged => this.Added.Any() || this.Removed.Any(); + + /// Whether any tracked location data changed since the last reset. + public bool IsChanged => this.IsLocationListChanged || this.Locations.Any(p => p.IsChanged); + + /// The tracked locations. + public IEnumerable Locations => this.LocationDict.Values; + + /// The locations removed since the last update. + public ICollection Added { get; } = new HashSet(new ObjectReferenceComparer()); + + /// The locations added since the last update. + public ICollection Removed { get; } = new HashSet(new ObjectReferenceComparer()); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The game's list of locations. + /// The game's list of active mine locations. + public WorldLocationsTracker(ObservableCollection locations, IList activeMineLocations) + { + this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations); + this.MineLocationListWatcher = WatcherFactory.ForReferenceList(activeMineLocations); + } + + /// Update the current value if needed. + public void Update() + { + // update watchers + this.LocationListWatcher.Update(); + this.MineLocationListWatcher.Update(); + foreach (LocationTracker watcher in this.Locations) + watcher.Update(); + + // detect added/removed locations + if (this.LocationListWatcher.IsChanged) + { + this.Remove(this.LocationListWatcher.Removed); + this.Add(this.LocationListWatcher.Added); + } + if (this.MineLocationListWatcher.IsChanged) + { + this.Remove(this.MineLocationListWatcher.Removed); + this.Add(this.MineLocationListWatcher.Added); + } + + // detect building changed + foreach (LocationTracker watcher in this.Locations.Where(p => p.BuildingsWatcher.IsChanged).ToArray()) + { + this.Remove(watcher.BuildingsWatcher.Removed); + this.Add(watcher.BuildingsWatcher.Added); + } + + // detect building interiors changed (e.g. construction completed) + foreach (KeyValuePair pair in this.BuildingIndoors.Where(p => !object.Equals(p.Key.indoors.Value, p.Value))) + { + GameLocation oldIndoors = pair.Value; + GameLocation newIndoors = pair.Key.indoors.Value; + + if (oldIndoors != null) + this.Added.Add(oldIndoors); + if (newIndoors != null) + this.Removed.Add(newIndoors); + } + } + + /// Set the current location list as the baseline. + public void ResetLocationList() + { + this.Removed.Clear(); + this.Added.Clear(); + this.LocationListWatcher.Reset(); + this.MineLocationListWatcher.Reset(); + } + + /// Set the current value as the baseline. + public void Reset() + { + this.ResetLocationList(); + foreach (IWatcher watcher in this.GetWatchers()) + watcher.Reset(); + } + + /// Stop watching the player fields and release all references. + public void Dispose() + { + foreach (IWatcher watcher in this.GetWatchers()) + watcher.Dispose(); + } + + + /********* + ** Private methods + *********/ + /**** + ** Enumerable wrappers + ****/ + /// Add the given buildings. + /// The buildings to add. + public void Add(IEnumerable buildings) + { + foreach (Building building in buildings) + this.Add(building); + } + + /// Add the given locations. + /// The locations to add. + public void Add(IEnumerable locations) + { + foreach (GameLocation location in locations) + this.Add(location); + } + + /// Remove the given buildings. + /// The buildings to remove. + public void Remove(IEnumerable buildings) + { + foreach (Building building in buildings) + this.Remove(building); + } + + /// Remove the given locations. + /// The locations to remove. + public void Remove(IEnumerable locations) + { + foreach (GameLocation location in locations) + this.Remove(location); + } + + /**** + ** Main add/remove logic + ****/ + /// Add the given building. + /// The building to add. + public void Add(Building building) + { + if (building == null) + return; + + GameLocation indoors = building.indoors.Value; + this.BuildingIndoors[building] = indoors; + this.Add(indoors); + } + + /// Add the given location. + /// The location to add. + public void Add(GameLocation location) + { + if (location == null) + return; + + // remove old location if needed + this.Remove(location); + + // add location + this.Added.Add(location); + this.LocationDict[location] = new LocationTracker(location); + + // add buildings + if (location is BuildableGameLocation buildableLocation) + this.Add(buildableLocation.buildings); + } + + /// Remove the given building. + /// The building to remove. + public void Remove(Building building) + { + if (building == null) + return; + + this.BuildingIndoors.Remove(building); + this.Remove(building.indoors.Value); + } + + /// Remove the given location. + /// The location to remove. + public void Remove(GameLocation location) + { + if (location == null) + return; + + if (this.LocationDict.TryGetValue(location, out LocationTracker watcher)) + { + // track change + this.Removed.Add(location); + + // remove + this.LocationDict.Remove(location); + watcher.Dispose(); + if (location is BuildableGameLocation buildableLocation) + this.Remove(buildableLocation.buildings); + } + } + + /**** + ** Helpers + ****/ + /// The underlying watchers. + private IEnumerable GetWatchers() + { + yield return this.LocationListWatcher; + yield return this.MineLocationListWatcher; + foreach (LocationTracker watcher in this.Locations) + yield return watcher; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs new file mode 100644 index 00000000..6c0fdc90 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Utilities +{ + /// A wrapper meant for tracking recursive contexts. + /// The key type. + internal class ContextHash : HashSet + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public ContextHash() { } + + /// Construct an instance. + /// The implementation to use when comparing values in the set, or null to use the default comparer for the set type. + public ContextHash(IEqualityComparer comparer) + : base(comparer) { } + + /// Add a key while an action is in progress, and remove it when it completes. + /// The key to add. + /// The action to perform. + /// The specified key is already added. + public void Track(T key, Action action) + { + if (this.Contains(key)) + throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); + + this.Add(key); + try + { + action(); + } + finally + { + this.Remove(key); + } + } + + /// Add a key while an action is in progress, and remove it when it completes. + /// The value type returned by the method. + /// The key to add. + /// The action to perform. + public TResult Track(T key, Func action) + { + if (this.Contains(key)) + throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); + + this.Add(key); + try + { + return action(); + } + finally + { + this.Remove(key); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Utilities/Countdown.cs b/src/StardewModdingAPI/Framework/Utilities/Countdown.cs new file mode 100644 index 00000000..921a35ce --- /dev/null +++ b/src/StardewModdingAPI/Framework/Utilities/Countdown.cs @@ -0,0 +1,44 @@ +namespace StardewModdingAPI.Framework.Utilities +{ + /// Counts down from a baseline value. + internal class Countdown + { + /********* + ** Accessors + *********/ + /// The initial value from which to count down. + public int Initial { get; } + + /// The current value. + public int Current { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial value from which to count down. + public Countdown(int initial) + { + this.Initial = initial; + this.Current = initial; + } + + /// Reduce the current value by one. + /// Returns whether the value was decremented (i.e. wasn't already zero). + public bool Decrement() + { + if (this.Current <= 0) + return false; + + this.Current--; + return true; + } + + /// Restart the countdown. + public void Reset() + { + this.Current = this.Initial; + } + } +} diff --git a/src/StardewModdingAPI/Framework/WatcherCore.cs b/src/StardewModdingAPI/Framework/WatcherCore.cs new file mode 100644 index 00000000..32b7fdc6 --- /dev/null +++ b/src/StardewModdingAPI/Framework/WatcherCore.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Framework.Input; +using StardewModdingAPI.Framework.StateTracking; +using StardewModdingAPI.Framework.StateTracking.FieldWatchers; +using StardewValley; +using StardewValley.Locations; +using StardewValley.Menus; + +namespace StardewModdingAPI.Framework +{ + /// Monitors the entire game state for changes, virally spreading watchers into any new entities that get created. + internal class WatcherCore + { + /********* + ** Fields + *********/ + /// The underlying watchers for convenience. These are accessible individually as separate properties. + private readonly List Watchers = new List(); + + + /********* + ** Accessors + *********/ + /// Tracks changes to the window size. + public readonly IValueWatcher WindowSizeWatcher; + + /// Tracks changes to the current player. + public PlayerTracker CurrentPlayerTracker; + + /// Tracks changes to the time of day (in 24-hour military format). + public readonly IValueWatcher TimeWatcher; + + /// Tracks changes to the save ID. + public readonly IValueWatcher SaveIdWatcher; + + /// Tracks changes to the game's locations. + public readonly WorldLocationsTracker LocationsWatcher; + + /// Tracks changes to . + public readonly IValueWatcher ActiveMenuWatcher; + + /// Tracks changes to the cursor position. + public readonly IValueWatcher CursorWatcher; + + /// Tracks changes to the mouse wheel scroll. + public readonly IValueWatcher MouseWheelScrollWatcher; + + /// Tracks changes to the content locale. + public readonly IValueWatcher LocaleWatcher; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Manages input visible to the game. + public WatcherCore(SInputState inputState) + { + // init watchers + this.CursorWatcher = WatcherFactory.ForEquatable(() => inputState.CursorPosition); + this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => inputState.RealMouse.ScrollWheelValue); + this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); + this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); + this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); + this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu); + this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection)Game1.locations, MineShaft.activeMines); + this.LocaleWatcher = WatcherFactory.ForGenericEquality(() => LocalizedContentManager.CurrentLanguageCode); + this.Watchers.AddRange(new IWatcher[] + { + this.CursorWatcher, + this.MouseWheelScrollWatcher, + this.SaveIdWatcher, + this.WindowSizeWatcher, + this.TimeWatcher, + this.ActiveMenuWatcher, + this.LocationsWatcher, + this.LocaleWatcher + }); + } + + /// Update the watchers and adjust for added or removed entities. + public void Update() + { + // reset player + if (Context.IsWorldReady) + { + if (this.CurrentPlayerTracker == null || this.CurrentPlayerTracker.Player != Game1.player) + { + this.CurrentPlayerTracker?.Dispose(); + this.CurrentPlayerTracker = new PlayerTracker(Game1.player); + } + } + else + { + if (this.CurrentPlayerTracker != null) + { + this.CurrentPlayerTracker.Dispose(); + this.CurrentPlayerTracker = null; + } + } + + // update values + foreach (IWatcher watcher in this.Watchers) + watcher.Update(); + this.CurrentPlayerTracker?.Update(); + this.LocationsWatcher.Update(); + } + + /// Reset the current values as the baseline. + public void Reset() + { + foreach (IWatcher watcher in this.Watchers) + watcher.Reset(); + this.CurrentPlayerTracker?.Reset(); + this.LocationsWatcher.Reset(); + } + } +} diff --git a/src/StardewModdingAPI/GamePlatform.cs b/src/StardewModdingAPI/GamePlatform.cs new file mode 100644 index 00000000..3bd74462 --- /dev/null +++ b/src/StardewModdingAPI/GamePlatform.cs @@ -0,0 +1,17 @@ +using StardewModdingAPI.Internal; + +namespace StardewModdingAPI +{ + /// The game's platform version. + public enum GamePlatform + { + /// The Linux version of the game. + Linux = Platform.Linux, + + /// The Mac version of the game. + Mac = Platform.Mac, + + /// The Windows version of the game. + Windows = Platform.Windows + } +} diff --git a/src/StardewModdingAPI/IAssetData.cs b/src/StardewModdingAPI/IAssetData.cs new file mode 100644 index 00000000..c3021144 --- /dev/null +++ b/src/StardewModdingAPI/IAssetData.cs @@ -0,0 +1,47 @@ +using System; + +namespace StardewModdingAPI +{ + /// Generic metadata and methods for a content asset being loaded. + /// The expected data type. + public interface IAssetData : IAssetInfo + { + /********* + ** Accessors + *********/ + /// The content data being read. + TValue Data { get; } + + + /********* + ** Public methods + *********/ + /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. + /// The new content value. + /// The is null. + /// The 's type is not compatible with the loaded asset's type. + void ReplaceWith(TValue value); + } + + /// Generic metadata and methods for a content asset being loaded. + public interface IAssetData : IAssetData + { + /********* + ** Public methods + *********/ + /// Get a helper to manipulate the data as a dictionary. + /// The expected dictionary key. + /// The expected dictionary value. + /// The content being read isn't a dictionary. + IAssetDataForDictionary AsDictionary(); + + /// Get a helper to manipulate the data as an image. + /// The content being read isn't an image. + IAssetDataForImage AsImage(); + + /// Get the data as a given type. + /// The expected data type. + /// The data can't be converted to . + TData GetData(); + } +} diff --git a/src/StardewModdingAPI/IAssetDataForDictionary.cs b/src/StardewModdingAPI/IAssetDataForDictionary.cs new file mode 100644 index 00000000..911599d9 --- /dev/null +++ b/src/StardewModdingAPI/IAssetDataForDictionary.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using StardewModdingAPI.Framework.Content; + +namespace StardewModdingAPI +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + public interface IAssetDataForDictionary : IAssetData> + { +#if !SMAPI_3_0_STRICT + /********* + ** Public methods + *********/ + /// Add or replace an entry in the dictionary. + /// The entry key. + /// The entry value. + [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] + void Set(TKey key, TValue value); + + /// Add or replace an entry in the dictionary. + /// The entry key. + /// A callback which accepts the current value and returns the new value. + [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] + void Set(TKey key, Func value); + + /// Dynamically replace values in the dictionary. + /// A lambda which takes the current key and value for an entry, and returns the new value. + [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] + void Set(Func replacer); +#endif + } +} diff --git a/src/StardewModdingAPI/IAssetDataForImage.cs b/src/StardewModdingAPI/IAssetDataForImage.cs new file mode 100644 index 00000000..1109194f --- /dev/null +++ b/src/StardewModdingAPI/IAssetDataForImage.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI +{ + /// Encapsulates access and changes to image content being read from a data file. + public interface IAssetDataForImage : IAssetData + { + /********* + ** Public methods + *********/ + /// Overwrite part of the image. + /// The image to patch into the content. + /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. + /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. + /// Indicates how an image should be patched. + /// One of the arguments is null. + /// The is outside the bounds of the spritesheet. + /// The content being read isn't an image. + void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace); + } +} diff --git a/src/StardewModdingAPI/IAssetEditor.cs b/src/StardewModdingAPI/IAssetEditor.cs new file mode 100644 index 00000000..d2c6f295 --- /dev/null +++ b/src/StardewModdingAPI/IAssetEditor.cs @@ -0,0 +1,17 @@ +namespace StardewModdingAPI +{ + /// Edits matching content assets. + public interface IAssetEditor + { + /********* + ** Public methods + *********/ + /// Get whether this instance can edit the given asset. + /// Basic metadata about the asset being loaded. + bool CanEdit(IAssetInfo asset); + + /// Edit a matched asset. + /// A helper which encapsulates metadata about an asset and enables changes to it. + void Edit(IAssetData asset); + } +} diff --git a/src/StardewModdingAPI/IAssetInfo.cs b/src/StardewModdingAPI/IAssetInfo.cs new file mode 100644 index 00000000..5dd58e2e --- /dev/null +++ b/src/StardewModdingAPI/IAssetInfo.cs @@ -0,0 +1,28 @@ +using System; + +namespace StardewModdingAPI +{ + /// Basic metadata for a content asset. + public interface IAssetInfo + { + /********* + ** Accessors + *********/ + /// The content's locale code, if the content is localised. + string Locale { get; } + + /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + string AssetName { get; } + + /// The content data type. + Type DataType { get; } + + + /********* + ** Public methods + *********/ + /// Get whether the asset name being loaded matches a given name after normalisation. + /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). + bool AssetNameEquals(string path); + } +} diff --git a/src/StardewModdingAPI/IAssetLoader.cs b/src/StardewModdingAPI/IAssetLoader.cs new file mode 100644 index 00000000..ad97b941 --- /dev/null +++ b/src/StardewModdingAPI/IAssetLoader.cs @@ -0,0 +1,17 @@ +namespace StardewModdingAPI +{ + /// Provides the initial version for matching assets loaded by the game. SMAPI will raise an error if two mods try to load the same asset; in most cases you should use instead. + public interface IAssetLoader + { + /********* + ** Public methods + *********/ + /// Get whether this instance can load the initial version of the given asset. + /// Basic metadata about the asset being loaded. + bool CanLoad(IAssetInfo asset); + + /// Load a matched asset. + /// Basic metadata about the asset being loaded. + T Load(IAssetInfo asset); + } +} diff --git a/src/StardewModdingAPI/ICommandHelper.cs b/src/StardewModdingAPI/ICommandHelper.cs new file mode 100644 index 00000000..196e1051 --- /dev/null +++ b/src/StardewModdingAPI/ICommandHelper.cs @@ -0,0 +1,26 @@ +using System; + +namespace StardewModdingAPI +{ + /// Provides an API for managing console commands. + public interface ICommandHelper : IModLinked + { + /********* + ** Public methods + *********/ + /// Add a console command. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + /// The or is null or empty. + /// The is not a valid format. + /// There's already a command with that name. + ICommandHelper Add(string name, string documentation, Action callback); + + /// Trigger a command. + /// The command name. + /// The command arguments. + /// Returns whether a matching command was triggered. + bool Trigger(string name, string[] arguments); + } +} diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs new file mode 100644 index 00000000..1b87183d --- /dev/null +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using xTile; + +namespace StardewModdingAPI +{ + /// Provides an API for loading content assets. + public interface IContentHelper : IModLinked + { + /********* + ** Accessors + *********/ + /// Interceptors which provide the initial versions of matching content assets. + IList AssetLoaders { get; } + + /// Interceptors which edit matching content assets after they're loaded. + IList AssetEditors { get; } + + /// The game's current locale code (like pt-BR). + string CurrentLocale { get; } + + /// The game's current locale as an enum value. + LocalizedContentManager.LanguageCode CurrentLocaleConstant { get; } + + + /********* + ** Public methods + *********/ + /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + T Load(string key, ContentSource source = ContentSource.ModFolder); + + /// Normalise an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like on generated asset names, and isn't necessary when passing asset names into other content helper methods. + /// The asset key. + [Pure] + string NormaliseAssetName(string assetName); + + /// 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. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); + + /// 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. + /// The asset key to invalidate in the content folder. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + bool InvalidateCache(string key); + + /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. + /// The asset type to remove from the cache. + /// Returns whether any assets were invalidated. + bool InvalidateCache(); + + /// 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. + /// A predicate matching the assets to invalidate. + /// Returns whether any cache entries were invalidated. + bool InvalidateCache(Func predicate); + } +} diff --git a/src/StardewModdingAPI/IContentPack.cs b/src/StardewModdingAPI/IContentPack.cs new file mode 100644 index 00000000..9ba32394 --- /dev/null +++ b/src/StardewModdingAPI/IContentPack.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using xTile; + +namespace StardewModdingAPI +{ + /// An API that provides access to a content pack. + public interface IContentPack + { + /********* + ** Accessors + *********/ + /// The full path to the content pack's folder. + string DirectoryPath { get; } + + /// The content pack's manifest. + IManifest Manifest { get; } + + + /********* + ** Public methods + *********/ + /// Read a JSON file from the content pack folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the content pack directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The is not relative or contains directory climbing (../). + TModel ReadJsonFile(string path) where TModel : class; + + /// Save data to a JSON file in the content pack's folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the mod folder. + /// The arbitrary data to save. + /// The is not relative or contains directory climbing (../). + void WriteJsonFile(string path, TModel data) where TModel : class; + + /// Load content from the content pack folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. + /// The expected data type. The main supported types are , , and dictionaries; other types may be supported by the game's content pipeline. + /// The local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + /// The content asset couldn't be loaded (e.g. because it doesn't exist). + T LoadAsset(string key); + + /// 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. + /// The the local path to a content file relative to the content pack folder. + /// The is empty or contains invalid characters. + string GetActualAssetKey(string key); + } +} diff --git a/src/StardewModdingAPI/IContentPackHelper.cs b/src/StardewModdingAPI/IContentPackHelper.cs new file mode 100644 index 00000000..e4949f58 --- /dev/null +++ b/src/StardewModdingAPI/IContentPackHelper.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// Provides an API for managing content packs. + public interface IContentPackHelper : IModLinked + { + /********* + ** Public methods + *********/ + /// Get all content packs loaded for this mod. + IEnumerable GetOwned(); + + /// Create a temporary content pack to read files from a directory, using randomised manifest fields. Temporary content packs will not appear in the SMAPI log and update checks will not be performed. + /// The absolute directory path containing the content pack files. + IContentPack CreateFake(string directoryPath); + + /// Create a temporary content pack to read files from a directory. Temporary content packs will not appear in the SMAPI log and update checks will not be performed. + /// The absolute directory path containing the content pack files. + /// The content pack's unique ID. + /// The content pack name. + /// The content pack description. + /// The content pack author's name. + /// The content pack version. + IContentPack CreateTemporary(string directoryPath, string id, string name, string description, string author, ISemanticVersion version); + } +} diff --git a/src/StardewModdingAPI/ICursorPosition.cs b/src/StardewModdingAPI/ICursorPosition.cs new file mode 100644 index 00000000..21c57db0 --- /dev/null +++ b/src/StardewModdingAPI/ICursorPosition.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Xna.Framework; + +namespace StardewModdingAPI +{ + /// Represents a cursor position in the different coordinate systems. + public interface ICursorPosition : IEquatable + { + /// The pixel position relative to the top-left corner of the in-game map. + Vector2 AbsolutePixels { get; } + + /// The pixel position relative to the top-left corner of the visible screen. + Vector2 ScreenPixels { get; } + + /// The tile position under the cursor relative to the top-left corner of the map. + Vector2 Tile { get; } + + /// The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than if that's too far from the player. + Vector2 GrabTile { get; } + } +} diff --git a/src/StardewModdingAPI/IDataHelper.cs b/src/StardewModdingAPI/IDataHelper.cs new file mode 100644 index 00000000..6afdc529 --- /dev/null +++ b/src/StardewModdingAPI/IDataHelper.cs @@ -0,0 +1,61 @@ +using System; + +namespace StardewModdingAPI +{ + /// Provides an API for reading and storing local mod data. + public interface IDataHelper + { + /********* + ** Public methods + *********/ + /**** + ** JSON file + ****/ + /// Read data from a JSON file in the mod's folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the mod folder. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The is not relative or contains directory climbing (../). + TModel ReadJsonFile(string path) where TModel : class; + + /// Save data to a JSON file in the mod's folder. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The file path relative to the mod folder. + /// The arbitrary data to save. + /// The is not relative or contains directory climbing (../). + void WriteJsonFile(string path, TModel data) where TModel : class; + + /**** + ** Save file + ****/ + /// Read arbitrary data stored in the current save slot. This is only possible if a save has been loaded. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// Returns the parsed data, or null if the entry doesn't exist or is empty. + /// The player hasn't loaded a save file yet or isn't the main player. + TModel ReadSaveData(string key) where TModel : class; + + /// 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. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// The arbitrary data to save. + /// The player hasn't loaded a save file yet or isn't the main player. + void WriteSaveData(string key, TModel data) where TModel : class; + + + /**** + ** Global app data + ****/ + /// Read arbitrary data stored on the local computer, synchronised by GOG/Steam if applicable. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// Returns the parsed data, or null if the entry doesn't exist or is empty. + TModel ReadGlobalData(string key) where TModel : class; + + /// Save arbitrary data to the local computer, synchronised by GOG/Steam if applicable. + /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. + /// The unique key identifying the data. + /// The arbitrary data to save. + void WriteGlobalData(string key, TModel data) where TModel : class; + } +} diff --git a/src/StardewModdingAPI/IInputHelper.cs b/src/StardewModdingAPI/IInputHelper.cs new file mode 100644 index 00000000..328f504b --- /dev/null +++ b/src/StardewModdingAPI/IInputHelper.cs @@ -0,0 +1,21 @@ +namespace StardewModdingAPI +{ + /// Provides an API for checking and changing input state. + public interface IInputHelper : IModLinked + { + /// Get the current cursor position. + ICursorPosition GetCursorPosition(); + + /// Get whether a button is currently pressed. + /// The button. + bool IsDown(SButton button); + + /// Get whether a button is currently suppressed, so the game won't see it. + /// The button. + bool IsSuppressed(SButton button); + + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. + /// The button to suppress. + void Suppress(SButton button); + } +} diff --git a/src/StardewModdingAPI/IMod.cs b/src/StardewModdingAPI/IMod.cs new file mode 100644 index 00000000..44ef32c9 --- /dev/null +++ b/src/StardewModdingAPI/IMod.cs @@ -0,0 +1,29 @@ +namespace StardewModdingAPI +{ + /// The implementation for a Stardew Valley mod. + public interface IMod + { + /********* + ** Accessors + *********/ + /// Provides simplified APIs for writing mods. + IModHelper Helper { get; } + + /// Writes messages to the console and log file. + IMonitor Monitor { get; } + + /// The mod's manifest. + IManifest ModManifest { get; } + + + /********* + ** Public methods + *********/ + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + void Entry(IModHelper helper); + + /// Get an API that other mods can access. This is always called after . + object GetApi(); + } +} diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/StardewModdingAPI/IModHelper.cs new file mode 100644 index 00000000..0220b4f7 --- /dev/null +++ b/src/StardewModdingAPI/IModHelper.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI +{ + /// Provides simplified APIs for writing mods. + public interface IModHelper + { + /********* + ** Accessors + *********/ + /// The full path to the mod's folder. + string DirectoryPath { get; } + + /// Manages access to events raised by SMAPI, which let your mod react when something happens in the game. + IModEvents Events { get; } + + /// An API for managing console commands. + ICommandHelper ConsoleCommands { get; } + + /// An API for loading content assets. + IContentHelper Content { get; } + + /// An API for managing content packs. + IContentPackHelper ContentPacks { get; } + + /// An API for reading and writing persistent mod data. + IDataHelper Data { get; } + + /// An API for checking and changing input state. + IInputHelper Input { get; } + + /// Simplifies access to private game code. + IReflectionHelper Reflection { get; } + + /// Metadata about loaded mods. + IModRegistry ModRegistry { get; } + + /// Provides multiplayer utilities. + IMultiplayerHelper Multiplayer { get; } + + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + ITranslationHelper Translation { get; } + + + /********* + ** Public methods + *********/ + /**** + ** Mod config file + ****/ + /// Read the mod's configuration file (and create it if needed). + /// The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types. + TConfig ReadConfig() where TConfig : class, new(); + + /// Save to the mod's configuration file. + /// The config class type. + /// The config settings to save. + void WriteConfig(TConfig config) where TConfig : class, new(); + +#if !SMAPI_3_0_STRICT + /**** + ** Generic JSON files + ****/ + /// Read a JSON file. + /// The model type. + /// The file path relative to the mod directory. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + [Obsolete("Use " + nameof(IModHelper.Data) + "." + nameof(IDataHelper.ReadJsonFile) + " instead")] + TModel ReadJsonFile(string path) where TModel : class; + + /// Save to a JSON file. + /// The model type. + /// The file path relative to the mod directory. + /// The model to save. + [Obsolete("Use " + nameof(IModHelper.Data) + "." + nameof(IDataHelper.WriteJsonFile) + " instead")] + void WriteJsonFile(string path, TModel model) where TModel : class; + + /**** + ** Content packs + ****/ + /// Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI. + /// The absolute directory path containing the content pack files. + /// The content pack's unique ID. + /// The content pack name. + /// The content pack description. + /// The content pack author's name. + /// The content pack version. + [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ContentPacks) + "." + nameof(IContentPackHelper.CreateTemporary) + " instead")] + IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version); + + /// Get all content packs loaded for this mod. + [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ContentPacks) + "." + nameof(IContentPackHelper.GetOwned) + " instead")] + IEnumerable GetContentPacks(); +#endif + } +} diff --git a/src/StardewModdingAPI/IModInfo.cs b/src/StardewModdingAPI/IModInfo.cs new file mode 100644 index 00000000..3c85d454 --- /dev/null +++ b/src/StardewModdingAPI/IModInfo.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// Metadata for a loaded mod. + public interface IModInfo + { + /// The mod manifest. + IManifest Manifest { get; } + + /// Whether the mod is a content pack. + bool IsContentPack { get; } + } +} diff --git a/src/StardewModdingAPI/IModLinked.cs b/src/StardewModdingAPI/IModLinked.cs new file mode 100644 index 00000000..172ee30c --- /dev/null +++ b/src/StardewModdingAPI/IModLinked.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// An instance linked to a mod. + public interface IModLinked + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod for which the instance was created. + string ModID { get; } + } +} diff --git a/src/StardewModdingAPI/IModRegistry.cs b/src/StardewModdingAPI/IModRegistry.cs new file mode 100644 index 00000000..10b3121e --- /dev/null +++ b/src/StardewModdingAPI/IModRegistry.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// Provides an API for fetching metadata about loaded mods. + public interface IModRegistry : IModLinked + { + /// Get metadata for all loaded mods. + IEnumerable GetAll(); + + /// Get metadata for a loaded mod. + /// The mod's unique ID. + /// Returns the matching mod's metadata, or null if not found. + IModInfo Get(string uniqueID); + + /// Get whether a mod has been loaded. + /// The mod's unique ID. + bool IsLoaded(string uniqueID); + + /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods. + /// The mod's unique ID. + object GetApi(string uniqueID); + + /// 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 null. + /// The interface which matches the properties and methods you intend to access. + /// The mod's unique ID. + TInterface GetApi(string uniqueID) where TInterface : class; + } +} diff --git a/src/StardewModdingAPI/IMonitor.cs b/src/StardewModdingAPI/IMonitor.cs new file mode 100644 index 00000000..0f153e10 --- /dev/null +++ b/src/StardewModdingAPI/IMonitor.cs @@ -0,0 +1,32 @@ +namespace StardewModdingAPI +{ + /// Encapsulates monitoring and logging for a given module. + public interface IMonitor + { + /********* + ** Accessors + *********/ + /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. + bool IsExiting { get; } + + /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. + bool IsVerbose { get; } + + + /********* + ** Methods + *********/ + /// Log a message for the player or developer. + /// The message to log. + /// The log severity level. + void Log(string message, LogLevel level = LogLevel.Debug); + + /// Log a message that only appears when is enabled. + /// The message to log. + void VerboseLog(string message); + + /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. + /// The reason for the shutdown. + void ExitGameImmediately(string reason); + } +} diff --git a/src/StardewModdingAPI/IMultiplayerHelper.cs b/src/StardewModdingAPI/IMultiplayerHelper.cs new file mode 100644 index 00000000..4067a676 --- /dev/null +++ b/src/StardewModdingAPI/IMultiplayerHelper.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using StardewValley; + +namespace StardewModdingAPI +{ + /// Provides multiplayer utilities. + public interface IMultiplayerHelper : IModLinked + { + /// Get a new multiplayer ID. + long GetNewID(); + + /// Get the locations which are being actively synced from the host. + IEnumerable GetActiveLocations(); + + /// Get a connected player. + /// The player's unique ID. + /// Returns the connected player, or null if no such player is connected. + IMultiplayerPeer GetConnectedPlayer(long id); + + /// Get all connected players. + IEnumerable GetConnectedPlayers(); + + /// Send a message to mods installed by connected players. + /// The data type. This can be a class with a default constructor, or a value type. + /// The data to send over the network. + /// A message type which receiving mods can use to decide whether it's the one they want to handle, like SetPlayerLocation. This doesn't need to be globally unique, since mods should check the originating mod ID. + /// The mod IDs which should receive the message on the destination computers, or null for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast. + /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. + /// The or is null. + void SendMessage(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null); + } +} diff --git a/src/StardewModdingAPI/IMultiplayerPeer.cs b/src/StardewModdingAPI/IMultiplayerPeer.cs new file mode 100644 index 00000000..0d4d3261 --- /dev/null +++ b/src/StardewModdingAPI/IMultiplayerPeer.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// Metadata about a connected player. + public interface IMultiplayerPeer + { + /********* + ** Accessors + *********/ + /// The player's unique ID. + long PlayerID { get; } + + /// Whether this is a connection to the host player. + bool IsHost { get; } + + /// Whether the player has SMAPI installed. + bool HasSmapi { get; } + + /// The player's OS platform, if is true. + GamePlatform? Platform { get; } + + /// The installed version of Stardew Valley, if is true. + ISemanticVersion GameVersion { get; } + + /// The installed version of SMAPI, if is true. + ISemanticVersion ApiVersion { get; } + + /// The installed mods, if is true. + IEnumerable Mods { get; } + + + /********* + ** Methods + *********/ + /// Get metadata for a mod installed by the player. + /// The unique mod ID. + /// Returns the mod info, or null if the player doesn't have that mod. + IMultiplayerPeerMod GetMod(string id); + } +} diff --git a/src/StardewModdingAPI/IMultiplayerPeerMod.cs b/src/StardewModdingAPI/IMultiplayerPeerMod.cs new file mode 100644 index 00000000..005408b1 --- /dev/null +++ b/src/StardewModdingAPI/IMultiplayerPeerMod.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI +{ + /// Metadata about a mod installed by a connected player. + public interface IMultiplayerPeerMod + { + /// The mod's display name. + string Name { get; } + + /// The unique mod ID. + string ID { get; } + + /// The mod version. + ISemanticVersion Version { get; } + } +} diff --git a/src/StardewModdingAPI/IReflectedField.cs b/src/StardewModdingAPI/IReflectedField.cs new file mode 100644 index 00000000..43ddad42 --- /dev/null +++ b/src/StardewModdingAPI/IReflectedField.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A field obtained through reflection. + /// The field value type. + public interface IReflectedField + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + FieldInfo FieldInfo { get; } + + + /********* + ** Public methods + *********/ + /// Get the field value. + TValue GetValue(); + + /// Set the field value. + //// The value to set. + void SetValue(TValue value); + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/IReflectedMethod.cs b/src/StardewModdingAPI/IReflectedMethod.cs new file mode 100644 index 00000000..de83b98c --- /dev/null +++ b/src/StardewModdingAPI/IReflectedMethod.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A method obtained through reflection. + public interface IReflectedMethod + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + MethodInfo MethodInfo { get; } + + + /********* + ** Public methods + *********/ + /// Invoke the method. + /// The return type. + /// The method arguments to pass in. + TValue Invoke(params object[] arguments); + + /// Invoke the method. + /// The method arguments to pass in. + void Invoke(params object[] arguments); + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/IReflectedProperty.cs b/src/StardewModdingAPI/IReflectedProperty.cs new file mode 100644 index 00000000..73ad9f30 --- /dev/null +++ b/src/StardewModdingAPI/IReflectedProperty.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace StardewModdingAPI +{ + /// A property obtained through reflection. + /// The property value type. + public interface IReflectedProperty + { + /********* + ** Accessors + *********/ + /// The reflection metadata. + PropertyInfo PropertyInfo { get; } + + + /********* + ** Public methods + *********/ + /// Get the property value. + TValue GetValue(); + + /// Set the property value. + //// The value to set. + void SetValue(TValue value); + } +} diff --git a/src/StardewModdingAPI/IReflectionHelper.cs b/src/StardewModdingAPI/IReflectionHelper.cs new file mode 100644 index 00000000..a2b9eb32 --- /dev/null +++ b/src/StardewModdingAPI/IReflectionHelper.cs @@ -0,0 +1,51 @@ +using System; + +namespace StardewModdingAPI +{ + /// Provides an API for accessing inaccessible code. + public interface IReflectionHelper : IModLinked + { + /********* + ** Public methods + *********/ + /// Get an instance field. + /// The field type. + /// The object which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedField GetField(object obj, string name, bool required = true); + + /// Get a static field. + /// The field type. + /// The type which has the field. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedField GetField(Type type, string name, bool required = true); + + /// Get an instance property. + /// The property type. + /// The object which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + IReflectedProperty GetProperty(object obj, string name, bool required = true); + + /// Get a static property. + /// The property type. + /// The type which has the property. + /// The property name. + /// Whether to throw an exception if the property is not found. + IReflectedProperty GetProperty(Type type, string name, bool required = true); + + /// Get an instance method. + /// The object which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedMethod GetMethod(object obj, string name, bool required = true); + + /// Get a static method. + /// The type which has the method. + /// The field name. + /// Whether to throw an exception if the field is not found. + IReflectedMethod GetMethod(Type type, string name, bool required = true); + } +} diff --git a/src/StardewModdingAPI/ITranslationHelper.cs b/src/StardewModdingAPI/ITranslationHelper.cs new file mode 100644 index 00000000..c4b72444 --- /dev/null +++ b/src/StardewModdingAPI/ITranslationHelper.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using StardewValley; + +namespace StardewModdingAPI +{ + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + public interface ITranslationHelper : IModLinked + { + /********* + ** Accessors + *********/ + /// The current locale. + string Locale { get; } + + /// The game's current language code. + LocalizedContentManager.LanguageCode LocaleEnum { get; } + + + /********* + ** Public methods + *********/ + /// Get all translations for the current locale. + IEnumerable GetTranslations(); + + /// Get a translation for the current locale. + /// The translation key. + Translation Get(string key); + + /// Get a translation for the current locale. + /// The translation key. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. + Translation Get(string key, object tokens); + } +} diff --git a/src/StardewModdingAPI/LogLevel.cs b/src/StardewModdingAPI/LogLevel.cs new file mode 100644 index 00000000..7987f82a --- /dev/null +++ b/src/StardewModdingAPI/LogLevel.cs @@ -0,0 +1,26 @@ +using StardewModdingAPI.Internal.ConsoleWriting; + +namespace StardewModdingAPI +{ + /// The log severity levels. + public enum LogLevel + { + /// Tracing info intended for developers. + Trace = ConsoleLogLevel.Trace, + + /// Troubleshooting info that may be relevant to the player. + Debug = ConsoleLogLevel.Debug, + + /// Info relevant to the player. This should be used judiciously. + Info = ConsoleLogLevel.Info, + + /// An issue the player should be aware of. This should be used rarely. + Warn = ConsoleLogLevel.Warn, + + /// A message indicating something went wrong. + Error = ConsoleLogLevel.Error, + + /// Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue. + Alert = ConsoleLogLevel.Alert + } +} diff --git a/src/StardewModdingAPI/Metadata/CoreAssetPropagator.cs b/src/StardewModdingAPI/Metadata/CoreAssetPropagator.cs new file mode 100644 index 00000000..a64dc89b --- /dev/null +++ b/src/StardewModdingAPI/Metadata/CoreAssetPropagator.cs @@ -0,0 +1,750 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; +using StardewValley.BellsAndWhistles; +using StardewValley.Buildings; +using StardewValley.Characters; +using StardewValley.Locations; +using StardewValley.Menus; +using StardewValley.Objects; +using StardewValley.Projectiles; +using StardewValley.TerrainFeatures; +using xTile; +using xTile.Tiles; + +namespace StardewModdingAPI.Metadata +{ + /// Propagates changes to core assets to the game state. + internal class CoreAssetPropagator + { + /********* + ** Fields + *********/ + /// Normalises an asset key to match the cache key. + private readonly Func GetNormalisedPath; + + /// Simplifies access to private game code. + private readonly Reflector Reflection; + + /// Encapsulates monitoring and logging. + private readonly IMonitor Monitor; + + + /********* + ** Public methods + *********/ + /// Initialise the core asset data. + /// Normalises an asset key to match the cache key. + /// Simplifies access to private code. + /// Encapsulates monitoring and logging. + public CoreAssetPropagator(Func getNormalisedPath, Reflector reflection, IMonitor monitor) + { + this.GetNormalisedPath = getNormalisedPath; + this.Reflection = reflection; + this.Monitor = monitor; + } + + /// Reload one of the game's core assets (if applicable). + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// The asset type to reload. + /// Returns whether an asset was reloaded. + public bool Propagate(LocalizedContentManager content, string key, Type type) + { + object result = this.PropagateImpl(content, key, type); + if (result is bool b) + return b; + return result != null; + } + + + /********* + ** Private methods + *********/ + /// Reload one of the game's core assets (if applicable). + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// The asset type to reload. + /// Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true. + private object PropagateImpl(LocalizedContentManager content, string key, Type type) + { + key = this.GetNormalisedPath(key); + + /**** + ** Special case: current map tilesheet + ** We only need to do this for the current location, since tilesheets are reloaded when you enter a location. + ** Just in case, we should still propagate by key even if a tilesheet is matched. + ****/ + if (Game1.currentLocation?.map?.TileSheets != null) + { + foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets) + { + if (this.GetNormalisedPath(tilesheet.ImageSource) == key) + Game1.mapDisplayDevice.LoadTileSheet(tilesheet); + } + } + + /**** + ** Propagate map changes + ****/ + if (type == typeof(Map)) + { + bool anyChanged = false; + foreach (GameLocation location in this.GetLocations()) + { + if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.GetNormalisedPath(location.mapPath.Value) == key) + { + // reload map data + this.Reflection.GetMethod(location, "reloadMap").Invoke(); + this.Reflection.GetMethod(location, "updateWarps").Invoke(); + + // reload doors + { + Type interiorDoorDictType = Type.GetType($"StardewValley.InteriorDoorDictionary, {Constants.GameAssemblyName}", throwOnError: true); + ConstructorInfo constructor = interiorDoorDictType.GetConstructor(new[] { typeof(GameLocation) }); + if (constructor == null) + throw new InvalidOperationException("Can't reset location doors: constructor not found for InteriorDoorDictionary type."); + object instance = constructor.Invoke(new object[] { location }); + + this.Reflection.GetField(location, "interiorDoors").SetValue(instance); + } + + anyChanged = true; + } + } + return anyChanged; + } + + /**** + ** Propagate by key + ****/ + Reflector reflection = this.Reflection; + switch (key.ToLower().Replace("/", "\\")) // normalised key so we can compare statically + { + /**** + ** Animals + ****/ + case "animals\\cat": + return this.ReloadPetOrHorseSprites(content, key); + case "animals\\dog": + return this.ReloadPetOrHorseSprites(content, key); + case "animals\\horse": + return this.ReloadPetOrHorseSprites(content, key); + + /**** + ** Buildings + ****/ + case "buildings\\houses": // Farm + reflection.GetField(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load(key)); + return true; + + /**** + ** Content\Characters\Farmer + ****/ + case "characters\\farmer\\accessories": // Game1.loadContent + return FarmerRenderer.accessoriesTexture = content.Load(key); + + case "characters\\farmer\\farmer_base": // Farmer + if (Game1.player == null || !Game1.player.IsMale) + return false; + return Game1.player.FarmerRenderer = new FarmerRenderer(key); + + case "characters\\farmer\\farmer_girl_base": // Farmer + if (Game1.player == null || Game1.player.IsMale) + return false; + return Game1.player.FarmerRenderer = new FarmerRenderer(key); + + case "characters\\farmer\\hairstyles": // Game1.loadContent + return FarmerRenderer.hairStylesTexture = content.Load(key); + + case "characters\\farmer\\hats": // Game1.loadContent + return FarmerRenderer.hatsTexture = content.Load(key); + + case "characters\\farmer\\shirts": // Game1.loadContent + return FarmerRenderer.shirtsTexture = content.Load(key); + + /**** + ** Content\Data + ****/ + case "data\\achievements": // Game1.loadContent + return Game1.achievements = content.Load>(key); + + case "data\\bigcraftablesinformation": // Game1.loadContent + return Game1.bigCraftablesInformation = content.Load>(key); + + case "data\\cookingrecipes": // CraftingRecipe.InitShared + return CraftingRecipe.cookingRecipes = content.Load>(key); + + case "data\\craftingrecipes": // CraftingRecipe.InitShared + return CraftingRecipe.craftingRecipes = content.Load>(key); + + case "data\\npcdispositions": // NPC constructor + return this.ReloadNpcDispositions(content, key); + + case "data\\npcgifttastes": // Game1.loadContent + return Game1.NPCGiftTastes = content.Load>(key); + + case "data\\objectinformation": // Game1.loadContent + return Game1.objectInformation = content.Load>(key); + + /**** + ** Content\Fonts + ****/ + case "fonts\\spritefont1": // Game1.loadContent + return Game1.dialogueFont = content.Load(key); + + case "fonts\\smallfont": // Game1.loadContent + return Game1.smallFont = content.Load(key); + + case "fonts\\tinyfont": // Game1.loadContent + return Game1.tinyFont = content.Load(key); + + case "fonts\\tinyfontborder": // Game1.loadContent + return Game1.tinyFontBorder = content.Load(key); + + /**** + ** Content\Lighting + ****/ + case "loosesprites\\lighting\\greenlight": // Game1.loadContent + return Game1.cauldronLight = content.Load(key); + + case "loosesprites\\lighting\\indoorwindowlight": // Game1.loadContent + return Game1.indoorWindowLight = content.Load(key); + + case "loosesprites\\lighting\\lantern": // Game1.loadContent + return Game1.lantern = content.Load(key); + + case "loosesprites\\lighting\\sconcelight": // Game1.loadContent + return Game1.sconceLight = content.Load(key); + + case "loosesprites\\lighting\\windowlight": // Game1.loadContent + return Game1.windowLight = content.Load(key); + + /**** + ** Content\LooseSprites + ****/ + case "loosesprites\\controllermaps": // Game1.loadContent + return Game1.controllerMaps = content.Load(key); + + case "loosesprites\\cursors": // Game1.loadContent + return Game1.mouseCursors = content.Load(key); + + case "loosesprites\\daybg": // Game1.loadContent + return Game1.daybg = content.Load(key); + + case "loosesprites\\font_bold": // Game1.loadContent + return SpriteText.spriteTexture = content.Load(key); + + case "loosesprites\\font_colored": // Game1.loadContent + return SpriteText.coloredTexture = content.Load(key); + + case "loosesprites\\nightbg": // Game1.loadContent + return Game1.nightbg = content.Load(key); + + case "loosesprites\\shadow": // Game1.loadContent + return Game1.shadowTexture = content.Load(key); + + /**** + ** Content\Critters + ****/ + case "tilesheets\\crops": // Game1.loadContent + return Game1.cropSpriteSheet = content.Load(key); + + case "tilesheets\\debris": // Game1.loadContent + return Game1.debrisSpriteSheet = content.Load(key); + + case "tilesheets\\emotes": // Game1.loadContent + return Game1.emoteSpriteSheet = content.Load(key); + + case "tilesheets\\furniture": // Game1.loadContent + return Furniture.furnitureTexture = content.Load(key); + + case "tilesheets\\projectiles": // Game1.loadContent + return Projectile.projectileSheet = content.Load(key); + + case "tilesheets\\rain": // Game1.loadContent + return Game1.rainTexture = content.Load(key); + + case "tilesheets\\tools": // Game1.ResetToolSpriteSheet + Game1.ResetToolSpriteSheet(); + return true; + + case "tilesheets\\weapons": // Game1.loadContent + return Tool.weaponsTexture = content.Load(key); + + /**** + ** Content\Maps + ****/ + case "maps\\menutiles": // Game1.loadContent + return Game1.menuTexture = content.Load(key); + + case "maps\\springobjects": // Game1.loadContent + return Game1.objectSpriteSheet = content.Load(key); + + case "maps\\walls_and_floors": // Wallpaper + return Wallpaper.wallpaperTexture = content.Load(key); + + /**** + ** Content\Minigames + ****/ + case "minigames\\clouds": // TitleMenu + if (Game1.activeClickableMenu is TitleMenu) + { + reflection.GetField(Game1.activeClickableMenu, "cloudsTexture").SetValue(content.Load(key)); + return true; + } + return false; + + case "minigames\\titlebuttons": // TitleMenu + if (Game1.activeClickableMenu is TitleMenu titleMenu) + { + Texture2D texture = content.Load(key); + reflection.GetField(titleMenu, "titleButtonsTexture").SetValue(texture); + foreach (TemporaryAnimatedSprite bird in reflection.GetField>(titleMenu, "birds").GetValue()) + bird.texture = texture; + return true; + } + return false; + + /**** + ** Content\TileSheets + ****/ + case "tilesheets\\animations": // Game1.loadContent + return Game1.animations = content.Load(key); + + case "tilesheets\\buffsicons": // Game1.loadContent + return Game1.buffsIcons = content.Load(key); + + case "tilesheets\\bushes": // new Bush() + reflection.GetField>(typeof(Bush), "texture").SetValue(new Lazy(() => content.Load(key))); + return true; + + case "tilesheets\\craftables": // Game1.loadContent + return Game1.bigCraftableSpriteSheet = content.Load(key); + + case "tilesheets\\fruittrees": // FruitTree + return FruitTree.texture = content.Load(key); + + /**** + ** Content\TerrainFeatures + ****/ + case "terrainfeatures\\flooring": // Flooring + return Flooring.floorsTexture = content.Load(key); + + case "terrainfeatures\\hoedirt": // from HoeDirt + return HoeDirt.lightTexture = content.Load(key); + + case "terrainfeatures\\hoedirtdark": // from HoeDirt + return HoeDirt.darkTexture = content.Load(key); + + case "terrainfeatures\\hoedirtsnow": // from HoeDirt + return HoeDirt.snowTexture = content.Load(key); + + case "terrainfeatures\\mushroom_tree": // from Tree + return this.ReloadTreeTextures(content, key, Tree.mushroomTree); + + case "terrainfeatures\\tree_palm": // from Tree + return this.ReloadTreeTextures(content, key, Tree.palmTree); + + case "terrainfeatures\\tree1_fall": // from Tree + case "terrainfeatures\\tree1_spring": // from Tree + case "terrainfeatures\\tree1_summer": // from Tree + case "terrainfeatures\\tree1_winter": // from Tree + return this.ReloadTreeTextures(content, key, Tree.bushyTree); + + case "terrainfeatures\\tree2_fall": // from Tree + case "terrainfeatures\\tree2_spring": // from Tree + case "terrainfeatures\\tree2_summer": // from Tree + case "terrainfeatures\\tree2_winter": // from Tree + return this.ReloadTreeTextures(content, key, Tree.leafyTree); + + case "terrainfeatures\\tree3_fall": // from Tree + case "terrainfeatures\\tree3_spring": // from Tree + case "terrainfeatures\\tree3_winter": // from Tree + return this.ReloadTreeTextures(content, key, Tree.pineTree); + } + + // dynamic textures + if (this.IsInFolder(key, "Animals")) + return this.ReloadFarmAnimalSprites(content, key); + + if (this.IsInFolder(key, "Buildings")) + return this.ReloadBuildings(content, key); + + if (this.IsInFolder(key, "Characters") || this.IsInFolder(key, "Characters\\Monsters")) + return this.ReloadNpcSprites(content, key); + + if (this.KeyStartsWith(key, "LooseSprites\\Fence")) + return this.ReloadFenceTextures(key); + + if (this.IsInFolder(key, "Portraits")) + return this.ReloadNpcPortraits(content, key); + + // dynamic data + if (this.IsInFolder(key, "Characters\\Dialogue")) + return this.ReloadNpcDialogue(key); + + if (this.IsInFolder(key, "Characters\\schedules")) + return this.ReloadNpcSchedules(key); + + return false; + } + + + /********* + ** Private methods + *********/ + /**** + ** Reload texture methods + ****/ + /// Reload the sprites for matching pets or horses. + /// The animal type. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any textures were reloaded. + private bool ReloadPetOrHorseSprites(LocalizedContentManager content, string key) + where TAnimal : NPC + { + // find matches + TAnimal[] animals = this.GetCharacters().OfType().ToArray(); + if (!animals.Any()) + return false; + + // update sprites + Texture2D texture = content.Load(key); + foreach (TAnimal animal in animals) + this.SetSpriteTexture(animal.Sprite, texture); + return true; + } + + /// Reload the sprites for matching farm animals. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any textures were reloaded. + /// Derived from . + private bool ReloadFarmAnimalSprites(LocalizedContentManager content, string key) + { + // find matches + FarmAnimal[] animals = this.GetFarmAnimals().ToArray(); + if (!animals.Any()) + return false; + + // update sprites + Lazy texture = new Lazy(() => content.Load(key)); + foreach (FarmAnimal animal in animals) + { + // get expected key + string expectedKey = animal.age.Value < animal.ageWhenMature.Value + ? $"Baby{(animal.type.Value == "Duck" ? "White Chicken" : animal.type.Value)}" + : animal.type.Value; + if (animal.showDifferentTextureWhenReadyForHarvest.Value && animal.currentProduce.Value <= 0) + expectedKey = $"Sheared{expectedKey}"; + expectedKey = $"Animals\\{expectedKey}"; + + // reload asset + if (expectedKey == key) + this.SetSpriteTexture(animal.Sprite, texture.Value); + } + return texture.IsValueCreated; + } + + /// Reload building textures. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any textures were reloaded. + private bool ReloadBuildings(LocalizedContentManager content, string key) + { + // get buildings + string type = Path.GetFileName(key); + Building[] buildings = Game1.locations + .OfType() + .SelectMany(p => p.buildings) + .Where(p => p.buildingType.Value == type) + .ToArray(); + + // reload buildings + if (buildings.Any()) + { + Lazy texture = new Lazy(() => content.Load(key)); + foreach (Building building in buildings) + building.texture = texture; + return true; + } + return false; + } + + /// Reload the sprites for a fence type. + /// The asset key to reload. + /// Returns whether any textures were reloaded. + private bool ReloadFenceTextures(string key) + { + // get fence type + if (!int.TryParse(this.GetSegments(key)[1].Substring("Fence".Length), out int fenceType)) + return false; + + // get fences + Fence[] fences = + ( + from location in this.GetLocations() + from fence in location.Objects.Values.OfType() + where + fence.whichType.Value == fenceType + || (fence.isGate.Value && fenceType == 1) // gates are hardcoded to draw fence type 1 + select fence + ) + .ToArray(); + + // update fence textures + foreach (Fence fence in fences) + this.Reflection.GetField>(fence, "fenceTexture").SetValue(new Lazy(fence.loadFenceTexture)); + return true; + } + + /// Reload the disposition data for matching NPCs. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any NPCs were affected. + private bool ReloadNpcDispositions(LocalizedContentManager content, string key) + { + IDictionary dispositions = content.Load>(key); + foreach (NPC character in this.GetCharacters()) + { + if (!character.isVillager() || !dispositions.ContainsKey(character.Name)) + continue; + + NPC clone = new NPC(null, character.Position, character.DefaultMap, character.FacingDirection, character.Name, null, character.Portrait, eventActor: false); + character.Age = clone.Age; + character.Manners = clone.Manners; + character.SocialAnxiety = clone.SocialAnxiety; + character.Optimism = clone.Optimism; + character.Gender = clone.Gender; + character.datable.Value = clone.datable.Value; + character.homeRegion = clone.homeRegion; + character.Birthday_Season = clone.Birthday_Season; + character.Birthday_Day = clone.Birthday_Day; + character.id = clone.id; + character.displayName = clone.displayName; + } + + return true; + } + + /// Reload the sprites for matching NPCs. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any textures were reloaded. + private bool ReloadNpcSprites(LocalizedContentManager content, string key) + { + // get NPCs + NPC[] characters = this.GetCharacters() + .Where(npc => npc.Sprite != null && this.GetNormalisedPath(npc.Sprite.textureName.Value) == key) + .ToArray(); + if (!characters.Any()) + return false; + + // update portrait + Texture2D texture = content.Load(key); + foreach (NPC character in characters) + this.SetSpriteTexture(character.Sprite, texture); + return true; + } + + /// Reload the portraits for matching NPCs. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any textures were reloaded. + private bool ReloadNpcPortraits(LocalizedContentManager content, string key) + { + // get NPCs + NPC[] villagers = this.GetCharacters() + .Where(npc => npc.isVillager() && this.GetNormalisedPath($"Portraits\\{this.Reflection.GetMethod(npc, "getTextureName").Invoke()}") == key) + .ToArray(); + if (!villagers.Any()) + return false; + + // update portrait + Texture2D texture = content.Load(key); + foreach (NPC villager in villagers) + { + villager.resetPortrait(); + villager.Portrait = texture; + } + + return true; + } + + /// Reload tree textures. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// The type to reload. + /// Returns whether any textures were reloaded. + private bool ReloadTreeTextures(LocalizedContentManager content, string key, int type) + { + Tree[] trees = Game1.locations + .SelectMany(p => p.terrainFeatures.Values.OfType()) + .Where(tree => tree.treeType.Value == type) + .ToArray(); + + if (trees.Any()) + { + Lazy texture = new Lazy(() => content.Load(key)); + foreach (Tree tree in trees) + this.Reflection.GetField>(tree, "texture").SetValue(texture); + return true; + } + + return false; + } + + /**** + ** Reload data methods + ****/ + /// Reload the dialogue data for matching NPCs. + /// The asset key to reload. + /// Returns whether any assets were reloaded. + private bool ReloadNpcDialogue(string key) + { + // get NPCs + string name = Path.GetFileName(key); + NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray(); + if (!villagers.Any()) + return false; + + // update dialogue + foreach (NPC villager in villagers) + villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue + return true; + } + + /// Reload the schedules for matching NPCs. + /// The asset key to reload. + /// Returns whether any assets were reloaded. + private bool ReloadNpcSchedules(string key) + { + // get NPCs + string name = Path.GetFileName(key); + NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray(); + if (!villagers.Any()) + return false; + + // update schedule + foreach (NPC villager in villagers) + { + // reload schedule + villager.Schedule = villager.getSchedule(Game1.dayOfMonth); + if (villager.Schedule == null) + { + this.Monitor.Log($"A mod set an invalid schedule for {villager.Name ?? key}, so the NPC may not behave correctly.", LogLevel.Warn); + return true; + } + + // switch to new schedule if needed + int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault(); + if (lastScheduleTime != 0) + { + this.Reflection.GetField(villager, "scheduleTimeToTry").SetValue(this.Reflection.GetField(typeof(NPC), "NO_TRY").GetValue()); // use time that's passed in to checkSchedule + villager.checkSchedule(lastScheduleTime); + } + } + return true; + } + + /**** + ** Helpers + ****/ + /// Reload the texture for an animated sprite. + /// The animated sprite to update. + /// The texture to set. + private void SetSpriteTexture(AnimatedSprite sprite, Texture2D texture) + { + this.Reflection.GetField(sprite, "spriteTexture").SetValue(texture); + } + + /// Get all NPCs in the game (excluding farm animals). + private IEnumerable GetCharacters() + { + return this.GetLocations().SelectMany(p => p.characters); + } + + /// Get all farm animals in the game. + private IEnumerable GetFarmAnimals() + { + foreach (GameLocation location in this.GetLocations()) + { + if (location is Farm farm) + { + foreach (FarmAnimal animal in farm.animals.Values) + yield return animal; + } + else if (location is AnimalHouse animalHouse) + foreach (FarmAnimal animal in animalHouse.animals.Values) + yield return animal; + } + } + + /// Get all locations in the game. + private IEnumerable GetLocations() + { + // get available root locations + IEnumerable rootLocations = Game1.locations; + if (SaveGame.loaded?.locations != null) + rootLocations = rootLocations.Concat(SaveGame.loaded.locations); + + // yield root + child locations + foreach (GameLocation location in rootLocations) + { + yield return location; + + if (location is BuildableGameLocation buildableLocation) + { + foreach (Building building in buildableLocation.buildings) + { + GameLocation indoors = building.indoors.Value; + if (indoors != null) + yield return indoors; + } + } + } + } + + /// Get whether a key starts with a substring after the substring is normalised. + /// The key to check. + /// The substring to normalise and find. + private bool KeyStartsWith(string key, string rawSubstring) + { + return key.StartsWith(this.GetNormalisedPath(rawSubstring), StringComparison.InvariantCultureIgnoreCase); + } + + /// Get whether a normalised asset key is in the given folder. + /// The normalised asset key (like Animals/cat). + /// The key folder (like Animals); doesn't need to be normalised. + /// Whether to return true if the key is inside a subfolder of the . + private bool IsInFolder(string key, string folder, bool allowSubfolders = false) + { + return + this.KeyStartsWith(key, $"{folder}\\") + && (allowSubfolders || this.CountSegments(key) == this.CountSegments(folder) + 1); + } + + /// Get the segments in a path (e.g. 'a/b' is 'a' and 'b'). + /// The path to check. + private string[] GetSegments(string path) + { + if (path == null) + return new string[0]; + return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + + /// Count the number of segments in a path (e.g. 'a/b' is 2). + /// The path to check. + private int CountSegments(string path) + { + return this.GetSegments(path).Length; + } + } +} diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs new file mode 100644 index 00000000..108271bb --- /dev/null +++ b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Rewriters; +using StardewModdingAPI.Framework.RewriteFacades; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.Metadata +{ + /// Provides CIL instruction handlers which rewrite mods for compatibility and throw exceptions for incompatible code. + internal class InstructionMetadata + { + /********* + ** Fields + *********/ + /// The assembly names to which to heuristically detect broken references. + /// The current implementation only works correctly with assemblies that should always be present. + private readonly string[] ValidateReferencesToAssemblies = { "StardewModdingAPI", "Stardew Valley", "StardewValley", "Netcode" }; + + + /********* + ** Public methods + *********/ + /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. + /// Whether to detect paranoid mode issues. + public IEnumerable GetHandlers(bool paranoidMode) + { + /**** + ** rewrite CIL to fix incompatible code + ****/ + // rewrite for crossplatform compatibility + yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true); + + //Method Rewrites + yield return new MethodParentRewriter(typeof(Game1), typeof(Game1Methods)); + yield return new MethodParentRewriter(typeof(Farmer), typeof(FarmerMethods)); + yield return new MethodParentRewriter(typeof(IClickableMenu), typeof(IClickableMenuMethods)); + yield return new MethodParentRewriter(typeof(FarmerRenderer), typeof(FarmerRendererMethods)); + + //Constructor Rewrites + yield return new MethodParentRewriter(typeof(HUDMessage), typeof(HUDMessageMethods)); + yield return new MethodParentRewriter(typeof(MapPageMethods), typeof(MapPageMethods)); + yield return new MethodParentRewriter(typeof(TextBox), typeof(TextBoxMethods)); + + // rewrite for Stardew Valley 1.3 + yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); + yield return new TypeReferenceRewriter("System.Collections.Generic.IList`1", typeof(List)); + yield return new FieldToPropertyRewriter(typeof(Game1), "player"); + yield return new FieldToPropertyRewriter(typeof(Game1), "currentLocation"); + yield return new FieldToPropertyRewriter(typeof(Character), "currentLocation"); + yield return new FieldToPropertyRewriter(typeof(Farmer), "currentLocation"); + yield return new FieldToPropertyRewriter(typeof(Game1), "gameMode"); + yield return new FieldToPropertyRewriter(typeof(Game1), "currentMinigame"); + yield return new FieldToPropertyRewriter(typeof(Game1), "activeClickableMenu"); + yield return new FieldToPropertyRewriter(typeof(Game1), "stats"); + + //isRaining and isDebrisWeather fix 50% done. + yield return new TypeFieldToAnotherTypeFieldRewriter(typeof(Game1), typeof(RainManager), "isRaining", "Instance"); + yield return new TypeFieldToAnotherTypeFieldRewriter(typeof(Game1), typeof(DebrisManager), "isDebrisWeather", "Instance"); + + /**** + ** detect mod issues + ****/ + // detect broken code + yield return new ReferenceToMissingMemberFinder(this.ValidateReferencesToAssemblies); + yield return new ReferenceToMemberWithUnexpectedTypeFinder(this.ValidateReferencesToAssemblies); + + /**** + ** detect code which may impact game stability + ****/ + yield return new TypeFinder("Harmony.HarmonyInstance", InstructionHandleResult.DetectedGamePatch); + yield return new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic); + yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser); + yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser); + yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser); + yield return new EventFinder(typeof(ISpecialisedEvents).FullName, nameof(ISpecialisedEvents.UnvalidatedUpdateTicked), InstructionHandleResult.DetectedUnvalidatedUpdateTick); + yield return new EventFinder(typeof(ISpecialisedEvents).FullName, nameof(ISpecialisedEvents.UnvalidatedUpdateTicking), InstructionHandleResult.DetectedUnvalidatedUpdateTick); +#if !SMAPI_3_0_STRICT + yield return new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick); +#endif + + /**** + ** detect paranoid issues + ****/ + if (paranoidMode) + { + // filesystem access + yield return new TypeFinder(typeof(System.IO.File).FullName, InstructionHandleResult.DetectedFilesystemAccess); + yield return new TypeFinder(typeof(System.IO.FileStream).FullName, InstructionHandleResult.DetectedFilesystemAccess); + yield return new TypeFinder(typeof(System.IO.FileInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess); + yield return new TypeFinder(typeof(System.IO.Directory).FullName, InstructionHandleResult.DetectedFilesystemAccess); + yield return new TypeFinder(typeof(System.IO.DirectoryInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess); + yield return new TypeFinder(typeof(System.IO.DriveInfo).FullName, InstructionHandleResult.DetectedFilesystemAccess); + yield return new TypeFinder(typeof(System.IO.FileSystemWatcher).FullName, InstructionHandleResult.DetectedFilesystemAccess); + + // shell access + yield return new TypeFinder(typeof(System.Diagnostics.Process).FullName, InstructionHandleResult.DetectedShellAccess); + } + } + } +} diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs new file mode 100644 index 00000000..3a753afc --- /dev/null +++ b/src/StardewModdingAPI/Mod.cs @@ -0,0 +1,53 @@ +using System; + +namespace StardewModdingAPI +{ + /// The base class for a mod. + public abstract class Mod : IMod, IDisposable + { + /********* + ** Accessors + *********/ + /// Provides simplified APIs for writing mods. + public IModHelper Helper { get; internal set; } + + /// Writes messages to the console and log file. + public IMonitor Monitor { get; internal set; } + + /// The mod's manifest. + public IManifest ModManifest { get; internal set; } + + + /********* + ** Public methods + *********/ + /// The mod entry point, called after the mod is first loaded. + /// Provides simplified APIs for writing mods. + public abstract void Entry(IModHelper helper); + + /// Get an API that other mods can access. This is always called after . + public virtual object GetApi() => null; + + /// Release or reset unmanaged resources. + public void Dispose() + { + (this.Helper as IDisposable)?.Dispose(); // deliberate do this outside overridable dispose method so mods don't accidentally suppress it + this.Dispose(true); + GC.SuppressFinalize(this); + } + + + /********* + ** Private methods + *********/ + /// Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit. + /// Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised. + protected virtual void Dispose(bool disposing) { } + + /// Destruct the instance. + ~Mod() + { + this.Dispose(false); + } + } +} diff --git a/src/StardewModdingAPI/PatchMode.cs b/src/StardewModdingAPI/PatchMode.cs new file mode 100644 index 00000000..b4286a89 --- /dev/null +++ b/src/StardewModdingAPI/PatchMode.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// Indicates how an image should be patched. + public enum PatchMode + { + /// Erase the original content within the area before drawing the new content. + Replace, + + /// Draw the new content over the original content, so the original content shows through any transparent pixels. + Overlay + } +} diff --git a/src/StardewModdingAPI/Patches/DialogueErrorPatch.cs b/src/StardewModdingAPI/Patches/DialogueErrorPatch.cs new file mode 100644 index 00000000..d8905fd1 --- /dev/null +++ b/src/StardewModdingAPI/Patches/DialogueErrorPatch.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Harmony; +using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch for the constructor which intercepts invalid dialogue lines and logs an error instead of crashing. + internal class DialogueErrorPatch : IHarmonyPatch + { + /********* + ** Private methods + *********/ + /// Writes messages to the console and log file on behalf of the game. + private static IMonitor MonitorForGame; + + /// Simplifies access to private code. + private static Reflector Reflection; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(DialogueErrorPatch)}"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the console and log file on behalf of the game. + /// Simplifies access to private code. + public DialogueErrorPatch(IMonitor monitorForGame, Reflector reflector) + { + DialogueErrorPatch.MonitorForGame = monitorForGame; + DialogueErrorPatch.Reflection = reflector; + } + + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + ConstructorInfo constructor = AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(DialogueErrorPatch.Prefix)); + + harmony.Patch(constructor, new HarmonyMethod(prefix), null); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of the Dialogue constructor. + /// The instance being patched. + /// The dialogue being parsed. + /// The NPC for which the dialogue is being parsed. + /// Returns whether to execute the original method. + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] + private static bool Prefix(Dialogue __instance, string masterDialogue, NPC speaker) + { + // get private members + bool nameArraysTranslated = DialogueErrorPatch.Reflection.GetField(typeof(Dialogue), "nameArraysTranslated").GetValue(); + IReflectedMethod translateArraysOfStrings = DialogueErrorPatch.Reflection.GetMethod(typeof(Dialogue), "TranslateArraysOfStrings"); + IReflectedMethod parseDialogueString = DialogueErrorPatch.Reflection.GetMethod(__instance, "parseDialogueString"); + IReflectedMethod checkForSpecialDialogueAttributes = DialogueErrorPatch.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); + IReflectedField> dialogues = DialogueErrorPatch.Reflection.GetField>(__instance, "dialogues"); + + // replicate base constructor + if (dialogues.GetValue() == null) + dialogues.SetValue(new List()); + + // duplicate code with try..catch + try + { + if (!nameArraysTranslated) + translateArraysOfStrings.Invoke(); + __instance.speaker = speaker; + parseDialogueString.Invoke(masterDialogue); + checkForSpecialDialogueAttributes.Invoke(); + } + catch (Exception baseEx) when (baseEx.InnerException is TargetInvocationException invocationEx && invocationEx.InnerException is Exception ex) + { + string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; + DialogueErrorPatch.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{ex}", LogLevel.Error); + + parseDialogueString.Invoke("..."); + checkForSpecialDialogueAttributes.Invoke(); + } + + return false; + } + } +} diff --git a/src/StardewModdingAPI/Patches/LoadForNewGamePatch.cs b/src/StardewModdingAPI/Patches/LoadForNewGamePatch.cs new file mode 100644 index 00000000..9e788e84 --- /dev/null +++ b/src/StardewModdingAPI/Patches/LoadForNewGamePatch.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Reflection; +using Harmony; +using StardewModdingAPI.Enums; +using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch for which notifies SMAPI for save creation load stages. + /// This patch hooks into , checks if TitleMenu.transitioningCharacterCreationMenu is true (which means the player is creating a new save file), then raises after the location list is cleared twice (the second clear happens right before locations are created), and when the method ends. + internal class LoadForNewGamePatch : IHarmonyPatch + { + /********* + ** Accessors + *********/ + /// Simplifies access to private code. + private static Reflector Reflection; + + /// A callback to invoke when the load stage changes. + private static Action OnStageChanged; + + /// Whether was called as part of save creation. + private static bool IsCreating; + + /// The number of times that has been cleared since started. + private static int TimesLocationsCleared = 0; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(LoadForNewGamePatch)}"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Simplifies access to private code. + /// A callback to invoke when the load stage changes. + public LoadForNewGamePatch(Reflector reflection, Action onStageChanged) + { + LoadForNewGamePatch.Reflection = reflection; + LoadForNewGamePatch.OnStageChanged = onStageChanged; + } + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + MethodInfo method = AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LoadForNewGamePatch.Prefix)); + MethodInfo postfix = AccessTools.Method(this.GetType(), nameof(LoadForNewGamePatch.Postfix)); + + harmony.Patch(method, new HarmonyMethod(prefix), new HarmonyMethod(postfix)); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of . + /// Returns whether to execute the original method. + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + private static bool Prefix() + { + LoadForNewGamePatch.IsCreating = Game1.activeClickableMenu is TitleMenu menu && LoadForNewGamePatch.Reflection.GetField(menu, "transitioningCharacterCreationMenu").GetValue(); + LoadForNewGamePatch.TimesLocationsCleared = 0; + if (LoadForNewGamePatch.IsCreating) + { + // raise CreatedBasicInfo after locations are cleared twice + ObservableCollection locations = (ObservableCollection)Game1.locations; + locations.CollectionChanged += LoadForNewGamePatch.OnLocationListChanged; + } + + return true; + } + + /// The method to call instead after . + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + private static void Postfix() + { + if (LoadForNewGamePatch.IsCreating) + { + // clean up + ObservableCollection locations = (ObservableCollection) Game1.locations; + locations.CollectionChanged -= LoadForNewGamePatch.OnLocationListChanged; + + // raise stage changed + LoadForNewGamePatch.OnStageChanged(LoadStage.CreatedLocations); + } + } + + /// Raised when changes. + /// The event sender. + /// The event arguments. + private static void OnLocationListChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (++LoadForNewGamePatch.TimesLocationsCleared == 2) + LoadForNewGamePatch.OnStageChanged(LoadStage.CreatedBasicInfo); + } + } +} diff --git a/src/StardewModdingAPI/Patches/ObjectErrorPatch.cs b/src/StardewModdingAPI/Patches/ObjectErrorPatch.cs new file mode 100644 index 00000000..0481259d --- /dev/null +++ b/src/StardewModdingAPI/Patches/ObjectErrorPatch.cs @@ -0,0 +1,55 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Harmony; +using StardewModdingAPI.Framework.Patching; +using StardewValley; +using SObject = StardewValley.Object; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch for which intercepts crashes due to the item no longer existing. + internal class ObjectErrorPatch : IHarmonyPatch + { + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(ObjectErrorPatch)}"; + + + /********* + ** Public methods + *********/ + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + MethodInfo method = AccessTools.Method(typeof(SObject), nameof(SObject.getDescription)); + MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(ObjectErrorPatch.Prefix)); + + harmony.Patch(method, new HarmonyMethod(prefix), null); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of . + /// The instance being patched. + /// The patched method's return value. + /// Returns whether to execute the original method. + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")] + private static bool Prefix(SObject __instance, ref string __result) + { + // invalid bigcraftables crash instead of showing '???' like invalid non-bigcraftables + if (!__instance.IsRecipe && __instance.bigCraftable.Value && !Game1.bigCraftablesInformation.ContainsKey(__instance.ParentSheetIndex)) + { + __result = "???"; + return false; + } + + return true; + } + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs new file mode 100644 index 00000000..3211f009 --- /dev/null +++ b/src/StardewModdingAPI/Program.cs @@ -0,0 +1,154 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +#if SMAPI_FOR_WINDOWS +#endif +using StardewModdingAPI.Framework; +using StardewModdingAPI.Internal; + +namespace StardewModdingAPI +{ + /// The main entry point for SMAPI, responsible for hooking into and launching the game. + internal class Program + { + /********* + ** Fields + *********/ + /// The absolute path to search for SMAPI's internal DLLs. + /// We can't use directly, since depends on DLLs loaded from this folder. + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")] + internal static readonly string DllSearchPath = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "StardewValley/smapi-internal"); + + + /********* + ** Public methods + *********/ + /// The main entry point which hooks into and launches the game. + /// The command-line arguments. + public static void Main(string[] args) + { + AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; + //Program.AssertGamePresent(); + //Program.AssertGameVersion(); + //Program.Start(args); + } + + + /********* + ** Private methods + *********/ + /// Method called when assembly resolution fails, which may return a manually resolved assembly. + /// The event sender. + /// The event arguments. + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) + { + try + { + AssemblyName name = new AssemblyName(e.Name); + foreach (FileInfo dll in new DirectoryInfo(Program.DllSearchPath).EnumerateFiles("*.dll")) + { + if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.InvariantCultureIgnoreCase)) + { + Android.Util.Log.Error("Program", $"Resolving assembly: {dll.FullName}"); + return Assembly.LoadFrom(dll.FullName); + } + + } + return null; + } + catch (Exception ex) + { + return null; + } + } + + /// Assert that the game is available. + /// This must be checked *before* any references to , and this method should not reference itself to avoid errors in Mono. + private static void AssertGamePresent() + { + Platform platform = EnvironmentUtility.DetectPlatform(); + string gameAssemblyName = platform == Platform.Windows ? "Stardew Valley" : "StardewValley"; + if (Type.GetType($"StardewValley.Game1, {gameAssemblyName}", throwOnError: false) == null) + { + Program.PrintErrorAndExit( + "Oops! SMAPI can't find the game. " + + (Assembly.GetCallingAssembly().Location.Contains(Path.Combine("internal", "Windows")) || Assembly.GetCallingAssembly().Location.Contains(Path.Combine("internal", "Mono")) + ? "It looks like you're running SMAPI from the download package, but you need to run the installed version instead. " + : "Make sure you're running StardewModdingAPI.exe in your game folder. " + ) + + "See the readme.txt file for details." + ); + } + } + + /// Assert that the game version is within and . + private static void AssertGameVersion() + { + // min version + if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) + { + 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." + ); + } + + // max version + else if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) + Program.PrintErrorAndExit($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI: https://smapi.io."); + + } + + /// Initialise SMAPI and launch the game. + /// The command-line arguments. + /// This method is separate from because that can't contain any references to assemblies loaded by (e.g. via ), or Mono will incorrectly show an assembly resolution error before assembly resolution is set up. + private static void Start(string[] args) + { + // get flags from arguments + bool writeToConsole = !args.Contains("--no-terminal"); + + // get mods path from arguments + string modsPath = null; + { + int pathIndex = Array.LastIndexOf(args, "--mods-path") + 1; + if (pathIndex >= 1 && args.Length >= pathIndex) + { + modsPath = args[pathIndex]; + if (!string.IsNullOrWhiteSpace(modsPath) && !Path.IsPathRooted(modsPath)) + modsPath = Path.Combine(Constants.ExecutionPath, modsPath); + } + if (string.IsNullOrWhiteSpace(modsPath)) + modsPath = Constants.DefaultModsPath; + } + + // load SMAPI + using (SCore core = new SCore(modsPath, writeToConsole)) + core.RunInteractively(); + } + + /// Write an error directly to the console and exit. + /// The error message to display. + private static void PrintErrorAndExit(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + Program.PressAnyKeyToExit(showMessage: true); + } + + /// Show a 'press any key to exit' message, and exit when they press a key. + /// Whether to print a 'press any key to exit' message to the console. + private static void PressAnyKeyToExit(bool showMessage) + { + if (showMessage) + Console.WriteLine("Game has ended. Press any key to exit."); + Thread.Sleep(100); + Console.ReadKey(); + Environment.Exit(0); + } + } +} diff --git a/src/StardewModdingAPI/Properties/AssemblyInfo.cs b/src/StardewModdingAPI/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..55678c45 --- /dev/null +++ b/src/StardewModdingAPI/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Android.App; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("StardewModdingAPI")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("StardewModdingAPI")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/StardewModdingAPI/Resources/AboutResources.txt b/src/StardewModdingAPI/Resources/AboutResources.txt new file mode 100644 index 00000000..c2bca974 --- /dev/null +++ b/src/StardewModdingAPI/Resources/AboutResources.txt @@ -0,0 +1,44 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.axml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable/ + icon.png + + layout/ + main.axml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called "R" +(this is an Android convention) that contains the tokens for each one of the resources +included. For example, for the above Resources layout, this is what the R class would expose: + +public class R { + public class drawable { + public const int icon = 0x123; + } + + public class layout { + public const int main = 0x456; + } + + public class strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main +to reference the layout/main.axml file, or R.strings.first_string to reference the first +string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/src/StardewModdingAPI/Resources/Resource.designer.cs b/src/StardewModdingAPI/Resources/Resource.designer.cs new file mode 100644 index 00000000..ba22531c --- /dev/null +++ b/src/StardewModdingAPI/Resources/Resource.designer.cs @@ -0,0 +1,5484 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +[assembly: global::Android.Runtime.ResourceDesignerAttribute("StardewModdingAPI.Resource", IsApplication=false)] + +namespace StardewModdingAPI +{ + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + public partial class Resource + { + + static Resource() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + public partial class Animation + { + + // aapt resource value: 0x7f040000 + public static int abc_fade_in = 2130968576; + + // aapt resource value: 0x7f040001 + public static int abc_fade_out = 2130968577; + + // aapt resource value: 0x7f040002 + public static int abc_grow_fade_in_from_bottom = 2130968578; + + // aapt resource value: 0x7f040003 + public static int abc_popup_enter = 2130968579; + + // aapt resource value: 0x7f040004 + public static int abc_popup_exit = 2130968580; + + // aapt resource value: 0x7f040005 + public static int abc_shrink_fade_out_from_bottom = 2130968581; + + // aapt resource value: 0x7f040006 + public static int abc_slide_in_bottom = 2130968582; + + // aapt resource value: 0x7f040007 + public static int abc_slide_in_top = 2130968583; + + // aapt resource value: 0x7f040008 + public static int abc_slide_out_bottom = 2130968584; + + // aapt resource value: 0x7f040009 + public static int abc_slide_out_top = 2130968585; + + // aapt resource value: 0x7f04000a + public static int abc_tooltip_enter = 2130968586; + + // aapt resource value: 0x7f04000b + public static int abc_tooltip_exit = 2130968587; + + static Animation() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Animation() + { + } + } + + public partial class Attribute + { + + // aapt resource value: 0x7f01004d + public static int actionBarDivider = 2130772045; + + // aapt resource value: 0x7f01004e + public static int actionBarItemBackground = 2130772046; + + // aapt resource value: 0x7f010047 + public static int actionBarPopupTheme = 2130772039; + + // aapt resource value: 0x7f01004c + public static int actionBarSize = 2130772044; + + // aapt resource value: 0x7f010049 + public static int actionBarSplitStyle = 2130772041; + + // aapt resource value: 0x7f010048 + public static int actionBarStyle = 2130772040; + + // aapt resource value: 0x7f010043 + public static int actionBarTabBarStyle = 2130772035; + + // aapt resource value: 0x7f010042 + public static int actionBarTabStyle = 2130772034; + + // aapt resource value: 0x7f010044 + public static int actionBarTabTextStyle = 2130772036; + + // aapt resource value: 0x7f01004a + public static int actionBarTheme = 2130772042; + + // aapt resource value: 0x7f01004b + public static int actionBarWidgetTheme = 2130772043; + + // aapt resource value: 0x7f010069 + public static int actionButtonStyle = 2130772073; + + // aapt resource value: 0x7f010065 + public static int actionDropDownStyle = 2130772069; + + // aapt resource value: 0x7f0100c0 + public static int actionLayout = 2130772160; + + // aapt resource value: 0x7f01004f + public static int actionMenuTextAppearance = 2130772047; + + // aapt resource value: 0x7f010050 + public static int actionMenuTextColor = 2130772048; + + // aapt resource value: 0x7f010053 + public static int actionModeBackground = 2130772051; + + // aapt resource value: 0x7f010052 + public static int actionModeCloseButtonStyle = 2130772050; + + // aapt resource value: 0x7f010055 + public static int actionModeCloseDrawable = 2130772053; + + // aapt resource value: 0x7f010057 + public static int actionModeCopyDrawable = 2130772055; + + // aapt resource value: 0x7f010056 + public static int actionModeCutDrawable = 2130772054; + + // aapt resource value: 0x7f01005b + public static int actionModeFindDrawable = 2130772059; + + // aapt resource value: 0x7f010058 + public static int actionModePasteDrawable = 2130772056; + + // aapt resource value: 0x7f01005d + public static int actionModePopupWindowStyle = 2130772061; + + // aapt resource value: 0x7f010059 + public static int actionModeSelectAllDrawable = 2130772057; + + // aapt resource value: 0x7f01005a + public static int actionModeShareDrawable = 2130772058; + + // aapt resource value: 0x7f010054 + public static int actionModeSplitBackground = 2130772052; + + // aapt resource value: 0x7f010051 + public static int actionModeStyle = 2130772049; + + // aapt resource value: 0x7f01005c + public static int actionModeWebSearchDrawable = 2130772060; + + // aapt resource value: 0x7f010045 + public static int actionOverflowButtonStyle = 2130772037; + + // aapt resource value: 0x7f010046 + public static int actionOverflowMenuStyle = 2130772038; + + // aapt resource value: 0x7f0100c2 + public static int actionProviderClass = 2130772162; + + // aapt resource value: 0x7f0100c1 + public static int actionViewClass = 2130772161; + + // aapt resource value: 0x7f010071 + public static int activityChooserViewStyle = 2130772081; + + // aapt resource value: 0x7f010096 + public static int alertDialogButtonGroupStyle = 2130772118; + + // aapt resource value: 0x7f010097 + public static int alertDialogCenterButtons = 2130772119; + + // aapt resource value: 0x7f010095 + public static int alertDialogStyle = 2130772117; + + // aapt resource value: 0x7f010098 + public static int alertDialogTheme = 2130772120; + + // aapt resource value: 0x7f0100af + public static int allowStacking = 2130772143; + + // aapt resource value: 0x7f010104 + public static int alpha = 2130772228; + + // aapt resource value: 0x7f0100bd + public static int alphabeticModifiers = 2130772157; + + // aapt resource value: 0x7f0100b6 + public static int arrowHeadLength = 2130772150; + + // aapt resource value: 0x7f0100b7 + public static int arrowShaftLength = 2130772151; + + // aapt resource value: 0x7f01009d + public static int autoCompleteTextViewStyle = 2130772125; + + // aapt resource value: 0x7f010033 + public static int autoSizeMaxTextSize = 2130772019; + + // aapt resource value: 0x7f010032 + public static int autoSizeMinTextSize = 2130772018; + + // aapt resource value: 0x7f010031 + public static int autoSizePresetSizes = 2130772017; + + // aapt resource value: 0x7f010030 + public static int autoSizeStepGranularity = 2130772016; + + // aapt resource value: 0x7f01002f + public static int autoSizeTextType = 2130772015; + + // aapt resource value: 0x7f01000c + public static int background = 2130771980; + + // aapt resource value: 0x7f01000e + public static int backgroundSplit = 2130771982; + + // aapt resource value: 0x7f01000d + public static int backgroundStacked = 2130771981; + + // aapt resource value: 0x7f0100f9 + public static int backgroundTint = 2130772217; + + // aapt resource value: 0x7f0100fa + public static int backgroundTintMode = 2130772218; + + // aapt resource value: 0x7f0100b8 + public static int barLength = 2130772152; + + // aapt resource value: 0x7f01006e + public static int borderlessButtonStyle = 2130772078; + + // aapt resource value: 0x7f01006b + public static int buttonBarButtonStyle = 2130772075; + + // aapt resource value: 0x7f01009b + public static int buttonBarNegativeButtonStyle = 2130772123; + + // aapt resource value: 0x7f01009c + public static int buttonBarNeutralButtonStyle = 2130772124; + + // aapt resource value: 0x7f01009a + public static int buttonBarPositiveButtonStyle = 2130772122; + + // aapt resource value: 0x7f01006a + public static int buttonBarStyle = 2130772074; + + // aapt resource value: 0x7f0100ee + public static int buttonGravity = 2130772206; + + // aapt resource value: 0x7f010027 + public static int buttonIconDimen = 2130772007; + + // aapt resource value: 0x7f010021 + public static int buttonPanelSideLayout = 2130772001; + + // aapt resource value: 0x7f01009e + public static int buttonStyle = 2130772126; + + // aapt resource value: 0x7f01009f + public static int buttonStyleSmall = 2130772127; + + // aapt resource value: 0x7f0100b0 + public static int buttonTint = 2130772144; + + // aapt resource value: 0x7f0100b1 + public static int buttonTintMode = 2130772145; + + // aapt resource value: 0x7f0100a0 + public static int checkboxStyle = 2130772128; + + // aapt resource value: 0x7f0100a1 + public static int checkedTextViewStyle = 2130772129; + + // aapt resource value: 0x7f0100d1 + public static int closeIcon = 2130772177; + + // aapt resource value: 0x7f01001e + public static int closeItemLayout = 2130771998; + + // aapt resource value: 0x7f0100f0 + public static int collapseContentDescription = 2130772208; + + // aapt resource value: 0x7f0100ef + public static int collapseIcon = 2130772207; + + // aapt resource value: 0x7f0100b2 + public static int color = 2130772146; + + // aapt resource value: 0x7f01008d + public static int colorAccent = 2130772109; + + // aapt resource value: 0x7f010094 + public static int colorBackgroundFloating = 2130772116; + + // aapt resource value: 0x7f010091 + public static int colorButtonNormal = 2130772113; + + // aapt resource value: 0x7f01008f + public static int colorControlActivated = 2130772111; + + // aapt resource value: 0x7f010090 + public static int colorControlHighlight = 2130772112; + + // aapt resource value: 0x7f01008e + public static int colorControlNormal = 2130772110; + + // aapt resource value: 0x7f0100ad + public static int colorError = 2130772141; + + // aapt resource value: 0x7f01008b + public static int colorPrimary = 2130772107; + + // aapt resource value: 0x7f01008c + public static int colorPrimaryDark = 2130772108; + + // aapt resource value: 0x7f010092 + public static int colorSwitchThumbNormal = 2130772114; + + // aapt resource value: 0x7f0100d6 + public static int commitIcon = 2130772182; + + // aapt resource value: 0x7f0100c3 + public static int contentDescription = 2130772163; + + // aapt resource value: 0x7f010017 + public static int contentInsetEnd = 2130771991; + + // aapt resource value: 0x7f01001b + public static int contentInsetEndWithActions = 2130771995; + + // aapt resource value: 0x7f010018 + public static int contentInsetLeft = 2130771992; + + // aapt resource value: 0x7f010019 + public static int contentInsetRight = 2130771993; + + // aapt resource value: 0x7f010016 + public static int contentInsetStart = 2130771990; + + // aapt resource value: 0x7f01001a + public static int contentInsetStartWithNavigation = 2130771994; + + // aapt resource value: 0x7f010093 + public static int controlBackground = 2130772115; + + // aapt resource value: 0x7f0100fb + public static int coordinatorLayoutStyle = 2130772219; + + // aapt resource value: 0x7f01000f + public static int customNavigationLayout = 2130771983; + + // aapt resource value: 0x7f0100d0 + public static int defaultQueryHint = 2130772176; + + // aapt resource value: 0x7f010064 + public static int dialogCornerRadius = 2130772068; + + // aapt resource value: 0x7f010062 + public static int dialogPreferredPadding = 2130772066; + + // aapt resource value: 0x7f010061 + public static int dialogTheme = 2130772065; + + // aapt resource value: 0x7f010005 + public static int displayOptions = 2130771973; + + // aapt resource value: 0x7f01000b + public static int divider = 2130771979; + + // aapt resource value: 0x7f010070 + public static int dividerHorizontal = 2130772080; + + // aapt resource value: 0x7f0100bc + public static int dividerPadding = 2130772156; + + // aapt resource value: 0x7f01006f + public static int dividerVertical = 2130772079; + + // aapt resource value: 0x7f0100b4 + public static int drawableSize = 2130772148; + + // aapt resource value: 0x7f010000 + public static int drawerArrowStyle = 2130771968; + + // aapt resource value: 0x7f010082 + public static int dropDownListViewStyle = 2130772098; + + // aapt resource value: 0x7f010066 + public static int dropdownListPreferredItemHeight = 2130772070; + + // aapt resource value: 0x7f010077 + public static int editTextBackground = 2130772087; + + // aapt resource value: 0x7f010076 + public static int editTextColor = 2130772086; + + // aapt resource value: 0x7f0100a2 + public static int editTextStyle = 2130772130; + + // aapt resource value: 0x7f01001c + public static int elevation = 2130771996; + + // aapt resource value: 0x7f010020 + public static int expandActivityOverflowButtonDrawable = 2130772000; + + // aapt resource value: 0x7f010036 + public static int firstBaselineToTopHeight = 2130772022; + + // aapt resource value: 0x7f01010c + public static int font = 2130772236; + + // aapt resource value: 0x7f010034 + public static int fontFamily = 2130772020; + + // aapt resource value: 0x7f010105 + public static int fontProviderAuthority = 2130772229; + + // aapt resource value: 0x7f010108 + public static int fontProviderCerts = 2130772232; + + // aapt resource value: 0x7f010109 + public static int fontProviderFetchStrategy = 2130772233; + + // aapt resource value: 0x7f01010a + public static int fontProviderFetchTimeout = 2130772234; + + // aapt resource value: 0x7f010106 + public static int fontProviderPackage = 2130772230; + + // aapt resource value: 0x7f010107 + public static int fontProviderQuery = 2130772231; + + // aapt resource value: 0x7f01010b + public static int fontStyle = 2130772235; + + // aapt resource value: 0x7f01010e + public static int fontVariationSettings = 2130772238; + + // aapt resource value: 0x7f01010d + public static int fontWeight = 2130772237; + + // aapt resource value: 0x7f0100b5 + public static int gapBetweenBars = 2130772149; + + // aapt resource value: 0x7f0100d2 + public static int goIcon = 2130772178; + + // aapt resource value: 0x7f010001 + public static int height = 2130771969; + + // aapt resource value: 0x7f010015 + public static int hideOnContentScroll = 2130771989; + + // aapt resource value: 0x7f010068 + public static int homeAsUpIndicator = 2130772072; + + // aapt resource value: 0x7f010010 + public static int homeLayout = 2130771984; + + // aapt resource value: 0x7f010009 + public static int icon = 2130771977; + + // aapt resource value: 0x7f0100c5 + public static int iconTint = 2130772165; + + // aapt resource value: 0x7f0100c6 + public static int iconTintMode = 2130772166; + + // aapt resource value: 0x7f0100ce + public static int iconifiedByDefault = 2130772174; + + // aapt resource value: 0x7f010078 + public static int imageButtonStyle = 2130772088; + + // aapt resource value: 0x7f010012 + public static int indeterminateProgressStyle = 2130771986; + + // aapt resource value: 0x7f01001f + public static int initialActivityCount = 2130771999; + + // aapt resource value: 0x7f010002 + public static int isLightTheme = 2130771970; + + // aapt resource value: 0x7f010014 + public static int itemPadding = 2130771988; + + // aapt resource value: 0x7f0100fc + public static int keylines = 2130772220; + + // aapt resource value: 0x7f010037 + public static int lastBaselineToBottomHeight = 2130772023; + + // aapt resource value: 0x7f0100cd + public static int layout = 2130772173; + + // aapt resource value: 0x7f0100ff + public static int layout_anchor = 2130772223; + + // aapt resource value: 0x7f010101 + public static int layout_anchorGravity = 2130772225; + + // aapt resource value: 0x7f0100fe + public static int layout_behavior = 2130772222; + + // aapt resource value: 0x7f010103 + public static int layout_dodgeInsetEdges = 2130772227; + + // aapt resource value: 0x7f010102 + public static int layout_insetEdge = 2130772226; + + // aapt resource value: 0x7f010100 + public static int layout_keyline = 2130772224; + + // aapt resource value: 0x7f010035 + public static int lineHeight = 2130772021; + + // aapt resource value: 0x7f01008a + public static int listChoiceBackgroundIndicator = 2130772106; + + // aapt resource value: 0x7f010063 + public static int listDividerAlertDialog = 2130772067; + + // aapt resource value: 0x7f010025 + public static int listItemLayout = 2130772005; + + // aapt resource value: 0x7f010022 + public static int listLayout = 2130772002; + + // aapt resource value: 0x7f0100aa + public static int listMenuViewStyle = 2130772138; + + // aapt resource value: 0x7f010083 + public static int listPopupWindowStyle = 2130772099; + + // aapt resource value: 0x7f01007d + public static int listPreferredItemHeight = 2130772093; + + // aapt resource value: 0x7f01007f + public static int listPreferredItemHeightLarge = 2130772095; + + // aapt resource value: 0x7f01007e + public static int listPreferredItemHeightSmall = 2130772094; + + // aapt resource value: 0x7f010080 + public static int listPreferredItemPaddingLeft = 2130772096; + + // aapt resource value: 0x7f010081 + public static int listPreferredItemPaddingRight = 2130772097; + + // aapt resource value: 0x7f01000a + public static int logo = 2130771978; + + // aapt resource value: 0x7f0100f3 + public static int logoDescription = 2130772211; + + // aapt resource value: 0x7f0100ed + public static int maxButtonHeight = 2130772205; + + // aapt resource value: 0x7f0100ba + public static int measureWithLargestChild = 2130772154; + + // aapt resource value: 0x7f010023 + public static int multiChoiceItemLayout = 2130772003; + + // aapt resource value: 0x7f0100f2 + public static int navigationContentDescription = 2130772210; + + // aapt resource value: 0x7f0100f1 + public static int navigationIcon = 2130772209; + + // aapt resource value: 0x7f010004 + public static int navigationMode = 2130771972; + + // aapt resource value: 0x7f0100be + public static int numericModifiers = 2130772158; + + // aapt resource value: 0x7f0100c9 + public static int overlapAnchor = 2130772169; + + // aapt resource value: 0x7f0100cb + public static int paddingBottomNoButtons = 2130772171; + + // aapt resource value: 0x7f0100f7 + public static int paddingEnd = 2130772215; + + // aapt resource value: 0x7f0100f6 + public static int paddingStart = 2130772214; + + // aapt resource value: 0x7f0100cc + public static int paddingTopNoTitle = 2130772172; + + // aapt resource value: 0x7f010087 + public static int panelBackground = 2130772103; + + // aapt resource value: 0x7f010089 + public static int panelMenuListTheme = 2130772105; + + // aapt resource value: 0x7f010088 + public static int panelMenuListWidth = 2130772104; + + // aapt resource value: 0x7f010074 + public static int popupMenuStyle = 2130772084; + + // aapt resource value: 0x7f01001d + public static int popupTheme = 2130771997; + + // aapt resource value: 0x7f010075 + public static int popupWindowStyle = 2130772085; + + // aapt resource value: 0x7f0100c7 + public static int preserveIconSpacing = 2130772167; + + // aapt resource value: 0x7f010013 + public static int progressBarPadding = 2130771987; + + // aapt resource value: 0x7f010011 + public static int progressBarStyle = 2130771985; + + // aapt resource value: 0x7f0100d8 + public static int queryBackground = 2130772184; + + // aapt resource value: 0x7f0100cf + public static int queryHint = 2130772175; + + // aapt resource value: 0x7f0100a3 + public static int radioButtonStyle = 2130772131; + + // aapt resource value: 0x7f0100a4 + public static int ratingBarStyle = 2130772132; + + // aapt resource value: 0x7f0100a5 + public static int ratingBarStyleIndicator = 2130772133; + + // aapt resource value: 0x7f0100a6 + public static int ratingBarStyleSmall = 2130772134; + + // aapt resource value: 0x7f0100d4 + public static int searchHintIcon = 2130772180; + + // aapt resource value: 0x7f0100d3 + public static int searchIcon = 2130772179; + + // aapt resource value: 0x7f01007c + public static int searchViewStyle = 2130772092; + + // aapt resource value: 0x7f0100a7 + public static int seekBarStyle = 2130772135; + + // aapt resource value: 0x7f01006c + public static int selectableItemBackground = 2130772076; + + // aapt resource value: 0x7f01006d + public static int selectableItemBackgroundBorderless = 2130772077; + + // aapt resource value: 0x7f0100bf + public static int showAsAction = 2130772159; + + // aapt resource value: 0x7f0100bb + public static int showDividers = 2130772155; + + // aapt resource value: 0x7f0100e4 + public static int showText = 2130772196; + + // aapt resource value: 0x7f010026 + public static int showTitle = 2130772006; + + // aapt resource value: 0x7f010024 + public static int singleChoiceItemLayout = 2130772004; + + // aapt resource value: 0x7f0100b3 + public static int spinBars = 2130772147; + + // aapt resource value: 0x7f010067 + public static int spinnerDropDownItemStyle = 2130772071; + + // aapt resource value: 0x7f0100a8 + public static int spinnerStyle = 2130772136; + + // aapt resource value: 0x7f0100e3 + public static int splitTrack = 2130772195; + + // aapt resource value: 0x7f010028 + public static int srcCompat = 2130772008; + + // aapt resource value: 0x7f0100ca + public static int state_above_anchor = 2130772170; + + // aapt resource value: 0x7f0100fd + public static int statusBarBackground = 2130772221; + + // aapt resource value: 0x7f0100c8 + public static int subMenuArrow = 2130772168; + + // aapt resource value: 0x7f0100d9 + public static int submitBackground = 2130772185; + + // aapt resource value: 0x7f010006 + public static int subtitle = 2130771974; + + // aapt resource value: 0x7f0100e6 + public static int subtitleTextAppearance = 2130772198; + + // aapt resource value: 0x7f0100f5 + public static int subtitleTextColor = 2130772213; + + // aapt resource value: 0x7f010008 + public static int subtitleTextStyle = 2130771976; + + // aapt resource value: 0x7f0100d7 + public static int suggestionRowLayout = 2130772183; + + // aapt resource value: 0x7f0100e1 + public static int switchMinWidth = 2130772193; + + // aapt resource value: 0x7f0100e2 + public static int switchPadding = 2130772194; + + // aapt resource value: 0x7f0100a9 + public static int switchStyle = 2130772137; + + // aapt resource value: 0x7f0100e0 + public static int switchTextAppearance = 2130772192; + + // aapt resource value: 0x7f01002e + public static int textAllCaps = 2130772014; + + // aapt resource value: 0x7f01005e + public static int textAppearanceLargePopupMenu = 2130772062; + + // aapt resource value: 0x7f010084 + public static int textAppearanceListItem = 2130772100; + + // aapt resource value: 0x7f010085 + public static int textAppearanceListItemSecondary = 2130772101; + + // aapt resource value: 0x7f010086 + public static int textAppearanceListItemSmall = 2130772102; + + // aapt resource value: 0x7f010060 + public static int textAppearancePopupMenuHeader = 2130772064; + + // aapt resource value: 0x7f01007a + public static int textAppearanceSearchResultSubtitle = 2130772090; + + // aapt resource value: 0x7f010079 + public static int textAppearanceSearchResultTitle = 2130772089; + + // aapt resource value: 0x7f01005f + public static int textAppearanceSmallPopupMenu = 2130772063; + + // aapt resource value: 0x7f010099 + public static int textColorAlertDialogListItem = 2130772121; + + // aapt resource value: 0x7f01007b + public static int textColorSearchUrl = 2130772091; + + // aapt resource value: 0x7f0100f8 + public static int theme = 2130772216; + + // aapt resource value: 0x7f0100b9 + public static int thickness = 2130772153; + + // aapt resource value: 0x7f0100df + public static int thumbTextPadding = 2130772191; + + // aapt resource value: 0x7f0100da + public static int thumbTint = 2130772186; + + // aapt resource value: 0x7f0100db + public static int thumbTintMode = 2130772187; + + // aapt resource value: 0x7f01002b + public static int tickMark = 2130772011; + + // aapt resource value: 0x7f01002c + public static int tickMarkTint = 2130772012; + + // aapt resource value: 0x7f01002d + public static int tickMarkTintMode = 2130772013; + + // aapt resource value: 0x7f010029 + public static int tint = 2130772009; + + // aapt resource value: 0x7f01002a + public static int tintMode = 2130772010; + + // aapt resource value: 0x7f010003 + public static int title = 2130771971; + + // aapt resource value: 0x7f0100e7 + public static int titleMargin = 2130772199; + + // aapt resource value: 0x7f0100eb + public static int titleMarginBottom = 2130772203; + + // aapt resource value: 0x7f0100e9 + public static int titleMarginEnd = 2130772201; + + // aapt resource value: 0x7f0100e8 + public static int titleMarginStart = 2130772200; + + // aapt resource value: 0x7f0100ea + public static int titleMarginTop = 2130772202; + + // aapt resource value: 0x7f0100ec + public static int titleMargins = 2130772204; + + // aapt resource value: 0x7f0100e5 + public static int titleTextAppearance = 2130772197; + + // aapt resource value: 0x7f0100f4 + public static int titleTextColor = 2130772212; + + // aapt resource value: 0x7f010007 + public static int titleTextStyle = 2130771975; + + // aapt resource value: 0x7f010073 + public static int toolbarNavigationButtonStyle = 2130772083; + + // aapt resource value: 0x7f010072 + public static int toolbarStyle = 2130772082; + + // aapt resource value: 0x7f0100ac + public static int tooltipForegroundColor = 2130772140; + + // aapt resource value: 0x7f0100ab + public static int tooltipFrameBackground = 2130772139; + + // aapt resource value: 0x7f0100c4 + public static int tooltipText = 2130772164; + + // aapt resource value: 0x7f0100dc + public static int track = 2130772188; + + // aapt resource value: 0x7f0100dd + public static int trackTint = 2130772189; + + // aapt resource value: 0x7f0100de + public static int trackTintMode = 2130772190; + + // aapt resource value: 0x7f01010f + public static int ttcIndex = 2130772239; + + // aapt resource value: 0x7f0100ae + public static int viewInflaterClass = 2130772142; + + // aapt resource value: 0x7f0100d5 + public static int voiceIcon = 2130772181; + + // aapt resource value: 0x7f010038 + public static int windowActionBar = 2130772024; + + // aapt resource value: 0x7f01003a + public static int windowActionBarOverlay = 2130772026; + + // aapt resource value: 0x7f01003b + public static int windowActionModeOverlay = 2130772027; + + // aapt resource value: 0x7f01003f + public static int windowFixedHeightMajor = 2130772031; + + // aapt resource value: 0x7f01003d + public static int windowFixedHeightMinor = 2130772029; + + // aapt resource value: 0x7f01003c + public static int windowFixedWidthMajor = 2130772028; + + // aapt resource value: 0x7f01003e + public static int windowFixedWidthMinor = 2130772030; + + // aapt resource value: 0x7f010040 + public static int windowMinWidthMajor = 2130772032; + + // aapt resource value: 0x7f010041 + public static int windowMinWidthMinor = 2130772033; + + // aapt resource value: 0x7f010039 + public static int windowNoTitle = 2130772025; + + static Attribute() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Attribute() + { + } + } + + public partial class Boolean + { + + // aapt resource value: 0x7f080000 + public static int abc_action_bar_embed_tabs = 2131230720; + + // aapt resource value: 0x7f080001 + public static int abc_allow_stacked_button_bar = 2131230721; + + // aapt resource value: 0x7f080002 + public static int abc_config_actionMenuItemAllCaps = 2131230722; + + static Boolean() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Boolean() + { + } + } + + public partial class Color + { + + // aapt resource value: 0x7f09003e + public static int abc_background_cache_hint_selector_material_dark = 2131296318; + + // aapt resource value: 0x7f09003f + public static int abc_background_cache_hint_selector_material_light = 2131296319; + + // aapt resource value: 0x7f090040 + public static int abc_btn_colored_borderless_text_material = 2131296320; + + // aapt resource value: 0x7f090041 + public static int abc_btn_colored_text_material = 2131296321; + + // aapt resource value: 0x7f090042 + public static int abc_color_highlight_material = 2131296322; + + // aapt resource value: 0x7f090043 + public static int abc_hint_foreground_material_dark = 2131296323; + + // aapt resource value: 0x7f090044 + public static int abc_hint_foreground_material_light = 2131296324; + + // aapt resource value: 0x7f090000 + public static int abc_input_method_navigation_guard = 2131296256; + + // aapt resource value: 0x7f090045 + public static int abc_primary_text_disable_only_material_dark = 2131296325; + + // aapt resource value: 0x7f090046 + public static int abc_primary_text_disable_only_material_light = 2131296326; + + // aapt resource value: 0x7f090047 + public static int abc_primary_text_material_dark = 2131296327; + + // aapt resource value: 0x7f090048 + public static int abc_primary_text_material_light = 2131296328; + + // aapt resource value: 0x7f090049 + public static int abc_search_url_text = 2131296329; + + // aapt resource value: 0x7f090001 + public static int abc_search_url_text_normal = 2131296257; + + // aapt resource value: 0x7f090002 + public static int abc_search_url_text_pressed = 2131296258; + + // aapt resource value: 0x7f090003 + public static int abc_search_url_text_selected = 2131296259; + + // aapt resource value: 0x7f09004a + public static int abc_secondary_text_material_dark = 2131296330; + + // aapt resource value: 0x7f09004b + public static int abc_secondary_text_material_light = 2131296331; + + // aapt resource value: 0x7f09004c + public static int abc_tint_btn_checkable = 2131296332; + + // aapt resource value: 0x7f09004d + public static int abc_tint_default = 2131296333; + + // aapt resource value: 0x7f09004e + public static int abc_tint_edittext = 2131296334; + + // aapt resource value: 0x7f09004f + public static int abc_tint_seek_thumb = 2131296335; + + // aapt resource value: 0x7f090050 + public static int abc_tint_spinner = 2131296336; + + // aapt resource value: 0x7f090051 + public static int abc_tint_switch_track = 2131296337; + + // aapt resource value: 0x7f090004 + public static int accent_material_dark = 2131296260; + + // aapt resource value: 0x7f090005 + public static int accent_material_light = 2131296261; + + // aapt resource value: 0x7f090006 + public static int background_floating_material_dark = 2131296262; + + // aapt resource value: 0x7f090007 + public static int background_floating_material_light = 2131296263; + + // aapt resource value: 0x7f090008 + public static int background_material_dark = 2131296264; + + // aapt resource value: 0x7f090009 + public static int background_material_light = 2131296265; + + // aapt resource value: 0x7f09000a + public static int bright_foreground_disabled_material_dark = 2131296266; + + // aapt resource value: 0x7f09000b + public static int bright_foreground_disabled_material_light = 2131296267; + + // aapt resource value: 0x7f09000c + public static int bright_foreground_inverse_material_dark = 2131296268; + + // aapt resource value: 0x7f09000d + public static int bright_foreground_inverse_material_light = 2131296269; + + // aapt resource value: 0x7f09000e + public static int bright_foreground_material_dark = 2131296270; + + // aapt resource value: 0x7f09000f + public static int bright_foreground_material_light = 2131296271; + + // aapt resource value: 0x7f090010 + public static int button_material_dark = 2131296272; + + // aapt resource value: 0x7f090011 + public static int button_material_light = 2131296273; + + // aapt resource value: 0x7f090012 + public static int dim_foreground_disabled_material_dark = 2131296274; + + // aapt resource value: 0x7f090013 + public static int dim_foreground_disabled_material_light = 2131296275; + + // aapt resource value: 0x7f090014 + public static int dim_foreground_material_dark = 2131296276; + + // aapt resource value: 0x7f090015 + public static int dim_foreground_material_light = 2131296277; + + // aapt resource value: 0x7f090016 + public static int error_color_material_dark = 2131296278; + + // aapt resource value: 0x7f090017 + public static int error_color_material_light = 2131296279; + + // aapt resource value: 0x7f090018 + public static int foreground_material_dark = 2131296280; + + // aapt resource value: 0x7f090019 + public static int foreground_material_light = 2131296281; + + // aapt resource value: 0x7f09001a + public static int highlighted_text_material_dark = 2131296282; + + // aapt resource value: 0x7f09001b + public static int highlighted_text_material_light = 2131296283; + + // aapt resource value: 0x7f09001c + public static int material_blue_grey_800 = 2131296284; + + // aapt resource value: 0x7f09001d + public static int material_blue_grey_900 = 2131296285; + + // aapt resource value: 0x7f09001e + public static int material_blue_grey_950 = 2131296286; + + // aapt resource value: 0x7f09001f + public static int material_deep_teal_200 = 2131296287; + + // aapt resource value: 0x7f090020 + public static int material_deep_teal_500 = 2131296288; + + // aapt resource value: 0x7f090021 + public static int material_grey_100 = 2131296289; + + // aapt resource value: 0x7f090022 + public static int material_grey_300 = 2131296290; + + // aapt resource value: 0x7f090023 + public static int material_grey_50 = 2131296291; + + // aapt resource value: 0x7f090024 + public static int material_grey_600 = 2131296292; + + // aapt resource value: 0x7f090025 + public static int material_grey_800 = 2131296293; + + // aapt resource value: 0x7f090026 + public static int material_grey_850 = 2131296294; + + // aapt resource value: 0x7f090027 + public static int material_grey_900 = 2131296295; + + // aapt resource value: 0x7f09003c + public static int notification_action_color_filter = 2131296316; + + // aapt resource value: 0x7f09003d + public static int notification_icon_bg_color = 2131296317; + + // aapt resource value: 0x7f090028 + public static int primary_dark_material_dark = 2131296296; + + // aapt resource value: 0x7f090029 + public static int primary_dark_material_light = 2131296297; + + // aapt resource value: 0x7f09002a + public static int primary_material_dark = 2131296298; + + // aapt resource value: 0x7f09002b + public static int primary_material_light = 2131296299; + + // aapt resource value: 0x7f09002c + public static int primary_text_default_material_dark = 2131296300; + + // aapt resource value: 0x7f09002d + public static int primary_text_default_material_light = 2131296301; + + // aapt resource value: 0x7f09002e + public static int primary_text_disabled_material_dark = 2131296302; + + // aapt resource value: 0x7f09002f + public static int primary_text_disabled_material_light = 2131296303; + + // aapt resource value: 0x7f090030 + public static int ripple_material_dark = 2131296304; + + // aapt resource value: 0x7f090031 + public static int ripple_material_light = 2131296305; + + // aapt resource value: 0x7f090032 + public static int secondary_text_default_material_dark = 2131296306; + + // aapt resource value: 0x7f090033 + public static int secondary_text_default_material_light = 2131296307; + + // aapt resource value: 0x7f090034 + public static int secondary_text_disabled_material_dark = 2131296308; + + // aapt resource value: 0x7f090035 + public static int secondary_text_disabled_material_light = 2131296309; + + // aapt resource value: 0x7f090036 + public static int switch_thumb_disabled_material_dark = 2131296310; + + // aapt resource value: 0x7f090037 + public static int switch_thumb_disabled_material_light = 2131296311; + + // aapt resource value: 0x7f090052 + public static int switch_thumb_material_dark = 2131296338; + + // aapt resource value: 0x7f090053 + public static int switch_thumb_material_light = 2131296339; + + // aapt resource value: 0x7f090038 + public static int switch_thumb_normal_material_dark = 2131296312; + + // aapt resource value: 0x7f090039 + public static int switch_thumb_normal_material_light = 2131296313; + + // aapt resource value: 0x7f09003a + public static int tooltip_background_dark = 2131296314; + + // aapt resource value: 0x7f09003b + public static int tooltip_background_light = 2131296315; + + static Color() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Color() + { + } + } + + public partial class Dimension + { + + // aapt resource value: 0x7f06000b + public static int abc_action_bar_content_inset_material = 2131099659; + + // aapt resource value: 0x7f06000c + public static int abc_action_bar_content_inset_with_nav = 2131099660; + + // aapt resource value: 0x7f060001 + public static int abc_action_bar_default_height_material = 2131099649; + + // aapt resource value: 0x7f06000d + public static int abc_action_bar_default_padding_end_material = 2131099661; + + // aapt resource value: 0x7f06000e + public static int abc_action_bar_default_padding_start_material = 2131099662; + + // aapt resource value: 0x7f060010 + public static int abc_action_bar_elevation_material = 2131099664; + + // aapt resource value: 0x7f060011 + public static int abc_action_bar_icon_vertical_padding_material = 2131099665; + + // aapt resource value: 0x7f060012 + public static int abc_action_bar_overflow_padding_end_material = 2131099666; + + // aapt resource value: 0x7f060013 + public static int abc_action_bar_overflow_padding_start_material = 2131099667; + + // aapt resource value: 0x7f060014 + public static int abc_action_bar_stacked_max_height = 2131099668; + + // aapt resource value: 0x7f060015 + public static int abc_action_bar_stacked_tab_max_width = 2131099669; + + // aapt resource value: 0x7f060016 + public static int abc_action_bar_subtitle_bottom_margin_material = 2131099670; + + // aapt resource value: 0x7f060017 + public static int abc_action_bar_subtitle_top_margin_material = 2131099671; + + // aapt resource value: 0x7f060018 + public static int abc_action_button_min_height_material = 2131099672; + + // aapt resource value: 0x7f060019 + public static int abc_action_button_min_width_material = 2131099673; + + // aapt resource value: 0x7f06001a + public static int abc_action_button_min_width_overflow_material = 2131099674; + + // aapt resource value: 0x7f060000 + public static int abc_alert_dialog_button_bar_height = 2131099648; + + // aapt resource value: 0x7f06001b + public static int abc_alert_dialog_button_dimen = 2131099675; + + // aapt resource value: 0x7f06001c + public static int abc_button_inset_horizontal_material = 2131099676; + + // aapt resource value: 0x7f06001d + public static int abc_button_inset_vertical_material = 2131099677; + + // aapt resource value: 0x7f06001e + public static int abc_button_padding_horizontal_material = 2131099678; + + // aapt resource value: 0x7f06001f + public static int abc_button_padding_vertical_material = 2131099679; + + // aapt resource value: 0x7f060020 + public static int abc_cascading_menus_min_smallest_width = 2131099680; + + // aapt resource value: 0x7f060004 + public static int abc_config_prefDialogWidth = 2131099652; + + // aapt resource value: 0x7f060021 + public static int abc_control_corner_material = 2131099681; + + // aapt resource value: 0x7f060022 + public static int abc_control_inset_material = 2131099682; + + // aapt resource value: 0x7f060023 + public static int abc_control_padding_material = 2131099683; + + // aapt resource value: 0x7f060024 + public static int abc_dialog_corner_radius_material = 2131099684; + + // aapt resource value: 0x7f060005 + public static int abc_dialog_fixed_height_major = 2131099653; + + // aapt resource value: 0x7f060006 + public static int abc_dialog_fixed_height_minor = 2131099654; + + // aapt resource value: 0x7f060007 + public static int abc_dialog_fixed_width_major = 2131099655; + + // aapt resource value: 0x7f060008 + public static int abc_dialog_fixed_width_minor = 2131099656; + + // aapt resource value: 0x7f060025 + public static int abc_dialog_list_padding_bottom_no_buttons = 2131099685; + + // aapt resource value: 0x7f060026 + public static int abc_dialog_list_padding_top_no_title = 2131099686; + + // aapt resource value: 0x7f060009 + public static int abc_dialog_min_width_major = 2131099657; + + // aapt resource value: 0x7f06000a + public static int abc_dialog_min_width_minor = 2131099658; + + // aapt resource value: 0x7f060027 + public static int abc_dialog_padding_material = 2131099687; + + // aapt resource value: 0x7f060028 + public static int abc_dialog_padding_top_material = 2131099688; + + // aapt resource value: 0x7f060029 + public static int abc_dialog_title_divider_material = 2131099689; + + // aapt resource value: 0x7f06002a + public static int abc_disabled_alpha_material_dark = 2131099690; + + // aapt resource value: 0x7f06002b + public static int abc_disabled_alpha_material_light = 2131099691; + + // aapt resource value: 0x7f06002c + public static int abc_dropdownitem_icon_width = 2131099692; + + // aapt resource value: 0x7f06002d + public static int abc_dropdownitem_text_padding_left = 2131099693; + + // aapt resource value: 0x7f06002e + public static int abc_dropdownitem_text_padding_right = 2131099694; + + // aapt resource value: 0x7f06002f + public static int abc_edit_text_inset_bottom_material = 2131099695; + + // aapt resource value: 0x7f060030 + public static int abc_edit_text_inset_horizontal_material = 2131099696; + + // aapt resource value: 0x7f060031 + public static int abc_edit_text_inset_top_material = 2131099697; + + // aapt resource value: 0x7f060032 + public static int abc_floating_window_z = 2131099698; + + // aapt resource value: 0x7f060033 + public static int abc_list_item_padding_horizontal_material = 2131099699; + + // aapt resource value: 0x7f060034 + public static int abc_panel_menu_list_width = 2131099700; + + // aapt resource value: 0x7f060035 + public static int abc_progress_bar_height_material = 2131099701; + + // aapt resource value: 0x7f060036 + public static int abc_search_view_preferred_height = 2131099702; + + // aapt resource value: 0x7f060037 + public static int abc_search_view_preferred_width = 2131099703; + + // aapt resource value: 0x7f060038 + public static int abc_seekbar_track_background_height_material = 2131099704; + + // aapt resource value: 0x7f060039 + public static int abc_seekbar_track_progress_height_material = 2131099705; + + // aapt resource value: 0x7f06003a + public static int abc_select_dialog_padding_start_material = 2131099706; + + // aapt resource value: 0x7f06000f + public static int abc_switch_padding = 2131099663; + + // aapt resource value: 0x7f06003b + public static int abc_text_size_body_1_material = 2131099707; + + // aapt resource value: 0x7f06003c + public static int abc_text_size_body_2_material = 2131099708; + + // aapt resource value: 0x7f06003d + public static int abc_text_size_button_material = 2131099709; + + // aapt resource value: 0x7f06003e + public static int abc_text_size_caption_material = 2131099710; + + // aapt resource value: 0x7f06003f + public static int abc_text_size_display_1_material = 2131099711; + + // aapt resource value: 0x7f060040 + public static int abc_text_size_display_2_material = 2131099712; + + // aapt resource value: 0x7f060041 + public static int abc_text_size_display_3_material = 2131099713; + + // aapt resource value: 0x7f060042 + public static int abc_text_size_display_4_material = 2131099714; + + // aapt resource value: 0x7f060043 + public static int abc_text_size_headline_material = 2131099715; + + // aapt resource value: 0x7f060044 + public static int abc_text_size_large_material = 2131099716; + + // aapt resource value: 0x7f060045 + public static int abc_text_size_medium_material = 2131099717; + + // aapt resource value: 0x7f060046 + public static int abc_text_size_menu_header_material = 2131099718; + + // aapt resource value: 0x7f060047 + public static int abc_text_size_menu_material = 2131099719; + + // aapt resource value: 0x7f060048 + public static int abc_text_size_small_material = 2131099720; + + // aapt resource value: 0x7f060049 + public static int abc_text_size_subhead_material = 2131099721; + + // aapt resource value: 0x7f060002 + public static int abc_text_size_subtitle_material_toolbar = 2131099650; + + // aapt resource value: 0x7f06004a + public static int abc_text_size_title_material = 2131099722; + + // aapt resource value: 0x7f060003 + public static int abc_text_size_title_material_toolbar = 2131099651; + + // aapt resource value: 0x7f060060 + public static int compat_button_inset_horizontal_material = 2131099744; + + // aapt resource value: 0x7f060061 + public static int compat_button_inset_vertical_material = 2131099745; + + // aapt resource value: 0x7f060062 + public static int compat_button_padding_horizontal_material = 2131099746; + + // aapt resource value: 0x7f060063 + public static int compat_button_padding_vertical_material = 2131099747; + + // aapt resource value: 0x7f060064 + public static int compat_control_corner_material = 2131099748; + + // aapt resource value: 0x7f060065 + public static int compat_notification_large_icon_max_height = 2131099749; + + // aapt resource value: 0x7f060066 + public static int compat_notification_large_icon_max_width = 2131099750; + + // aapt resource value: 0x7f06004b + public static int disabled_alpha_material_dark = 2131099723; + + // aapt resource value: 0x7f06004c + public static int disabled_alpha_material_light = 2131099724; + + // aapt resource value: 0x7f06004d + public static int highlight_alpha_material_colored = 2131099725; + + // aapt resource value: 0x7f06004e + public static int highlight_alpha_material_dark = 2131099726; + + // aapt resource value: 0x7f06004f + public static int highlight_alpha_material_light = 2131099727; + + // aapt resource value: 0x7f060050 + public static int hint_alpha_material_dark = 2131099728; + + // aapt resource value: 0x7f060051 + public static int hint_alpha_material_light = 2131099729; + + // aapt resource value: 0x7f060052 + public static int hint_pressed_alpha_material_dark = 2131099730; + + // aapt resource value: 0x7f060053 + public static int hint_pressed_alpha_material_light = 2131099731; + + // aapt resource value: 0x7f060067 + public static int notification_action_icon_size = 2131099751; + + // aapt resource value: 0x7f060068 + public static int notification_action_text_size = 2131099752; + + // aapt resource value: 0x7f060069 + public static int notification_big_circle_margin = 2131099753; + + // aapt resource value: 0x7f06005d + public static int notification_content_margin_start = 2131099741; + + // aapt resource value: 0x7f06006a + public static int notification_large_icon_height = 2131099754; + + // aapt resource value: 0x7f06006b + public static int notification_large_icon_width = 2131099755; + + // aapt resource value: 0x7f06005e + public static int notification_main_column_padding_top = 2131099742; + + // aapt resource value: 0x7f06005f + public static int notification_media_narrow_margin = 2131099743; + + // aapt resource value: 0x7f06006c + public static int notification_right_icon_size = 2131099756; + + // aapt resource value: 0x7f06005c + public static int notification_right_side_padding_top = 2131099740; + + // aapt resource value: 0x7f06006d + public static int notification_small_icon_background_padding = 2131099757; + + // aapt resource value: 0x7f06006e + public static int notification_small_icon_size_as_large = 2131099758; + + // aapt resource value: 0x7f06006f + public static int notification_subtext_size = 2131099759; + + // aapt resource value: 0x7f060070 + public static int notification_top_pad = 2131099760; + + // aapt resource value: 0x7f060071 + public static int notification_top_pad_large_text = 2131099761; + + // aapt resource value: 0x7f060054 + public static int tooltip_corner_radius = 2131099732; + + // aapt resource value: 0x7f060055 + public static int tooltip_horizontal_padding = 2131099733; + + // aapt resource value: 0x7f060056 + public static int tooltip_margin = 2131099734; + + // aapt resource value: 0x7f060057 + public static int tooltip_precise_anchor_extra_offset = 2131099735; + + // aapt resource value: 0x7f060058 + public static int tooltip_precise_anchor_threshold = 2131099736; + + // aapt resource value: 0x7f060059 + public static int tooltip_vertical_padding = 2131099737; + + // aapt resource value: 0x7f06005a + public static int tooltip_y_offset_non_touch = 2131099738; + + // aapt resource value: 0x7f06005b + public static int tooltip_y_offset_touch = 2131099739; + + static Dimension() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Dimension() + { + } + } + + public partial class Drawable + { + + // aapt resource value: 0x7f020000 + public static int abc_ab_share_pack_mtrl_alpha = 2130837504; + + // aapt resource value: 0x7f020001 + public static int abc_action_bar_item_background_material = 2130837505; + + // aapt resource value: 0x7f020002 + public static int abc_btn_borderless_material = 2130837506; + + // aapt resource value: 0x7f020003 + public static int abc_btn_check_material = 2130837507; + + // aapt resource value: 0x7f020004 + public static int abc_btn_check_to_on_mtrl_000 = 2130837508; + + // aapt resource value: 0x7f020005 + public static int abc_btn_check_to_on_mtrl_015 = 2130837509; + + // aapt resource value: 0x7f020006 + public static int abc_btn_colored_material = 2130837510; + + // aapt resource value: 0x7f020007 + public static int abc_btn_default_mtrl_shape = 2130837511; + + // aapt resource value: 0x7f020008 + public static int abc_btn_radio_material = 2130837512; + + // aapt resource value: 0x7f020009 + public static int abc_btn_radio_to_on_mtrl_000 = 2130837513; + + // aapt resource value: 0x7f02000a + public static int abc_btn_radio_to_on_mtrl_015 = 2130837514; + + // aapt resource value: 0x7f02000b + public static int abc_btn_switch_to_on_mtrl_00001 = 2130837515; + + // aapt resource value: 0x7f02000c + public static int abc_btn_switch_to_on_mtrl_00012 = 2130837516; + + // aapt resource value: 0x7f02000d + public static int abc_cab_background_internal_bg = 2130837517; + + // aapt resource value: 0x7f02000e + public static int abc_cab_background_top_material = 2130837518; + + // aapt resource value: 0x7f02000f + public static int abc_cab_background_top_mtrl_alpha = 2130837519; + + // aapt resource value: 0x7f020010 + public static int abc_control_background_material = 2130837520; + + // aapt resource value: 0x7f020011 + public static int abc_dialog_material_background = 2130837521; + + // aapt resource value: 0x7f020012 + public static int abc_edit_text_material = 2130837522; + + // aapt resource value: 0x7f020013 + public static int abc_ic_ab_back_material = 2130837523; + + // aapt resource value: 0x7f020014 + public static int abc_ic_arrow_drop_right_black_24dp = 2130837524; + + // aapt resource value: 0x7f020015 + public static int abc_ic_clear_material = 2130837525; + + // aapt resource value: 0x7f020016 + public static int abc_ic_commit_search_api_mtrl_alpha = 2130837526; + + // aapt resource value: 0x7f020017 + public static int abc_ic_go_search_api_material = 2130837527; + + // aapt resource value: 0x7f020018 + public static int abc_ic_menu_copy_mtrl_am_alpha = 2130837528; + + // aapt resource value: 0x7f020019 + public static int abc_ic_menu_cut_mtrl_alpha = 2130837529; + + // aapt resource value: 0x7f02001a + public static int abc_ic_menu_overflow_material = 2130837530; + + // aapt resource value: 0x7f02001b + public static int abc_ic_menu_paste_mtrl_am_alpha = 2130837531; + + // aapt resource value: 0x7f02001c + public static int abc_ic_menu_selectall_mtrl_alpha = 2130837532; + + // aapt resource value: 0x7f02001d + public static int abc_ic_menu_share_mtrl_alpha = 2130837533; + + // aapt resource value: 0x7f02001e + public static int abc_ic_search_api_material = 2130837534; + + // aapt resource value: 0x7f02001f + public static int abc_ic_star_black_16dp = 2130837535; + + // aapt resource value: 0x7f020020 + public static int abc_ic_star_black_36dp = 2130837536; + + // aapt resource value: 0x7f020021 + public static int abc_ic_star_black_48dp = 2130837537; + + // aapt resource value: 0x7f020022 + public static int abc_ic_star_half_black_16dp = 2130837538; + + // aapt resource value: 0x7f020023 + public static int abc_ic_star_half_black_36dp = 2130837539; + + // aapt resource value: 0x7f020024 + public static int abc_ic_star_half_black_48dp = 2130837540; + + // aapt resource value: 0x7f020025 + public static int abc_ic_voice_search_api_material = 2130837541; + + // aapt resource value: 0x7f020026 + public static int abc_item_background_holo_dark = 2130837542; + + // aapt resource value: 0x7f020027 + public static int abc_item_background_holo_light = 2130837543; + + // aapt resource value: 0x7f020028 + public static int abc_list_divider_material = 2130837544; + + // aapt resource value: 0x7f020029 + public static int abc_list_divider_mtrl_alpha = 2130837545; + + // aapt resource value: 0x7f02002a + public static int abc_list_focused_holo = 2130837546; + + // aapt resource value: 0x7f02002b + public static int abc_list_longpressed_holo = 2130837547; + + // aapt resource value: 0x7f02002c + public static int abc_list_pressed_holo_dark = 2130837548; + + // aapt resource value: 0x7f02002d + public static int abc_list_pressed_holo_light = 2130837549; + + // aapt resource value: 0x7f02002e + public static int abc_list_selector_background_transition_holo_dark = 2130837550; + + // aapt resource value: 0x7f02002f + public static int abc_list_selector_background_transition_holo_light = 2130837551; + + // aapt resource value: 0x7f020030 + public static int abc_list_selector_disabled_holo_dark = 2130837552; + + // aapt resource value: 0x7f020031 + public static int abc_list_selector_disabled_holo_light = 2130837553; + + // aapt resource value: 0x7f020032 + public static int abc_list_selector_holo_dark = 2130837554; + + // aapt resource value: 0x7f020033 + public static int abc_list_selector_holo_light = 2130837555; + + // aapt resource value: 0x7f020034 + public static int abc_menu_hardkey_panel_mtrl_mult = 2130837556; + + // aapt resource value: 0x7f020035 + public static int abc_popup_background_mtrl_mult = 2130837557; + + // aapt resource value: 0x7f020036 + public static int abc_ratingbar_indicator_material = 2130837558; + + // aapt resource value: 0x7f020037 + public static int abc_ratingbar_material = 2130837559; + + // aapt resource value: 0x7f020038 + public static int abc_ratingbar_small_material = 2130837560; + + // aapt resource value: 0x7f020039 + public static int abc_scrubber_control_off_mtrl_alpha = 2130837561; + + // aapt resource value: 0x7f02003a + public static int abc_scrubber_control_to_pressed_mtrl_000 = 2130837562; + + // aapt resource value: 0x7f02003b + public static int abc_scrubber_control_to_pressed_mtrl_005 = 2130837563; + + // aapt resource value: 0x7f02003c + public static int abc_scrubber_primary_mtrl_alpha = 2130837564; + + // aapt resource value: 0x7f02003d + public static int abc_scrubber_track_mtrl_alpha = 2130837565; + + // aapt resource value: 0x7f02003e + public static int abc_seekbar_thumb_material = 2130837566; + + // aapt resource value: 0x7f02003f + public static int abc_seekbar_tick_mark_material = 2130837567; + + // aapt resource value: 0x7f020040 + public static int abc_seekbar_track_material = 2130837568; + + // aapt resource value: 0x7f020041 + public static int abc_spinner_mtrl_am_alpha = 2130837569; + + // aapt resource value: 0x7f020042 + public static int abc_spinner_textfield_background_material = 2130837570; + + // aapt resource value: 0x7f020043 + public static int abc_switch_thumb_material = 2130837571; + + // aapt resource value: 0x7f020044 + public static int abc_switch_track_mtrl_alpha = 2130837572; + + // aapt resource value: 0x7f020045 + public static int abc_tab_indicator_material = 2130837573; + + // aapt resource value: 0x7f020046 + public static int abc_tab_indicator_mtrl_alpha = 2130837574; + + // aapt resource value: 0x7f020047 + public static int abc_text_cursor_material = 2130837575; + + // aapt resource value: 0x7f020048 + public static int abc_text_select_handle_left_mtrl_dark = 2130837576; + + // aapt resource value: 0x7f020049 + public static int abc_text_select_handle_left_mtrl_light = 2130837577; + + // aapt resource value: 0x7f02004a + public static int abc_text_select_handle_middle_mtrl_dark = 2130837578; + + // aapt resource value: 0x7f02004b + public static int abc_text_select_handle_middle_mtrl_light = 2130837579; + + // aapt resource value: 0x7f02004c + public static int abc_text_select_handle_right_mtrl_dark = 2130837580; + + // aapt resource value: 0x7f02004d + public static int abc_text_select_handle_right_mtrl_light = 2130837581; + + // aapt resource value: 0x7f02004e + public static int abc_textfield_activated_mtrl_alpha = 2130837582; + + // aapt resource value: 0x7f02004f + public static int abc_textfield_default_mtrl_alpha = 2130837583; + + // aapt resource value: 0x7f020050 + public static int abc_textfield_search_activated_mtrl_alpha = 2130837584; + + // aapt resource value: 0x7f020051 + public static int abc_textfield_search_default_mtrl_alpha = 2130837585; + + // aapt resource value: 0x7f020052 + public static int abc_textfield_search_material = 2130837586; + + // aapt resource value: 0x7f020053 + public static int abc_vector_test = 2130837587; + + // aapt resource value: 0x7f020054 + public static int notification_action_background = 2130837588; + + // aapt resource value: 0x7f020055 + public static int notification_bg = 2130837589; + + // aapt resource value: 0x7f020056 + public static int notification_bg_low = 2130837590; + + // aapt resource value: 0x7f020057 + public static int notification_bg_low_normal = 2130837591; + + // aapt resource value: 0x7f020058 + public static int notification_bg_low_pressed = 2130837592; + + // aapt resource value: 0x7f020059 + public static int notification_bg_normal = 2130837593; + + // aapt resource value: 0x7f02005a + public static int notification_bg_normal_pressed = 2130837594; + + // aapt resource value: 0x7f02005b + public static int notification_icon_background = 2130837595; + + // aapt resource value: 0x7f020060 + public static int notification_template_icon_bg = 2130837600; + + // aapt resource value: 0x7f020061 + public static int notification_template_icon_low_bg = 2130837601; + + // aapt resource value: 0x7f02005c + public static int notification_tile_bg = 2130837596; + + // aapt resource value: 0x7f02005d + public static int notify_panel_notification_icon_bg = 2130837597; + + // aapt resource value: 0x7f02005e + public static int tooltip_frame_dark = 2130837598; + + // aapt resource value: 0x7f02005f + public static int tooltip_frame_light = 2130837599; + + static Drawable() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Drawable() + { + } + } + + public partial class Id + { + + // aapt resource value: 0x7f0a0026 + public static int ALT = 2131361830; + + // aapt resource value: 0x7f0a0027 + public static int CTRL = 2131361831; + + // aapt resource value: 0x7f0a0028 + public static int FUNCTION = 2131361832; + + // aapt resource value: 0x7f0a0029 + public static int META = 2131361833; + + // aapt resource value: 0x7f0a002a + public static int SHIFT = 2131361834; + + // aapt resource value: 0x7f0a002b + public static int SYM = 2131361835; + + // aapt resource value: 0x7f0a0067 + public static int action_bar = 2131361895; + + // aapt resource value: 0x7f0a0000 + public static int action_bar_activity_content = 2131361792; + + // aapt resource value: 0x7f0a0066 + public static int action_bar_container = 2131361894; + + // aapt resource value: 0x7f0a0062 + public static int action_bar_root = 2131361890; + + // aapt resource value: 0x7f0a0001 + public static int action_bar_spinner = 2131361793; + + // aapt resource value: 0x7f0a0044 + public static int action_bar_subtitle = 2131361860; + + // aapt resource value: 0x7f0a0043 + public static int action_bar_title = 2131361859; + + // aapt resource value: 0x7f0a0077 + public static int action_container = 2131361911; + + // aapt resource value: 0x7f0a0068 + public static int action_context_bar = 2131361896; + + // aapt resource value: 0x7f0a0082 + public static int action_divider = 2131361922; + + // aapt resource value: 0x7f0a0078 + public static int action_image = 2131361912; + + // aapt resource value: 0x7f0a0002 + public static int action_menu_divider = 2131361794; + + // aapt resource value: 0x7f0a0003 + public static int action_menu_presenter = 2131361795; + + // aapt resource value: 0x7f0a0064 + public static int action_mode_bar = 2131361892; + + // aapt resource value: 0x7f0a0063 + public static int action_mode_bar_stub = 2131361891; + + // aapt resource value: 0x7f0a0045 + public static int action_mode_close_button = 2131361861; + + // aapt resource value: 0x7f0a0079 + public static int action_text = 2131361913; + + // aapt resource value: 0x7f0a0083 + public static int actions = 2131361923; + + // aapt resource value: 0x7f0a0046 + public static int activity_chooser_view_content = 2131361862; + + // aapt resource value: 0x7f0a001b + public static int add = 2131361819; + + // aapt resource value: 0x7f0a0059 + public static int alertTitle = 2131361881; + + // aapt resource value: 0x7f0a003e + public static int all = 2131361854; + + // aapt resource value: 0x7f0a002c + public static int always = 2131361836; + + // aapt resource value: 0x7f0a0086 + public static int appIcon = 2131361926; + + // aapt resource value: 0x7f0a003f + public static int async = 2131361855; + + // aapt resource value: 0x7f0a0023 + public static int beginning = 2131361827; + + // aapt resource value: 0x7f0a0040 + public static int blocking = 2131361856; + + // aapt resource value: 0x7f0a0031 + public static int bottom = 2131361841; + + // aapt resource value: 0x7f0a004c + public static int buttonPanel = 2131361868; + + // aapt resource value: 0x7f0a0033 + public static int center = 2131361843; + + // aapt resource value: 0x7f0a0034 + public static int center_horizontal = 2131361844; + + // aapt resource value: 0x7f0a0035 + public static int center_vertical = 2131361845; + + // aapt resource value: 0x7f0a0060 + public static int checkbox = 2131361888; + + // aapt resource value: 0x7f0a0081 + public static int chronometer = 2131361921; + + // aapt resource value: 0x7f0a0036 + public static int clip_horizontal = 2131361846; + + // aapt resource value: 0x7f0a0037 + public static int clip_vertical = 2131361847; + + // aapt resource value: 0x7f0a002d + public static int collapseActionView = 2131361837; + + // aapt resource value: 0x7f0a005c + public static int content = 2131361884; + + // aapt resource value: 0x7f0a004f + public static int contentPanel = 2131361871; + + // aapt resource value: 0x7f0a0056 + public static int custom = 2131361878; + + // aapt resource value: 0x7f0a0055 + public static int customPanel = 2131361877; + + // aapt resource value: 0x7f0a0065 + public static int decor_content_parent = 2131361893; + + // aapt resource value: 0x7f0a0049 + public static int default_activity_button = 2131361865; + + // aapt resource value: 0x7f0a008b + public static int description = 2131361931; + + // aapt resource value: 0x7f0a0014 + public static int disableHome = 2131361812; + + // aapt resource value: 0x7f0a0069 + public static int edit_query = 2131361897; + + // aapt resource value: 0x7f0a0024 + public static int end = 2131361828; + + // aapt resource value: 0x7f0a0047 + public static int expand_activities_button = 2131361863; + + // aapt resource value: 0x7f0a005f + public static int expanded_menu = 2131361887; + + // aapt resource value: 0x7f0a0038 + public static int fill = 2131361848; + + // aapt resource value: 0x7f0a0039 + public static int fill_horizontal = 2131361849; + + // aapt resource value: 0x7f0a003a + public static int fill_vertical = 2131361850; + + // aapt resource value: 0x7f0a0041 + public static int forever = 2131361857; + + // aapt resource value: 0x7f0a005b + public static int group_divider = 2131361883; + + // aapt resource value: 0x7f0a0004 + public static int home = 2131361796; + + // aapt resource value: 0x7f0a0015 + public static int homeAsUp = 2131361813; + + // aapt resource value: 0x7f0a004b + public static int icon = 2131361867; + + // aapt resource value: 0x7f0a0084 + public static int icon_group = 2131361924; + + // aapt resource value: 0x7f0a002e + public static int ifRoom = 2131361838; + + // aapt resource value: 0x7f0a0048 + public static int image = 2131361864; + + // aapt resource value: 0x7f0a007d + public static int info = 2131361917; + + // aapt resource value: 0x7f0a0042 + public static int italic = 2131361858; + + // aapt resource value: 0x7f0a003b + public static int left = 2131361851; + + // aapt resource value: 0x7f0a0009 + public static int line1 = 2131361801; + + // aapt resource value: 0x7f0a000a + public static int line3 = 2131361802; + + // aapt resource value: 0x7f0a0011 + public static int listMode = 2131361809; + + // aapt resource value: 0x7f0a004a + public static int list_item = 2131361866; + + // aapt resource value: 0x7f0a0076 + public static int message = 2131361910; + + // aapt resource value: 0x7f0a0025 + public static int middle = 2131361829; + + // aapt resource value: 0x7f0a001c + public static int multiply = 2131361820; + + // aapt resource value: 0x7f0a002f + public static int never = 2131361839; + + // aapt resource value: 0x7f0a0016 + public static int none = 2131361814; + + // aapt resource value: 0x7f0a0012 + public static int normal = 2131361810; + + // aapt resource value: 0x7f0a0085 + public static int notificationLayout = 2131361925; + + // aapt resource value: 0x7f0a007f + public static int notification_background = 2131361919; + + // aapt resource value: 0x7f0a007b + public static int notification_main_column = 2131361915; + + // aapt resource value: 0x7f0a007a + public static int notification_main_column_container = 2131361914; + + // aapt resource value: 0x7f0a004e + public static int parentPanel = 2131361870; + + // aapt resource value: 0x7f0a008a + public static int progress_bar = 2131361930; + + // aapt resource value: 0x7f0a0089 + public static int progress_bar_frame = 2131361929; + + // aapt resource value: 0x7f0a0005 + public static int progress_circular = 2131361797; + + // aapt resource value: 0x7f0a0006 + public static int progress_horizontal = 2131361798; + + // aapt resource value: 0x7f0a0087 + public static int progress_text = 2131361927; + + // aapt resource value: 0x7f0a0061 + public static int radio = 2131361889; + + // aapt resource value: 0x7f0a003c + public static int right = 2131361852; + + // aapt resource value: 0x7f0a007e + public static int right_icon = 2131361918; + + // aapt resource value: 0x7f0a007c + public static int right_side = 2131361916; + + // aapt resource value: 0x7f0a001d + public static int screen = 2131361821; + + // aapt resource value: 0x7f0a0054 + public static int scrollIndicatorDown = 2131361876; + + // aapt resource value: 0x7f0a0050 + public static int scrollIndicatorUp = 2131361872; + + // aapt resource value: 0x7f0a0051 + public static int scrollView = 2131361873; + + // aapt resource value: 0x7f0a006b + public static int search_badge = 2131361899; + + // aapt resource value: 0x7f0a006a + public static int search_bar = 2131361898; + + // aapt resource value: 0x7f0a006c + public static int search_button = 2131361900; + + // aapt resource value: 0x7f0a0071 + public static int search_close_btn = 2131361905; + + // aapt resource value: 0x7f0a006d + public static int search_edit_frame = 2131361901; + + // aapt resource value: 0x7f0a0073 + public static int search_go_btn = 2131361907; + + // aapt resource value: 0x7f0a006e + public static int search_mag_icon = 2131361902; + + // aapt resource value: 0x7f0a006f + public static int search_plate = 2131361903; + + // aapt resource value: 0x7f0a0070 + public static int search_src_text = 2131361904; + + // aapt resource value: 0x7f0a0074 + public static int search_voice_btn = 2131361908; + + // aapt resource value: 0x7f0a0075 + public static int select_dialog_listview = 2131361909; + + // aapt resource value: 0x7f0a005d + public static int shortcut = 2131361885; + + // aapt resource value: 0x7f0a0017 + public static int showCustom = 2131361815; + + // aapt resource value: 0x7f0a0018 + public static int showHome = 2131361816; + + // aapt resource value: 0x7f0a0019 + public static int showTitle = 2131361817; + + // aapt resource value: 0x7f0a004d + public static int spacer = 2131361869; + + // aapt resource value: 0x7f0a0007 + public static int split_action_bar = 2131361799; + + // aapt resource value: 0x7f0a001e + public static int src_atop = 2131361822; + + // aapt resource value: 0x7f0a001f + public static int src_in = 2131361823; + + // aapt resource value: 0x7f0a0020 + public static int src_over = 2131361824; + + // aapt resource value: 0x7f0a003d + public static int start = 2131361853; + + // aapt resource value: 0x7f0a005e + public static int submenuarrow = 2131361886; + + // aapt resource value: 0x7f0a0072 + public static int submit_area = 2131361906; + + // aapt resource value: 0x7f0a0013 + public static int tabMode = 2131361811; + + // aapt resource value: 0x7f0a000b + public static int tag_transition_group = 2131361803; + + // aapt resource value: 0x7f0a000c + public static int tag_unhandled_key_event_manager = 2131361804; + + // aapt resource value: 0x7f0a000d + public static int tag_unhandled_key_listeners = 2131361805; + + // aapt resource value: 0x7f0a000e + public static int text = 2131361806; + + // aapt resource value: 0x7f0a000f + public static int text2 = 2131361807; + + // aapt resource value: 0x7f0a0053 + public static int textSpacerNoButtons = 2131361875; + + // aapt resource value: 0x7f0a0052 + public static int textSpacerNoTitle = 2131361874; + + // aapt resource value: 0x7f0a0080 + public static int time = 2131361920; + + // aapt resource value: 0x7f0a0088 + public static int time_remaining = 2131361928; + + // aapt resource value: 0x7f0a0010 + public static int title = 2131361808; + + // aapt resource value: 0x7f0a005a + public static int titleDividerNoCustom = 2131361882; + + // aapt resource value: 0x7f0a0058 + public static int title_template = 2131361880; + + // aapt resource value: 0x7f0a0032 + public static int top = 2131361842; + + // aapt resource value: 0x7f0a0057 + public static int topPanel = 2131361879; + + // aapt resource value: 0x7f0a0021 + public static int uniform = 2131361825; + + // aapt resource value: 0x7f0a0008 + public static int up = 2131361800; + + // aapt resource value: 0x7f0a001a + public static int useLogo = 2131361818; + + // aapt resource value: 0x7f0a0030 + public static int withText = 2131361840; + + // aapt resource value: 0x7f0a0022 + public static int wrap_content = 2131361826; + + static Id() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Id() + { + } + } + + public partial class Integer + { + + // aapt resource value: 0x7f0b0000 + public static int abc_config_activityDefaultDur = 2131427328; + + // aapt resource value: 0x7f0b0001 + public static int abc_config_activityShortDur = 2131427329; + + // aapt resource value: 0x7f0b0002 + public static int cancel_button_image_alpha = 2131427330; + + // aapt resource value: 0x7f0b0003 + public static int config_tooltipAnimTime = 2131427331; + + // aapt resource value: 0x7f0b0004 + public static int status_bar_notification_info_maxnum = 2131427332; + + static Integer() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Integer() + { + } + } + + public partial class Layout + { + + // aapt resource value: 0x7f030000 + public static int abc_action_bar_title_item = 2130903040; + + // aapt resource value: 0x7f030001 + public static int abc_action_bar_up_container = 2130903041; + + // aapt resource value: 0x7f030002 + public static int abc_action_menu_item_layout = 2130903042; + + // aapt resource value: 0x7f030003 + public static int abc_action_menu_layout = 2130903043; + + // aapt resource value: 0x7f030004 + public static int abc_action_mode_bar = 2130903044; + + // aapt resource value: 0x7f030005 + public static int abc_action_mode_close_item_material = 2130903045; + + // aapt resource value: 0x7f030006 + public static int abc_activity_chooser_view = 2130903046; + + // aapt resource value: 0x7f030007 + public static int abc_activity_chooser_view_list_item = 2130903047; + + // aapt resource value: 0x7f030008 + public static int abc_alert_dialog_button_bar_material = 2130903048; + + // aapt resource value: 0x7f030009 + public static int abc_alert_dialog_material = 2130903049; + + // aapt resource value: 0x7f03000a + public static int abc_alert_dialog_title_material = 2130903050; + + // aapt resource value: 0x7f03000b + public static int abc_cascading_menu_item_layout = 2130903051; + + // aapt resource value: 0x7f03000c + public static int abc_dialog_title_material = 2130903052; + + // aapt resource value: 0x7f03000d + public static int abc_expanded_menu_layout = 2130903053; + + // aapt resource value: 0x7f03000e + public static int abc_list_menu_item_checkbox = 2130903054; + + // aapt resource value: 0x7f03000f + public static int abc_list_menu_item_icon = 2130903055; + + // aapt resource value: 0x7f030010 + public static int abc_list_menu_item_layout = 2130903056; + + // aapt resource value: 0x7f030011 + public static int abc_list_menu_item_radio = 2130903057; + + // aapt resource value: 0x7f030012 + public static int abc_popup_menu_header_item_layout = 2130903058; + + // aapt resource value: 0x7f030013 + public static int abc_popup_menu_item_layout = 2130903059; + + // aapt resource value: 0x7f030014 + public static int abc_screen_content_include = 2130903060; + + // aapt resource value: 0x7f030015 + public static int abc_screen_simple = 2130903061; + + // aapt resource value: 0x7f030016 + public static int abc_screen_simple_overlay_action_mode = 2130903062; + + // aapt resource value: 0x7f030017 + public static int abc_screen_toolbar = 2130903063; + + // aapt resource value: 0x7f030018 + public static int abc_search_dropdown_item_icons_2line = 2130903064; + + // aapt resource value: 0x7f030019 + public static int abc_search_view = 2130903065; + + // aapt resource value: 0x7f03001a + public static int abc_select_dialog_material = 2130903066; + + // aapt resource value: 0x7f03001b + public static int abc_tooltip = 2130903067; + + // aapt resource value: 0x7f03001c + public static int notification_action = 2130903068; + + // aapt resource value: 0x7f03001d + public static int notification_action_tombstone = 2130903069; + + // aapt resource value: 0x7f03001e + public static int notification_template_custom_big = 2130903070; + + // aapt resource value: 0x7f03001f + public static int notification_template_icon_group = 2130903071; + + // aapt resource value: 0x7f030020 + public static int notification_template_part_chronometer = 2130903072; + + // aapt resource value: 0x7f030021 + public static int notification_template_part_time = 2130903073; + + // aapt resource value: 0x7f030022 + public static int select_dialog_item_material = 2130903074; + + // aapt resource value: 0x7f030023 + public static int select_dialog_multichoice_material = 2130903075; + + // aapt resource value: 0x7f030024 + public static int select_dialog_singlechoice_material = 2130903076; + + // aapt resource value: 0x7f030025 + public static int status_bar_ongoing_event_progress_bar = 2130903077; + + // aapt resource value: 0x7f030026 + public static int support_simple_spinner_dropdown_item = 2130903078; + + static Layout() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Layout() + { + } + } + + public partial class String + { + + // aapt resource value: 0x7f050000 + public static int abc_action_bar_home_description = 2131034112; + + // aapt resource value: 0x7f050001 + public static int abc_action_bar_up_description = 2131034113; + + // aapt resource value: 0x7f050002 + public static int abc_action_menu_overflow_description = 2131034114; + + // aapt resource value: 0x7f050003 + public static int abc_action_mode_done = 2131034115; + + // aapt resource value: 0x7f050004 + public static int abc_activity_chooser_view_see_all = 2131034116; + + // aapt resource value: 0x7f050005 + public static int abc_activitychooserview_choose_application = 2131034117; + + // aapt resource value: 0x7f050006 + public static int abc_capital_off = 2131034118; + + // aapt resource value: 0x7f050007 + public static int abc_capital_on = 2131034119; + + // aapt resource value: 0x7f05001c + public static int abc_font_family_body_1_material = 2131034140; + + // aapt resource value: 0x7f05001d + public static int abc_font_family_body_2_material = 2131034141; + + // aapt resource value: 0x7f05001e + public static int abc_font_family_button_material = 2131034142; + + // aapt resource value: 0x7f05001f + public static int abc_font_family_caption_material = 2131034143; + + // aapt resource value: 0x7f050020 + public static int abc_font_family_display_1_material = 2131034144; + + // aapt resource value: 0x7f050021 + public static int abc_font_family_display_2_material = 2131034145; + + // aapt resource value: 0x7f050022 + public static int abc_font_family_display_3_material = 2131034146; + + // aapt resource value: 0x7f050023 + public static int abc_font_family_display_4_material = 2131034147; + + // aapt resource value: 0x7f050024 + public static int abc_font_family_headline_material = 2131034148; + + // aapt resource value: 0x7f050025 + public static int abc_font_family_menu_material = 2131034149; + + // aapt resource value: 0x7f050026 + public static int abc_font_family_subhead_material = 2131034150; + + // aapt resource value: 0x7f050027 + public static int abc_font_family_title_material = 2131034151; + + // aapt resource value: 0x7f050008 + public static int abc_menu_alt_shortcut_label = 2131034120; + + // aapt resource value: 0x7f050009 + public static int abc_menu_ctrl_shortcut_label = 2131034121; + + // aapt resource value: 0x7f05000a + public static int abc_menu_delete_shortcut_label = 2131034122; + + // aapt resource value: 0x7f05000b + public static int abc_menu_enter_shortcut_label = 2131034123; + + // aapt resource value: 0x7f05000c + public static int abc_menu_function_shortcut_label = 2131034124; + + // aapt resource value: 0x7f05000d + public static int abc_menu_meta_shortcut_label = 2131034125; + + // aapt resource value: 0x7f05000e + public static int abc_menu_shift_shortcut_label = 2131034126; + + // aapt resource value: 0x7f05000f + public static int abc_menu_space_shortcut_label = 2131034127; + + // aapt resource value: 0x7f050010 + public static int abc_menu_sym_shortcut_label = 2131034128; + + // aapt resource value: 0x7f050011 + public static int abc_prepend_shortcut_label = 2131034129; + + // aapt resource value: 0x7f050012 + public static int abc_search_hint = 2131034130; + + // aapt resource value: 0x7f050013 + public static int abc_searchview_description_clear = 2131034131; + + // aapt resource value: 0x7f050014 + public static int abc_searchview_description_query = 2131034132; + + // aapt resource value: 0x7f050015 + public static int abc_searchview_description_search = 2131034133; + + // aapt resource value: 0x7f050016 + public static int abc_searchview_description_submit = 2131034134; + + // aapt resource value: 0x7f050017 + public static int abc_searchview_description_voice = 2131034135; + + // aapt resource value: 0x7f050018 + public static int abc_shareactionprovider_share_with = 2131034136; + + // aapt resource value: 0x7f050019 + public static int abc_shareactionprovider_share_with_application = 2131034137; + + // aapt resource value: 0x7f05001a + public static int abc_toolbar_collapse_description = 2131034138; + + // aapt resource value: 0x7f050041 + public static int app_name = 2131034177; + + // aapt resource value: 0x7f050040 + public static int hello = 2131034176; + + // aapt resource value: 0x7f050029 + public static int kilobytes_per_second = 2131034153; + + // aapt resource value: 0x7f05002a + public static int notification_download_complete = 2131034154; + + // aapt resource value: 0x7f05002b + public static int notification_download_failed = 2131034155; + + // aapt resource value: 0x7f05001b + public static int search_menu_title = 2131034139; + + // aapt resource value: 0x7f05002c + public static int state_completed = 2131034156; + + // aapt resource value: 0x7f05002d + public static int state_connecting = 2131034157; + + // aapt resource value: 0x7f05002e + public static int state_downloading = 2131034158; + + // aapt resource value: 0x7f05002f + public static int state_failed = 2131034159; + + // aapt resource value: 0x7f050030 + public static int state_failed_cancelled = 2131034160; + + // aapt resource value: 0x7f050031 + public static int state_failed_fetching_url = 2131034161; + + // aapt resource value: 0x7f050032 + public static int state_failed_sdcard_full = 2131034162; + + // aapt resource value: 0x7f050033 + public static int state_failed_unlicensed = 2131034163; + + // aapt resource value: 0x7f050034 + public static int state_fetching_url = 2131034164; + + // aapt resource value: 0x7f050035 + public static int state_idle = 2131034165; + + // aapt resource value: 0x7f050036 + public static int state_paused_by_request = 2131034166; + + // aapt resource value: 0x7f050037 + public static int state_paused_network_setup_failure = 2131034167; + + // aapt resource value: 0x7f050038 + public static int state_paused_network_unavailable = 2131034168; + + // aapt resource value: 0x7f050039 + public static int state_paused_roaming = 2131034169; + + // aapt resource value: 0x7f05003a + public static int state_paused_sdcard_unavailable = 2131034170; + + // aapt resource value: 0x7f05003b + public static int state_paused_wifi_disabled = 2131034171; + + // aapt resource value: 0x7f05003c + public static int state_paused_wifi_unavailable = 2131034172; + + // aapt resource value: 0x7f05003d + public static int state_unknown = 2131034173; + + // aapt resource value: 0x7f050028 + public static int status_bar_notification_info_overflow = 2131034152; + + // aapt resource value: 0x7f05003e + public static int time_remaining = 2131034174; + + // aapt resource value: 0x7f05003f + public static int time_remaining_notification = 2131034175; + + static String() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private String() + { + } + } + + public partial class Style + { + + // aapt resource value: 0x7f070089 + public static int AlertDialog_AppCompat = 2131165321; + + // aapt resource value: 0x7f07008a + public static int AlertDialog_AppCompat_Light = 2131165322; + + // aapt resource value: 0x7f07008b + public static int Animation_AppCompat_Dialog = 2131165323; + + // aapt resource value: 0x7f07008c + public static int Animation_AppCompat_DropDownUp = 2131165324; + + // aapt resource value: 0x7f07008d + public static int Animation_AppCompat_Tooltip = 2131165325; + + // aapt resource value: 0x7f07008e + public static int Base_AlertDialog_AppCompat = 2131165326; + + // aapt resource value: 0x7f07008f + public static int Base_AlertDialog_AppCompat_Light = 2131165327; + + // aapt resource value: 0x7f070090 + public static int Base_Animation_AppCompat_Dialog = 2131165328; + + // aapt resource value: 0x7f070091 + public static int Base_Animation_AppCompat_DropDownUp = 2131165329; + + // aapt resource value: 0x7f070092 + public static int Base_Animation_AppCompat_Tooltip = 2131165330; + + // aapt resource value: 0x7f070093 + public static int Base_DialogWindowTitle_AppCompat = 2131165331; + + // aapt resource value: 0x7f070094 + public static int Base_DialogWindowTitleBackground_AppCompat = 2131165332; + + // aapt resource value: 0x7f07001d + public static int Base_TextAppearance_AppCompat = 2131165213; + + // aapt resource value: 0x7f07001e + public static int Base_TextAppearance_AppCompat_Body1 = 2131165214; + + // aapt resource value: 0x7f07001f + public static int Base_TextAppearance_AppCompat_Body2 = 2131165215; + + // aapt resource value: 0x7f070020 + public static int Base_TextAppearance_AppCompat_Button = 2131165216; + + // aapt resource value: 0x7f070021 + public static int Base_TextAppearance_AppCompat_Caption = 2131165217; + + // aapt resource value: 0x7f070022 + public static int Base_TextAppearance_AppCompat_Display1 = 2131165218; + + // aapt resource value: 0x7f070023 + public static int Base_TextAppearance_AppCompat_Display2 = 2131165219; + + // aapt resource value: 0x7f070024 + public static int Base_TextAppearance_AppCompat_Display3 = 2131165220; + + // aapt resource value: 0x7f070025 + public static int Base_TextAppearance_AppCompat_Display4 = 2131165221; + + // aapt resource value: 0x7f070026 + public static int Base_TextAppearance_AppCompat_Headline = 2131165222; + + // aapt resource value: 0x7f070027 + public static int Base_TextAppearance_AppCompat_Inverse = 2131165223; + + // aapt resource value: 0x7f070028 + public static int Base_TextAppearance_AppCompat_Large = 2131165224; + + // aapt resource value: 0x7f070029 + public static int Base_TextAppearance_AppCompat_Large_Inverse = 2131165225; + + // aapt resource value: 0x7f07002a + public static int Base_TextAppearance_AppCompat_Light_Widget_PopupMenu_Large = 2131165226; + + // aapt resource value: 0x7f07002b + public static int Base_TextAppearance_AppCompat_Light_Widget_PopupMenu_Small = 2131165227; + + // aapt resource value: 0x7f07002c + public static int Base_TextAppearance_AppCompat_Medium = 2131165228; + + // aapt resource value: 0x7f07002d + public static int Base_TextAppearance_AppCompat_Medium_Inverse = 2131165229; + + // aapt resource value: 0x7f07002e + public static int Base_TextAppearance_AppCompat_Menu = 2131165230; + + // aapt resource value: 0x7f070095 + public static int Base_TextAppearance_AppCompat_SearchResult = 2131165333; + + // aapt resource value: 0x7f07002f + public static int Base_TextAppearance_AppCompat_SearchResult_Subtitle = 2131165231; + + // aapt resource value: 0x7f070030 + public static int Base_TextAppearance_AppCompat_SearchResult_Title = 2131165232; + + // aapt resource value: 0x7f070031 + public static int Base_TextAppearance_AppCompat_Small = 2131165233; + + // aapt resource value: 0x7f070032 + public static int Base_TextAppearance_AppCompat_Small_Inverse = 2131165234; + + // aapt resource value: 0x7f070033 + public static int Base_TextAppearance_AppCompat_Subhead = 2131165235; + + // aapt resource value: 0x7f070096 + public static int Base_TextAppearance_AppCompat_Subhead_Inverse = 2131165334; + + // aapt resource value: 0x7f070034 + public static int Base_TextAppearance_AppCompat_Title = 2131165236; + + // aapt resource value: 0x7f070097 + public static int Base_TextAppearance_AppCompat_Title_Inverse = 2131165335; + + // aapt resource value: 0x7f070098 + public static int Base_TextAppearance_AppCompat_Tooltip = 2131165336; + + // aapt resource value: 0x7f070078 + public static int Base_TextAppearance_AppCompat_Widget_ActionBar_Menu = 2131165304; + + // aapt resource value: 0x7f070035 + public static int Base_TextAppearance_AppCompat_Widget_ActionBar_Subtitle = 2131165237; + + // aapt resource value: 0x7f070036 + public static int Base_TextAppearance_AppCompat_Widget_ActionBar_Subtitle_Inverse = 2131165238; + + // aapt resource value: 0x7f070037 + public static int Base_TextAppearance_AppCompat_Widget_ActionBar_Title = 2131165239; + + // aapt resource value: 0x7f070038 + public static int Base_TextAppearance_AppCompat_Widget_ActionBar_Title_Inverse = 2131165240; + + // aapt resource value: 0x7f070039 + public static int Base_TextAppearance_AppCompat_Widget_ActionMode_Subtitle = 2131165241; + + // aapt resource value: 0x7f07003a + public static int Base_TextAppearance_AppCompat_Widget_ActionMode_Title = 2131165242; + + // aapt resource value: 0x7f07003b + public static int Base_TextAppearance_AppCompat_Widget_Button = 2131165243; + + // aapt resource value: 0x7f07007f + public static int Base_TextAppearance_AppCompat_Widget_Button_Borderless_Colored = 2131165311; + + // aapt resource value: 0x7f070080 + public static int Base_TextAppearance_AppCompat_Widget_Button_Colored = 2131165312; + + // aapt resource value: 0x7f070079 + public static int Base_TextAppearance_AppCompat_Widget_Button_Inverse = 2131165305; + + // aapt resource value: 0x7f070099 + public static int Base_TextAppearance_AppCompat_Widget_DropDownItem = 2131165337; + + // aapt resource value: 0x7f07003c + public static int Base_TextAppearance_AppCompat_Widget_PopupMenu_Header = 2131165244; + + // aapt resource value: 0x7f07003d + public static int Base_TextAppearance_AppCompat_Widget_PopupMenu_Large = 2131165245; + + // aapt resource value: 0x7f07003e + public static int Base_TextAppearance_AppCompat_Widget_PopupMenu_Small = 2131165246; + + // aapt resource value: 0x7f07003f + public static int Base_TextAppearance_AppCompat_Widget_Switch = 2131165247; + + // aapt resource value: 0x7f070040 + public static int Base_TextAppearance_AppCompat_Widget_TextView_SpinnerItem = 2131165248; + + // aapt resource value: 0x7f07009a + public static int Base_TextAppearance_Widget_AppCompat_ExpandedMenu_Item = 2131165338; + + // aapt resource value: 0x7f070041 + public static int Base_TextAppearance_Widget_AppCompat_Toolbar_Subtitle = 2131165249; + + // aapt resource value: 0x7f070042 + public static int Base_TextAppearance_Widget_AppCompat_Toolbar_Title = 2131165250; + + // aapt resource value: 0x7f070043 + public static int Base_Theme_AppCompat = 2131165251; + + // aapt resource value: 0x7f07009b + public static int Base_Theme_AppCompat_CompactMenu = 2131165339; + + // aapt resource value: 0x7f070044 + public static int Base_Theme_AppCompat_Dialog = 2131165252; + + // aapt resource value: 0x7f07009c + public static int Base_Theme_AppCompat_Dialog_Alert = 2131165340; + + // aapt resource value: 0x7f07009d + public static int Base_Theme_AppCompat_Dialog_FixedSize = 2131165341; + + // aapt resource value: 0x7f07009e + public static int Base_Theme_AppCompat_Dialog_MinWidth = 2131165342; + + // aapt resource value: 0x7f070001 + public static int Base_Theme_AppCompat_DialogWhenLarge = 2131165185; + + // aapt resource value: 0x7f070045 + public static int Base_Theme_AppCompat_Light = 2131165253; + + // aapt resource value: 0x7f07009f + public static int Base_Theme_AppCompat_Light_DarkActionBar = 2131165343; + + // aapt resource value: 0x7f070046 + public static int Base_Theme_AppCompat_Light_Dialog = 2131165254; + + // aapt resource value: 0x7f0700a0 + public static int Base_Theme_AppCompat_Light_Dialog_Alert = 2131165344; + + // aapt resource value: 0x7f0700a1 + public static int Base_Theme_AppCompat_Light_Dialog_FixedSize = 2131165345; + + // aapt resource value: 0x7f0700a2 + public static int Base_Theme_AppCompat_Light_Dialog_MinWidth = 2131165346; + + // aapt resource value: 0x7f070002 + public static int Base_Theme_AppCompat_Light_DialogWhenLarge = 2131165186; + + // aapt resource value: 0x7f0700a3 + public static int Base_ThemeOverlay_AppCompat = 2131165347; + + // aapt resource value: 0x7f0700a4 + public static int Base_ThemeOverlay_AppCompat_ActionBar = 2131165348; + + // aapt resource value: 0x7f0700a5 + public static int Base_ThemeOverlay_AppCompat_Dark = 2131165349; + + // aapt resource value: 0x7f0700a6 + public static int Base_ThemeOverlay_AppCompat_Dark_ActionBar = 2131165350; + + // aapt resource value: 0x7f070047 + public static int Base_ThemeOverlay_AppCompat_Dialog = 2131165255; + + // aapt resource value: 0x7f0700a7 + public static int Base_ThemeOverlay_AppCompat_Dialog_Alert = 2131165351; + + // aapt resource value: 0x7f0700a8 + public static int Base_ThemeOverlay_AppCompat_Light = 2131165352; + + // aapt resource value: 0x7f070048 + public static int Base_V21_Theme_AppCompat = 2131165256; + + // aapt resource value: 0x7f070049 + public static int Base_V21_Theme_AppCompat_Dialog = 2131165257; + + // aapt resource value: 0x7f07004a + public static int Base_V21_Theme_AppCompat_Light = 2131165258; + + // aapt resource value: 0x7f07004b + public static int Base_V21_Theme_AppCompat_Light_Dialog = 2131165259; + + // aapt resource value: 0x7f07004c + public static int Base_V21_ThemeOverlay_AppCompat_Dialog = 2131165260; + + // aapt resource value: 0x7f070076 + public static int Base_V22_Theme_AppCompat = 2131165302; + + // aapt resource value: 0x7f070077 + public static int Base_V22_Theme_AppCompat_Light = 2131165303; + + // aapt resource value: 0x7f07007a + public static int Base_V23_Theme_AppCompat = 2131165306; + + // aapt resource value: 0x7f07007b + public static int Base_V23_Theme_AppCompat_Light = 2131165307; + + // aapt resource value: 0x7f070083 + public static int Base_V26_Theme_AppCompat = 2131165315; + + // aapt resource value: 0x7f070084 + public static int Base_V26_Theme_AppCompat_Light = 2131165316; + + // aapt resource value: 0x7f070085 + public static int Base_V26_Widget_AppCompat_Toolbar = 2131165317; + + // aapt resource value: 0x7f070087 + public static int Base_V28_Theme_AppCompat = 2131165319; + + // aapt resource value: 0x7f070088 + public static int Base_V28_Theme_AppCompat_Light = 2131165320; + + // aapt resource value: 0x7f0700a9 + public static int Base_V7_Theme_AppCompat = 2131165353; + + // aapt resource value: 0x7f0700aa + public static int Base_V7_Theme_AppCompat_Dialog = 2131165354; + + // aapt resource value: 0x7f0700ab + public static int Base_V7_Theme_AppCompat_Light = 2131165355; + + // aapt resource value: 0x7f0700ac + public static int Base_V7_Theme_AppCompat_Light_Dialog = 2131165356; + + // aapt resource value: 0x7f0700ad + public static int Base_V7_ThemeOverlay_AppCompat_Dialog = 2131165357; + + // aapt resource value: 0x7f0700ae + public static int Base_V7_Widget_AppCompat_AutoCompleteTextView = 2131165358; + + // aapt resource value: 0x7f0700af + public static int Base_V7_Widget_AppCompat_EditText = 2131165359; + + // aapt resource value: 0x7f0700b0 + public static int Base_V7_Widget_AppCompat_Toolbar = 2131165360; + + // aapt resource value: 0x7f0700b1 + public static int Base_Widget_AppCompat_ActionBar = 2131165361; + + // aapt resource value: 0x7f0700b2 + public static int Base_Widget_AppCompat_ActionBar_Solid = 2131165362; + + // aapt resource value: 0x7f0700b3 + public static int Base_Widget_AppCompat_ActionBar_TabBar = 2131165363; + + // aapt resource value: 0x7f07004d + public static int Base_Widget_AppCompat_ActionBar_TabText = 2131165261; + + // aapt resource value: 0x7f07004e + public static int Base_Widget_AppCompat_ActionBar_TabView = 2131165262; + + // aapt resource value: 0x7f07004f + public static int Base_Widget_AppCompat_ActionButton = 2131165263; + + // aapt resource value: 0x7f070050 + public static int Base_Widget_AppCompat_ActionButton_CloseMode = 2131165264; + + // aapt resource value: 0x7f070051 + public static int Base_Widget_AppCompat_ActionButton_Overflow = 2131165265; + + // aapt resource value: 0x7f0700b4 + public static int Base_Widget_AppCompat_ActionMode = 2131165364; + + // aapt resource value: 0x7f0700b5 + public static int Base_Widget_AppCompat_ActivityChooserView = 2131165365; + + // aapt resource value: 0x7f070052 + public static int Base_Widget_AppCompat_AutoCompleteTextView = 2131165266; + + // aapt resource value: 0x7f070053 + public static int Base_Widget_AppCompat_Button = 2131165267; + + // aapt resource value: 0x7f070054 + public static int Base_Widget_AppCompat_Button_Borderless = 2131165268; + + // aapt resource value: 0x7f070055 + public static int Base_Widget_AppCompat_Button_Borderless_Colored = 2131165269; + + // aapt resource value: 0x7f0700b6 + public static int Base_Widget_AppCompat_Button_ButtonBar_AlertDialog = 2131165366; + + // aapt resource value: 0x7f07007c + public static int Base_Widget_AppCompat_Button_Colored = 2131165308; + + // aapt resource value: 0x7f070056 + public static int Base_Widget_AppCompat_Button_Small = 2131165270; + + // aapt resource value: 0x7f070057 + public static int Base_Widget_AppCompat_ButtonBar = 2131165271; + + // aapt resource value: 0x7f0700b7 + public static int Base_Widget_AppCompat_ButtonBar_AlertDialog = 2131165367; + + // aapt resource value: 0x7f070058 + public static int Base_Widget_AppCompat_CompoundButton_CheckBox = 2131165272; + + // aapt resource value: 0x7f070059 + public static int Base_Widget_AppCompat_CompoundButton_RadioButton = 2131165273; + + // aapt resource value: 0x7f0700b8 + public static int Base_Widget_AppCompat_CompoundButton_Switch = 2131165368; + + // aapt resource value: 0x7f070000 + public static int Base_Widget_AppCompat_DrawerArrowToggle = 2131165184; + + // aapt resource value: 0x7f0700b9 + public static int Base_Widget_AppCompat_DrawerArrowToggle_Common = 2131165369; + + // aapt resource value: 0x7f07005a + public static int Base_Widget_AppCompat_DropDownItem_Spinner = 2131165274; + + // aapt resource value: 0x7f07005b + public static int Base_Widget_AppCompat_EditText = 2131165275; + + // aapt resource value: 0x7f07005c + public static int Base_Widget_AppCompat_ImageButton = 2131165276; + + // aapt resource value: 0x7f0700ba + public static int Base_Widget_AppCompat_Light_ActionBar = 2131165370; + + // aapt resource value: 0x7f0700bb + public static int Base_Widget_AppCompat_Light_ActionBar_Solid = 2131165371; + + // aapt resource value: 0x7f0700bc + public static int Base_Widget_AppCompat_Light_ActionBar_TabBar = 2131165372; + + // aapt resource value: 0x7f07005d + public static int Base_Widget_AppCompat_Light_ActionBar_TabText = 2131165277; + + // aapt resource value: 0x7f07005e + public static int Base_Widget_AppCompat_Light_ActionBar_TabText_Inverse = 2131165278; + + // aapt resource value: 0x7f07005f + public static int Base_Widget_AppCompat_Light_ActionBar_TabView = 2131165279; + + // aapt resource value: 0x7f070060 + public static int Base_Widget_AppCompat_Light_PopupMenu = 2131165280; + + // aapt resource value: 0x7f070061 + public static int Base_Widget_AppCompat_Light_PopupMenu_Overflow = 2131165281; + + // aapt resource value: 0x7f0700bd + public static int Base_Widget_AppCompat_ListMenuView = 2131165373; + + // aapt resource value: 0x7f070062 + public static int Base_Widget_AppCompat_ListPopupWindow = 2131165282; + + // aapt resource value: 0x7f070063 + public static int Base_Widget_AppCompat_ListView = 2131165283; + + // aapt resource value: 0x7f070064 + public static int Base_Widget_AppCompat_ListView_DropDown = 2131165284; + + // aapt resource value: 0x7f070065 + public static int Base_Widget_AppCompat_ListView_Menu = 2131165285; + + // aapt resource value: 0x7f070066 + public static int Base_Widget_AppCompat_PopupMenu = 2131165286; + + // aapt resource value: 0x7f070067 + public static int Base_Widget_AppCompat_PopupMenu_Overflow = 2131165287; + + // aapt resource value: 0x7f0700be + public static int Base_Widget_AppCompat_PopupWindow = 2131165374; + + // aapt resource value: 0x7f070068 + public static int Base_Widget_AppCompat_ProgressBar = 2131165288; + + // aapt resource value: 0x7f070069 + public static int Base_Widget_AppCompat_ProgressBar_Horizontal = 2131165289; + + // aapt resource value: 0x7f07006a + public static int Base_Widget_AppCompat_RatingBar = 2131165290; + + // aapt resource value: 0x7f07007d + public static int Base_Widget_AppCompat_RatingBar_Indicator = 2131165309; + + // aapt resource value: 0x7f07007e + public static int Base_Widget_AppCompat_RatingBar_Small = 2131165310; + + // aapt resource value: 0x7f0700bf + public static int Base_Widget_AppCompat_SearchView = 2131165375; + + // aapt resource value: 0x7f0700c0 + public static int Base_Widget_AppCompat_SearchView_ActionBar = 2131165376; + + // aapt resource value: 0x7f07006b + public static int Base_Widget_AppCompat_SeekBar = 2131165291; + + // aapt resource value: 0x7f0700c1 + public static int Base_Widget_AppCompat_SeekBar_Discrete = 2131165377; + + // aapt resource value: 0x7f07006c + public static int Base_Widget_AppCompat_Spinner = 2131165292; + + // aapt resource value: 0x7f070003 + public static int Base_Widget_AppCompat_Spinner_Underlined = 2131165187; + + // aapt resource value: 0x7f07006d + public static int Base_Widget_AppCompat_TextView_SpinnerItem = 2131165293; + + // aapt resource value: 0x7f070086 + public static int Base_Widget_AppCompat_Toolbar = 2131165318; + + // aapt resource value: 0x7f07006e + public static int Base_Widget_AppCompat_Toolbar_Button_Navigation = 2131165294; + + // aapt resource value: 0x7f07015d + public static int ButtonBackground = 2131165533; + + // aapt resource value: 0x7f07015b + public static int NotificationText = 2131165531; + + // aapt resource value: 0x7f07015a + public static int NotificationTextSecondary = 2131165530; + + // aapt resource value: 0x7f07015e + public static int NotificationTextShadow = 2131165534; + + // aapt resource value: 0x7f07015c + public static int NotificationTitle = 2131165532; + + // aapt resource value: 0x7f07006f + public static int Platform_AppCompat = 2131165295; + + // aapt resource value: 0x7f070070 + public static int Platform_AppCompat_Light = 2131165296; + + // aapt resource value: 0x7f070071 + public static int Platform_ThemeOverlay_AppCompat = 2131165297; + + // aapt resource value: 0x7f070072 + public static int Platform_ThemeOverlay_AppCompat_Dark = 2131165298; + + // aapt resource value: 0x7f070073 + public static int Platform_ThemeOverlay_AppCompat_Light = 2131165299; + + // aapt resource value: 0x7f070074 + public static int Platform_V21_AppCompat = 2131165300; + + // aapt resource value: 0x7f070075 + public static int Platform_V21_AppCompat_Light = 2131165301; + + // aapt resource value: 0x7f070081 + public static int Platform_V25_AppCompat = 2131165313; + + // aapt resource value: 0x7f070082 + public static int Platform_V25_AppCompat_Light = 2131165314; + + // aapt resource value: 0x7f0700c2 + public static int Platform_Widget_AppCompat_Spinner = 2131165378; + + // aapt resource value: 0x7f07000c + public static int RtlOverlay_DialogWindowTitle_AppCompat = 2131165196; + + // aapt resource value: 0x7f07000d + public static int RtlOverlay_Widget_AppCompat_ActionBar_TitleItem = 2131165197; + + // aapt resource value: 0x7f07000e + public static int RtlOverlay_Widget_AppCompat_DialogTitle_Icon = 2131165198; + + // aapt resource value: 0x7f07000f + public static int RtlOverlay_Widget_AppCompat_PopupMenuItem = 2131165199; + + // aapt resource value: 0x7f070010 + public static int RtlOverlay_Widget_AppCompat_PopupMenuItem_InternalGroup = 2131165200; + + // aapt resource value: 0x7f070011 + public static int RtlOverlay_Widget_AppCompat_PopupMenuItem_Shortcut = 2131165201; + + // aapt resource value: 0x7f070012 + public static int RtlOverlay_Widget_AppCompat_PopupMenuItem_SubmenuArrow = 2131165202; + + // aapt resource value: 0x7f070013 + public static int RtlOverlay_Widget_AppCompat_PopupMenuItem_Text = 2131165203; + + // aapt resource value: 0x7f070014 + public static int RtlOverlay_Widget_AppCompat_PopupMenuItem_Title = 2131165204; + + // aapt resource value: 0x7f070015 + public static int RtlOverlay_Widget_AppCompat_Search_DropDown = 2131165205; + + // aapt resource value: 0x7f070016 + public static int RtlOverlay_Widget_AppCompat_Search_DropDown_Icon1 = 2131165206; + + // aapt resource value: 0x7f070017 + public static int RtlOverlay_Widget_AppCompat_Search_DropDown_Icon2 = 2131165207; + + // aapt resource value: 0x7f070018 + public static int RtlOverlay_Widget_AppCompat_Search_DropDown_Query = 2131165208; + + // aapt resource value: 0x7f070019 + public static int RtlOverlay_Widget_AppCompat_Search_DropDown_Text = 2131165209; + + // aapt resource value: 0x7f07001a + public static int RtlOverlay_Widget_AppCompat_SearchView_MagIcon = 2131165210; + + // aapt resource value: 0x7f07001b + public static int RtlUnderlay_Widget_AppCompat_ActionButton = 2131165211; + + // aapt resource value: 0x7f07001c + public static int RtlUnderlay_Widget_AppCompat_ActionButton_Overflow = 2131165212; + + // aapt resource value: 0x7f0700c3 + public static int TextAppearance_AppCompat = 2131165379; + + // aapt resource value: 0x7f0700c4 + public static int TextAppearance_AppCompat_Body1 = 2131165380; + + // aapt resource value: 0x7f0700c5 + public static int TextAppearance_AppCompat_Body2 = 2131165381; + + // aapt resource value: 0x7f0700c6 + public static int TextAppearance_AppCompat_Button = 2131165382; + + // aapt resource value: 0x7f0700c7 + public static int TextAppearance_AppCompat_Caption = 2131165383; + + // aapt resource value: 0x7f0700c8 + public static int TextAppearance_AppCompat_Display1 = 2131165384; + + // aapt resource value: 0x7f0700c9 + public static int TextAppearance_AppCompat_Display2 = 2131165385; + + // aapt resource value: 0x7f0700ca + public static int TextAppearance_AppCompat_Display3 = 2131165386; + + // aapt resource value: 0x7f0700cb + public static int TextAppearance_AppCompat_Display4 = 2131165387; + + // aapt resource value: 0x7f0700cc + public static int TextAppearance_AppCompat_Headline = 2131165388; + + // aapt resource value: 0x7f0700cd + public static int TextAppearance_AppCompat_Inverse = 2131165389; + + // aapt resource value: 0x7f0700ce + public static int TextAppearance_AppCompat_Large = 2131165390; + + // aapt resource value: 0x7f0700cf + public static int TextAppearance_AppCompat_Large_Inverse = 2131165391; + + // aapt resource value: 0x7f0700d0 + public static int TextAppearance_AppCompat_Light_SearchResult_Subtitle = 2131165392; + + // aapt resource value: 0x7f0700d1 + public static int TextAppearance_AppCompat_Light_SearchResult_Title = 2131165393; + + // aapt resource value: 0x7f0700d2 + public static int TextAppearance_AppCompat_Light_Widget_PopupMenu_Large = 2131165394; + + // aapt resource value: 0x7f0700d3 + public static int TextAppearance_AppCompat_Light_Widget_PopupMenu_Small = 2131165395; + + // aapt resource value: 0x7f0700d4 + public static int TextAppearance_AppCompat_Medium = 2131165396; + + // aapt resource value: 0x7f0700d5 + public static int TextAppearance_AppCompat_Medium_Inverse = 2131165397; + + // aapt resource value: 0x7f0700d6 + public static int TextAppearance_AppCompat_Menu = 2131165398; + + // aapt resource value: 0x7f0700d7 + public static int TextAppearance_AppCompat_SearchResult_Subtitle = 2131165399; + + // aapt resource value: 0x7f0700d8 + public static int TextAppearance_AppCompat_SearchResult_Title = 2131165400; + + // aapt resource value: 0x7f0700d9 + public static int TextAppearance_AppCompat_Small = 2131165401; + + // aapt resource value: 0x7f0700da + public static int TextAppearance_AppCompat_Small_Inverse = 2131165402; + + // aapt resource value: 0x7f0700db + public static int TextAppearance_AppCompat_Subhead = 2131165403; + + // aapt resource value: 0x7f0700dc + public static int TextAppearance_AppCompat_Subhead_Inverse = 2131165404; + + // aapt resource value: 0x7f0700dd + public static int TextAppearance_AppCompat_Title = 2131165405; + + // aapt resource value: 0x7f0700de + public static int TextAppearance_AppCompat_Title_Inverse = 2131165406; + + // aapt resource value: 0x7f07000b + public static int TextAppearance_AppCompat_Tooltip = 2131165195; + + // aapt resource value: 0x7f0700df + public static int TextAppearance_AppCompat_Widget_ActionBar_Menu = 2131165407; + + // aapt resource value: 0x7f0700e0 + public static int TextAppearance_AppCompat_Widget_ActionBar_Subtitle = 2131165408; + + // aapt resource value: 0x7f0700e1 + public static int TextAppearance_AppCompat_Widget_ActionBar_Subtitle_Inverse = 2131165409; + + // aapt resource value: 0x7f0700e2 + public static int TextAppearance_AppCompat_Widget_ActionBar_Title = 2131165410; + + // aapt resource value: 0x7f0700e3 + public static int TextAppearance_AppCompat_Widget_ActionBar_Title_Inverse = 2131165411; + + // aapt resource value: 0x7f0700e4 + public static int TextAppearance_AppCompat_Widget_ActionMode_Subtitle = 2131165412; + + // aapt resource value: 0x7f0700e5 + public static int TextAppearance_AppCompat_Widget_ActionMode_Subtitle_Inverse = 2131165413; + + // aapt resource value: 0x7f0700e6 + public static int TextAppearance_AppCompat_Widget_ActionMode_Title = 2131165414; + + // aapt resource value: 0x7f0700e7 + public static int TextAppearance_AppCompat_Widget_ActionMode_Title_Inverse = 2131165415; + + // aapt resource value: 0x7f0700e8 + public static int TextAppearance_AppCompat_Widget_Button = 2131165416; + + // aapt resource value: 0x7f0700e9 + public static int TextAppearance_AppCompat_Widget_Button_Borderless_Colored = 2131165417; + + // aapt resource value: 0x7f0700ea + public static int TextAppearance_AppCompat_Widget_Button_Colored = 2131165418; + + // aapt resource value: 0x7f0700eb + public static int TextAppearance_AppCompat_Widget_Button_Inverse = 2131165419; + + // aapt resource value: 0x7f0700ec + public static int TextAppearance_AppCompat_Widget_DropDownItem = 2131165420; + + // aapt resource value: 0x7f0700ed + public static int TextAppearance_AppCompat_Widget_PopupMenu_Header = 2131165421; + + // aapt resource value: 0x7f0700ee + public static int TextAppearance_AppCompat_Widget_PopupMenu_Large = 2131165422; + + // aapt resource value: 0x7f0700ef + public static int TextAppearance_AppCompat_Widget_PopupMenu_Small = 2131165423; + + // aapt resource value: 0x7f0700f0 + public static int TextAppearance_AppCompat_Widget_Switch = 2131165424; + + // aapt resource value: 0x7f0700f1 + public static int TextAppearance_AppCompat_Widget_TextView_SpinnerItem = 2131165425; + + // aapt resource value: 0x7f070153 + public static int TextAppearance_Compat_Notification = 2131165523; + + // aapt resource value: 0x7f070154 + public static int TextAppearance_Compat_Notification_Info = 2131165524; + + // aapt resource value: 0x7f070159 + public static int TextAppearance_Compat_Notification_Line2 = 2131165529; + + // aapt resource value: 0x7f070155 + public static int TextAppearance_Compat_Notification_Time = 2131165525; + + // aapt resource value: 0x7f070156 + public static int TextAppearance_Compat_Notification_Title = 2131165526; + + // aapt resource value: 0x7f0700f2 + public static int TextAppearance_Widget_AppCompat_ExpandedMenu_Item = 2131165426; + + // aapt resource value: 0x7f0700f3 + public static int TextAppearance_Widget_AppCompat_Toolbar_Subtitle = 2131165427; + + // aapt resource value: 0x7f0700f4 + public static int TextAppearance_Widget_AppCompat_Toolbar_Title = 2131165428; + + // aapt resource value: 0x7f0700f5 + public static int Theme_AppCompat = 2131165429; + + // aapt resource value: 0x7f0700f6 + public static int Theme_AppCompat_CompactMenu = 2131165430; + + // aapt resource value: 0x7f070004 + public static int Theme_AppCompat_DayNight = 2131165188; + + // aapt resource value: 0x7f070005 + public static int Theme_AppCompat_DayNight_DarkActionBar = 2131165189; + + // aapt resource value: 0x7f070006 + public static int Theme_AppCompat_DayNight_Dialog = 2131165190; + + // aapt resource value: 0x7f070007 + public static int Theme_AppCompat_DayNight_Dialog_Alert = 2131165191; + + // aapt resource value: 0x7f070008 + public static int Theme_AppCompat_DayNight_Dialog_MinWidth = 2131165192; + + // aapt resource value: 0x7f070009 + public static int Theme_AppCompat_DayNight_DialogWhenLarge = 2131165193; + + // aapt resource value: 0x7f07000a + public static int Theme_AppCompat_DayNight_NoActionBar = 2131165194; + + // aapt resource value: 0x7f0700f7 + public static int Theme_AppCompat_Dialog = 2131165431; + + // aapt resource value: 0x7f0700f8 + public static int Theme_AppCompat_Dialog_Alert = 2131165432; + + // aapt resource value: 0x7f0700f9 + public static int Theme_AppCompat_Dialog_MinWidth = 2131165433; + + // aapt resource value: 0x7f0700fa + public static int Theme_AppCompat_DialogWhenLarge = 2131165434; + + // aapt resource value: 0x7f0700fb + public static int Theme_AppCompat_Light = 2131165435; + + // aapt resource value: 0x7f0700fc + public static int Theme_AppCompat_Light_DarkActionBar = 2131165436; + + // aapt resource value: 0x7f0700fd + public static int Theme_AppCompat_Light_Dialog = 2131165437; + + // aapt resource value: 0x7f0700fe + public static int Theme_AppCompat_Light_Dialog_Alert = 2131165438; + + // aapt resource value: 0x7f0700ff + public static int Theme_AppCompat_Light_Dialog_MinWidth = 2131165439; + + // aapt resource value: 0x7f070100 + public static int Theme_AppCompat_Light_DialogWhenLarge = 2131165440; + + // aapt resource value: 0x7f070101 + public static int Theme_AppCompat_Light_NoActionBar = 2131165441; + + // aapt resource value: 0x7f070102 + public static int Theme_AppCompat_NoActionBar = 2131165442; + + // aapt resource value: 0x7f070103 + public static int ThemeOverlay_AppCompat = 2131165443; + + // aapt resource value: 0x7f070104 + public static int ThemeOverlay_AppCompat_ActionBar = 2131165444; + + // aapt resource value: 0x7f070105 + public static int ThemeOverlay_AppCompat_Dark = 2131165445; + + // aapt resource value: 0x7f070106 + public static int ThemeOverlay_AppCompat_Dark_ActionBar = 2131165446; + + // aapt resource value: 0x7f070107 + public static int ThemeOverlay_AppCompat_Dialog = 2131165447; + + // aapt resource value: 0x7f070108 + public static int ThemeOverlay_AppCompat_Dialog_Alert = 2131165448; + + // aapt resource value: 0x7f070109 + public static int ThemeOverlay_AppCompat_Light = 2131165449; + + // aapt resource value: 0x7f07010a + public static int Widget_AppCompat_ActionBar = 2131165450; + + // aapt resource value: 0x7f07010b + public static int Widget_AppCompat_ActionBar_Solid = 2131165451; + + // aapt resource value: 0x7f07010c + public static int Widget_AppCompat_ActionBar_TabBar = 2131165452; + + // aapt resource value: 0x7f07010d + public static int Widget_AppCompat_ActionBar_TabText = 2131165453; + + // aapt resource value: 0x7f07010e + public static int Widget_AppCompat_ActionBar_TabView = 2131165454; + + // aapt resource value: 0x7f07010f + public static int Widget_AppCompat_ActionButton = 2131165455; + + // aapt resource value: 0x7f070110 + public static int Widget_AppCompat_ActionButton_CloseMode = 2131165456; + + // aapt resource value: 0x7f070111 + public static int Widget_AppCompat_ActionButton_Overflow = 2131165457; + + // aapt resource value: 0x7f070112 + public static int Widget_AppCompat_ActionMode = 2131165458; + + // aapt resource value: 0x7f070113 + public static int Widget_AppCompat_ActivityChooserView = 2131165459; + + // aapt resource value: 0x7f070114 + public static int Widget_AppCompat_AutoCompleteTextView = 2131165460; + + // aapt resource value: 0x7f070115 + public static int Widget_AppCompat_Button = 2131165461; + + // aapt resource value: 0x7f070116 + public static int Widget_AppCompat_Button_Borderless = 2131165462; + + // aapt resource value: 0x7f070117 + public static int Widget_AppCompat_Button_Borderless_Colored = 2131165463; + + // aapt resource value: 0x7f070118 + public static int Widget_AppCompat_Button_ButtonBar_AlertDialog = 2131165464; + + // aapt resource value: 0x7f070119 + public static int Widget_AppCompat_Button_Colored = 2131165465; + + // aapt resource value: 0x7f07011a + public static int Widget_AppCompat_Button_Small = 2131165466; + + // aapt resource value: 0x7f07011b + public static int Widget_AppCompat_ButtonBar = 2131165467; + + // aapt resource value: 0x7f07011c + public static int Widget_AppCompat_ButtonBar_AlertDialog = 2131165468; + + // aapt resource value: 0x7f07011d + public static int Widget_AppCompat_CompoundButton_CheckBox = 2131165469; + + // aapt resource value: 0x7f07011e + public static int Widget_AppCompat_CompoundButton_RadioButton = 2131165470; + + // aapt resource value: 0x7f07011f + public static int Widget_AppCompat_CompoundButton_Switch = 2131165471; + + // aapt resource value: 0x7f070120 + public static int Widget_AppCompat_DrawerArrowToggle = 2131165472; + + // aapt resource value: 0x7f070121 + public static int Widget_AppCompat_DropDownItem_Spinner = 2131165473; + + // aapt resource value: 0x7f070122 + public static int Widget_AppCompat_EditText = 2131165474; + + // aapt resource value: 0x7f070123 + public static int Widget_AppCompat_ImageButton = 2131165475; + + // aapt resource value: 0x7f070124 + public static int Widget_AppCompat_Light_ActionBar = 2131165476; + + // aapt resource value: 0x7f070125 + public static int Widget_AppCompat_Light_ActionBar_Solid = 2131165477; + + // aapt resource value: 0x7f070126 + public static int Widget_AppCompat_Light_ActionBar_Solid_Inverse = 2131165478; + + // aapt resource value: 0x7f070127 + public static int Widget_AppCompat_Light_ActionBar_TabBar = 2131165479; + + // aapt resource value: 0x7f070128 + public static int Widget_AppCompat_Light_ActionBar_TabBar_Inverse = 2131165480; + + // aapt resource value: 0x7f070129 + public static int Widget_AppCompat_Light_ActionBar_TabText = 2131165481; + + // aapt resource value: 0x7f07012a + public static int Widget_AppCompat_Light_ActionBar_TabText_Inverse = 2131165482; + + // aapt resource value: 0x7f07012b + public static int Widget_AppCompat_Light_ActionBar_TabView = 2131165483; + + // aapt resource value: 0x7f07012c + public static int Widget_AppCompat_Light_ActionBar_TabView_Inverse = 2131165484; + + // aapt resource value: 0x7f07012d + public static int Widget_AppCompat_Light_ActionButton = 2131165485; + + // aapt resource value: 0x7f07012e + public static int Widget_AppCompat_Light_ActionButton_CloseMode = 2131165486; + + // aapt resource value: 0x7f07012f + public static int Widget_AppCompat_Light_ActionButton_Overflow = 2131165487; + + // aapt resource value: 0x7f070130 + public static int Widget_AppCompat_Light_ActionMode_Inverse = 2131165488; + + // aapt resource value: 0x7f070131 + public static int Widget_AppCompat_Light_ActivityChooserView = 2131165489; + + // aapt resource value: 0x7f070132 + public static int Widget_AppCompat_Light_AutoCompleteTextView = 2131165490; + + // aapt resource value: 0x7f070133 + public static int Widget_AppCompat_Light_DropDownItem_Spinner = 2131165491; + + // aapt resource value: 0x7f070134 + public static int Widget_AppCompat_Light_ListPopupWindow = 2131165492; + + // aapt resource value: 0x7f070135 + public static int Widget_AppCompat_Light_ListView_DropDown = 2131165493; + + // aapt resource value: 0x7f070136 + public static int Widget_AppCompat_Light_PopupMenu = 2131165494; + + // aapt resource value: 0x7f070137 + public static int Widget_AppCompat_Light_PopupMenu_Overflow = 2131165495; + + // aapt resource value: 0x7f070138 + public static int Widget_AppCompat_Light_SearchView = 2131165496; + + // aapt resource value: 0x7f070139 + public static int Widget_AppCompat_Light_Spinner_DropDown_ActionBar = 2131165497; + + // aapt resource value: 0x7f07013a + public static int Widget_AppCompat_ListMenuView = 2131165498; + + // aapt resource value: 0x7f07013b + public static int Widget_AppCompat_ListPopupWindow = 2131165499; + + // aapt resource value: 0x7f07013c + public static int Widget_AppCompat_ListView = 2131165500; + + // aapt resource value: 0x7f07013d + public static int Widget_AppCompat_ListView_DropDown = 2131165501; + + // aapt resource value: 0x7f07013e + public static int Widget_AppCompat_ListView_Menu = 2131165502; + + // aapt resource value: 0x7f07013f + public static int Widget_AppCompat_PopupMenu = 2131165503; + + // aapt resource value: 0x7f070140 + public static int Widget_AppCompat_PopupMenu_Overflow = 2131165504; + + // aapt resource value: 0x7f070141 + public static int Widget_AppCompat_PopupWindow = 2131165505; + + // aapt resource value: 0x7f070142 + public static int Widget_AppCompat_ProgressBar = 2131165506; + + // aapt resource value: 0x7f070143 + public static int Widget_AppCompat_ProgressBar_Horizontal = 2131165507; + + // aapt resource value: 0x7f070144 + public static int Widget_AppCompat_RatingBar = 2131165508; + + // aapt resource value: 0x7f070145 + public static int Widget_AppCompat_RatingBar_Indicator = 2131165509; + + // aapt resource value: 0x7f070146 + public static int Widget_AppCompat_RatingBar_Small = 2131165510; + + // aapt resource value: 0x7f070147 + public static int Widget_AppCompat_SearchView = 2131165511; + + // aapt resource value: 0x7f070148 + public static int Widget_AppCompat_SearchView_ActionBar = 2131165512; + + // aapt resource value: 0x7f070149 + public static int Widget_AppCompat_SeekBar = 2131165513; + + // aapt resource value: 0x7f07014a + public static int Widget_AppCompat_SeekBar_Discrete = 2131165514; + + // aapt resource value: 0x7f07014b + public static int Widget_AppCompat_Spinner = 2131165515; + + // aapt resource value: 0x7f07014c + public static int Widget_AppCompat_Spinner_DropDown = 2131165516; + + // aapt resource value: 0x7f07014d + public static int Widget_AppCompat_Spinner_DropDown_ActionBar = 2131165517; + + // aapt resource value: 0x7f07014e + public static int Widget_AppCompat_Spinner_Underlined = 2131165518; + + // aapt resource value: 0x7f07014f + public static int Widget_AppCompat_TextView_SpinnerItem = 2131165519; + + // aapt resource value: 0x7f070150 + public static int Widget_AppCompat_Toolbar = 2131165520; + + // aapt resource value: 0x7f070151 + public static int Widget_AppCompat_Toolbar_Button_Navigation = 2131165521; + + // aapt resource value: 0x7f070157 + public static int Widget_Compat_NotificationActionContainer = 2131165527; + + // aapt resource value: 0x7f070158 + public static int Widget_Compat_NotificationActionText = 2131165528; + + // aapt resource value: 0x7f070152 + public static int Widget_Support_CoordinatorLayout = 2131165522; + + static Style() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Style() + { + } + } + + public partial class Styleable + { + + public static int[] ActionBar = new int[] { + 2130771969, + 2130771971, + 2130771972, + 2130771973, + 2130771974, + 2130771975, + 2130771976, + 2130771977, + 2130771978, + 2130771979, + 2130771980, + 2130771981, + 2130771982, + 2130771983, + 2130771984, + 2130771985, + 2130771986, + 2130771987, + 2130771988, + 2130771989, + 2130771990, + 2130771991, + 2130771992, + 2130771993, + 2130771994, + 2130771995, + 2130771996, + 2130771997, + 2130772072}; + + // aapt resource value: 10 + public static int ActionBar_background = 10; + + // aapt resource value: 12 + public static int ActionBar_backgroundSplit = 12; + + // aapt resource value: 11 + public static int ActionBar_backgroundStacked = 11; + + // aapt resource value: 21 + public static int ActionBar_contentInsetEnd = 21; + + // aapt resource value: 25 + public static int ActionBar_contentInsetEndWithActions = 25; + + // aapt resource value: 22 + public static int ActionBar_contentInsetLeft = 22; + + // aapt resource value: 23 + public static int ActionBar_contentInsetRight = 23; + + // aapt resource value: 20 + public static int ActionBar_contentInsetStart = 20; + + // aapt resource value: 24 + public static int ActionBar_contentInsetStartWithNavigation = 24; + + // aapt resource value: 13 + public static int ActionBar_customNavigationLayout = 13; + + // aapt resource value: 3 + public static int ActionBar_displayOptions = 3; + + // aapt resource value: 9 + public static int ActionBar_divider = 9; + + // aapt resource value: 26 + public static int ActionBar_elevation = 26; + + // aapt resource value: 0 + public static int ActionBar_height = 0; + + // aapt resource value: 19 + public static int ActionBar_hideOnContentScroll = 19; + + // aapt resource value: 28 + public static int ActionBar_homeAsUpIndicator = 28; + + // aapt resource value: 14 + public static int ActionBar_homeLayout = 14; + + // aapt resource value: 7 + public static int ActionBar_icon = 7; + + // aapt resource value: 16 + public static int ActionBar_indeterminateProgressStyle = 16; + + // aapt resource value: 18 + public static int ActionBar_itemPadding = 18; + + // aapt resource value: 8 + public static int ActionBar_logo = 8; + + // aapt resource value: 2 + public static int ActionBar_navigationMode = 2; + + // aapt resource value: 27 + public static int ActionBar_popupTheme = 27; + + // aapt resource value: 17 + public static int ActionBar_progressBarPadding = 17; + + // aapt resource value: 15 + public static int ActionBar_progressBarStyle = 15; + + // aapt resource value: 4 + public static int ActionBar_subtitle = 4; + + // aapt resource value: 6 + public static int ActionBar_subtitleTextStyle = 6; + + // aapt resource value: 1 + public static int ActionBar_title = 1; + + // aapt resource value: 5 + public static int ActionBar_titleTextStyle = 5; + + public static int[] ActionBarLayout = new int[] { + 16842931}; + + // aapt resource value: 0 + public static int ActionBarLayout_android_layout_gravity = 0; + + public static int[] ActionMenuItemView = new int[] { + 16843071}; + + // aapt resource value: 0 + public static int ActionMenuItemView_android_minWidth = 0; + + public static int[] ActionMenuView; + + public static int[] ActionMode = new int[] { + 2130771969, + 2130771975, + 2130771976, + 2130771980, + 2130771982, + 2130771998}; + + // aapt resource value: 3 + public static int ActionMode_background = 3; + + // aapt resource value: 4 + public static int ActionMode_backgroundSplit = 4; + + // aapt resource value: 5 + public static int ActionMode_closeItemLayout = 5; + + // aapt resource value: 0 + public static int ActionMode_height = 0; + + // aapt resource value: 2 + public static int ActionMode_subtitleTextStyle = 2; + + // aapt resource value: 1 + public static int ActionMode_titleTextStyle = 1; + + public static int[] ActivityChooserView = new int[] { + 2130771999, + 2130772000}; + + // aapt resource value: 1 + public static int ActivityChooserView_expandActivityOverflowButtonDrawable = 1; + + // aapt resource value: 0 + public static int ActivityChooserView_initialActivityCount = 0; + + public static int[] AlertDialog = new int[] { + 16842994, + 2130772001, + 2130772002, + 2130772003, + 2130772004, + 2130772005, + 2130772006, + 2130772007}; + + // aapt resource value: 0 + public static int AlertDialog_android_layout = 0; + + // aapt resource value: 7 + public static int AlertDialog_buttonIconDimen = 7; + + // aapt resource value: 1 + public static int AlertDialog_buttonPanelSideLayout = 1; + + // aapt resource value: 5 + public static int AlertDialog_listItemLayout = 5; + + // aapt resource value: 2 + public static int AlertDialog_listLayout = 2; + + // aapt resource value: 3 + public static int AlertDialog_multiChoiceItemLayout = 3; + + // aapt resource value: 6 + public static int AlertDialog_showTitle = 6; + + // aapt resource value: 4 + public static int AlertDialog_singleChoiceItemLayout = 4; + + public static int[] AnimatedStateListDrawableCompat = new int[] { + 16843036, + 16843156, + 16843157, + 16843158, + 16843532, + 16843533}; + + // aapt resource value: 3 + public static int AnimatedStateListDrawableCompat_android_constantSize = 3; + + // aapt resource value: 0 + public static int AnimatedStateListDrawableCompat_android_dither = 0; + + // aapt resource value: 4 + public static int AnimatedStateListDrawableCompat_android_enterFadeDuration = 4; + + // aapt resource value: 5 + public static int AnimatedStateListDrawableCompat_android_exitFadeDuration = 5; + + // aapt resource value: 2 + public static int AnimatedStateListDrawableCompat_android_variablePadding = 2; + + // aapt resource value: 1 + public static int AnimatedStateListDrawableCompat_android_visible = 1; + + public static int[] AnimatedStateListDrawableItem = new int[] { + 16842960, + 16843161}; + + // aapt resource value: 1 + public static int AnimatedStateListDrawableItem_android_drawable = 1; + + // aapt resource value: 0 + public static int AnimatedStateListDrawableItem_android_id = 0; + + public static int[] AnimatedStateListDrawableTransition = new int[] { + 16843161, + 16843849, + 16843850, + 16843851}; + + // aapt resource value: 0 + public static int AnimatedStateListDrawableTransition_android_drawable = 0; + + // aapt resource value: 2 + public static int AnimatedStateListDrawableTransition_android_fromId = 2; + + // aapt resource value: 3 + public static int AnimatedStateListDrawableTransition_android_reversible = 3; + + // aapt resource value: 1 + public static int AnimatedStateListDrawableTransition_android_toId = 1; + + public static int[] AppCompatImageView = new int[] { + 16843033, + 2130772008, + 2130772009, + 2130772010}; + + // aapt resource value: 0 + public static int AppCompatImageView_android_src = 0; + + // aapt resource value: 1 + public static int AppCompatImageView_srcCompat = 1; + + // aapt resource value: 2 + public static int AppCompatImageView_tint = 2; + + // aapt resource value: 3 + public static int AppCompatImageView_tintMode = 3; + + public static int[] AppCompatSeekBar = new int[] { + 16843074, + 2130772011, + 2130772012, + 2130772013}; + + // aapt resource value: 0 + public static int AppCompatSeekBar_android_thumb = 0; + + // aapt resource value: 1 + public static int AppCompatSeekBar_tickMark = 1; + + // aapt resource value: 2 + public static int AppCompatSeekBar_tickMarkTint = 2; + + // aapt resource value: 3 + public static int AppCompatSeekBar_tickMarkTintMode = 3; + + public static int[] AppCompatTextHelper = new int[] { + 16842804, + 16843117, + 16843118, + 16843119, + 16843120, + 16843666, + 16843667}; + + // aapt resource value: 2 + public static int AppCompatTextHelper_android_drawableBottom = 2; + + // aapt resource value: 6 + public static int AppCompatTextHelper_android_drawableEnd = 6; + + // aapt resource value: 3 + public static int AppCompatTextHelper_android_drawableLeft = 3; + + // aapt resource value: 4 + public static int AppCompatTextHelper_android_drawableRight = 4; + + // aapt resource value: 5 + public static int AppCompatTextHelper_android_drawableStart = 5; + + // aapt resource value: 1 + public static int AppCompatTextHelper_android_drawableTop = 1; + + // aapt resource value: 0 + public static int AppCompatTextHelper_android_textAppearance = 0; + + public static int[] AppCompatTextView = new int[] { + 16842804, + 2130772014, + 2130772015, + 2130772016, + 2130772017, + 2130772018, + 2130772019, + 2130772020, + 2130772021, + 2130772022, + 2130772023}; + + // aapt resource value: 0 + public static int AppCompatTextView_android_textAppearance = 0; + + // aapt resource value: 6 + public static int AppCompatTextView_autoSizeMaxTextSize = 6; + + // aapt resource value: 5 + public static int AppCompatTextView_autoSizeMinTextSize = 5; + + // aapt resource value: 4 + public static int AppCompatTextView_autoSizePresetSizes = 4; + + // aapt resource value: 3 + public static int AppCompatTextView_autoSizeStepGranularity = 3; + + // aapt resource value: 2 + public static int AppCompatTextView_autoSizeTextType = 2; + + // aapt resource value: 9 + public static int AppCompatTextView_firstBaselineToTopHeight = 9; + + // aapt resource value: 7 + public static int AppCompatTextView_fontFamily = 7; + + // aapt resource value: 10 + public static int AppCompatTextView_lastBaselineToBottomHeight = 10; + + // aapt resource value: 8 + public static int AppCompatTextView_lineHeight = 8; + + // aapt resource value: 1 + public static int AppCompatTextView_textAllCaps = 1; + + public static int[] AppCompatTheme = new int[] { + 16842839, + 16842926, + 2130772024, + 2130772025, + 2130772026, + 2130772027, + 2130772028, + 2130772029, + 2130772030, + 2130772031, + 2130772032, + 2130772033, + 2130772034, + 2130772035, + 2130772036, + 2130772037, + 2130772038, + 2130772039, + 2130772040, + 2130772041, + 2130772042, + 2130772043, + 2130772044, + 2130772045, + 2130772046, + 2130772047, + 2130772048, + 2130772049, + 2130772050, + 2130772051, + 2130772052, + 2130772053, + 2130772054, + 2130772055, + 2130772056, + 2130772057, + 2130772058, + 2130772059, + 2130772060, + 2130772061, + 2130772062, + 2130772063, + 2130772064, + 2130772065, + 2130772066, + 2130772067, + 2130772068, + 2130772069, + 2130772070, + 2130772071, + 2130772072, + 2130772073, + 2130772074, + 2130772075, + 2130772076, + 2130772077, + 2130772078, + 2130772079, + 2130772080, + 2130772081, + 2130772082, + 2130772083, + 2130772084, + 2130772085, + 2130772086, + 2130772087, + 2130772088, + 2130772089, + 2130772090, + 2130772091, + 2130772092, + 2130772093, + 2130772094, + 2130772095, + 2130772096, + 2130772097, + 2130772098, + 2130772099, + 2130772100, + 2130772101, + 2130772102, + 2130772103, + 2130772104, + 2130772105, + 2130772106, + 2130772107, + 2130772108, + 2130772109, + 2130772110, + 2130772111, + 2130772112, + 2130772113, + 2130772114, + 2130772115, + 2130772116, + 2130772117, + 2130772118, + 2130772119, + 2130772120, + 2130772121, + 2130772122, + 2130772123, + 2130772124, + 2130772125, + 2130772126, + 2130772127, + 2130772128, + 2130772129, + 2130772130, + 2130772131, + 2130772132, + 2130772133, + 2130772134, + 2130772135, + 2130772136, + 2130772137, + 2130772138, + 2130772139, + 2130772140, + 2130772141, + 2130772142}; + + // aapt resource value: 23 + public static int AppCompatTheme_actionBarDivider = 23; + + // aapt resource value: 24 + public static int AppCompatTheme_actionBarItemBackground = 24; + + // aapt resource value: 17 + public static int AppCompatTheme_actionBarPopupTheme = 17; + + // aapt resource value: 22 + public static int AppCompatTheme_actionBarSize = 22; + + // aapt resource value: 19 + public static int AppCompatTheme_actionBarSplitStyle = 19; + + // aapt resource value: 18 + public static int AppCompatTheme_actionBarStyle = 18; + + // aapt resource value: 13 + public static int AppCompatTheme_actionBarTabBarStyle = 13; + + // aapt resource value: 12 + public static int AppCompatTheme_actionBarTabStyle = 12; + + // aapt resource value: 14 + public static int AppCompatTheme_actionBarTabTextStyle = 14; + + // aapt resource value: 20 + public static int AppCompatTheme_actionBarTheme = 20; + + // aapt resource value: 21 + public static int AppCompatTheme_actionBarWidgetTheme = 21; + + // aapt resource value: 51 + public static int AppCompatTheme_actionButtonStyle = 51; + + // aapt resource value: 47 + public static int AppCompatTheme_actionDropDownStyle = 47; + + // aapt resource value: 25 + public static int AppCompatTheme_actionMenuTextAppearance = 25; + + // aapt resource value: 26 + public static int AppCompatTheme_actionMenuTextColor = 26; + + // aapt resource value: 29 + public static int AppCompatTheme_actionModeBackground = 29; + + // aapt resource value: 28 + public static int AppCompatTheme_actionModeCloseButtonStyle = 28; + + // aapt resource value: 31 + public static int AppCompatTheme_actionModeCloseDrawable = 31; + + // aapt resource value: 33 + public static int AppCompatTheme_actionModeCopyDrawable = 33; + + // aapt resource value: 32 + public static int AppCompatTheme_actionModeCutDrawable = 32; + + // aapt resource value: 37 + public static int AppCompatTheme_actionModeFindDrawable = 37; + + // aapt resource value: 34 + public static int AppCompatTheme_actionModePasteDrawable = 34; + + // aapt resource value: 39 + public static int AppCompatTheme_actionModePopupWindowStyle = 39; + + // aapt resource value: 35 + public static int AppCompatTheme_actionModeSelectAllDrawable = 35; + + // aapt resource value: 36 + public static int AppCompatTheme_actionModeShareDrawable = 36; + + // aapt resource value: 30 + public static int AppCompatTheme_actionModeSplitBackground = 30; + + // aapt resource value: 27 + public static int AppCompatTheme_actionModeStyle = 27; + + // aapt resource value: 38 + public static int AppCompatTheme_actionModeWebSearchDrawable = 38; + + // aapt resource value: 15 + public static int AppCompatTheme_actionOverflowButtonStyle = 15; + + // aapt resource value: 16 + public static int AppCompatTheme_actionOverflowMenuStyle = 16; + + // aapt resource value: 59 + public static int AppCompatTheme_activityChooserViewStyle = 59; + + // aapt resource value: 96 + public static int AppCompatTheme_alertDialogButtonGroupStyle = 96; + + // aapt resource value: 97 + public static int AppCompatTheme_alertDialogCenterButtons = 97; + + // aapt resource value: 95 + public static int AppCompatTheme_alertDialogStyle = 95; + + // aapt resource value: 98 + public static int AppCompatTheme_alertDialogTheme = 98; + + // aapt resource value: 1 + public static int AppCompatTheme_android_windowAnimationStyle = 1; + + // aapt resource value: 0 + public static int AppCompatTheme_android_windowIsFloating = 0; + + // aapt resource value: 103 + public static int AppCompatTheme_autoCompleteTextViewStyle = 103; + + // aapt resource value: 56 + public static int AppCompatTheme_borderlessButtonStyle = 56; + + // aapt resource value: 53 + public static int AppCompatTheme_buttonBarButtonStyle = 53; + + // aapt resource value: 101 + public static int AppCompatTheme_buttonBarNegativeButtonStyle = 101; + + // aapt resource value: 102 + public static int AppCompatTheme_buttonBarNeutralButtonStyle = 102; + + // aapt resource value: 100 + public static int AppCompatTheme_buttonBarPositiveButtonStyle = 100; + + // aapt resource value: 52 + public static int AppCompatTheme_buttonBarStyle = 52; + + // aapt resource value: 104 + public static int AppCompatTheme_buttonStyle = 104; + + // aapt resource value: 105 + public static int AppCompatTheme_buttonStyleSmall = 105; + + // aapt resource value: 106 + public static int AppCompatTheme_checkboxStyle = 106; + + // aapt resource value: 107 + public static int AppCompatTheme_checkedTextViewStyle = 107; + + // aapt resource value: 87 + public static int AppCompatTheme_colorAccent = 87; + + // aapt resource value: 94 + public static int AppCompatTheme_colorBackgroundFloating = 94; + + // aapt resource value: 91 + public static int AppCompatTheme_colorButtonNormal = 91; + + // aapt resource value: 89 + public static int AppCompatTheme_colorControlActivated = 89; + + // aapt resource value: 90 + public static int AppCompatTheme_colorControlHighlight = 90; + + // aapt resource value: 88 + public static int AppCompatTheme_colorControlNormal = 88; + + // aapt resource value: 119 + public static int AppCompatTheme_colorError = 119; + + // aapt resource value: 85 + public static int AppCompatTheme_colorPrimary = 85; + + // aapt resource value: 86 + public static int AppCompatTheme_colorPrimaryDark = 86; + + // aapt resource value: 92 + public static int AppCompatTheme_colorSwitchThumbNormal = 92; + + // aapt resource value: 93 + public static int AppCompatTheme_controlBackground = 93; + + // aapt resource value: 46 + public static int AppCompatTheme_dialogCornerRadius = 46; + + // aapt resource value: 44 + public static int AppCompatTheme_dialogPreferredPadding = 44; + + // aapt resource value: 43 + public static int AppCompatTheme_dialogTheme = 43; + + // aapt resource value: 58 + public static int AppCompatTheme_dividerHorizontal = 58; + + // aapt resource value: 57 + public static int AppCompatTheme_dividerVertical = 57; + + // aapt resource value: 76 + public static int AppCompatTheme_dropDownListViewStyle = 76; + + // aapt resource value: 48 + public static int AppCompatTheme_dropdownListPreferredItemHeight = 48; + + // aapt resource value: 65 + public static int AppCompatTheme_editTextBackground = 65; + + // aapt resource value: 64 + public static int AppCompatTheme_editTextColor = 64; + + // aapt resource value: 108 + public static int AppCompatTheme_editTextStyle = 108; + + // aapt resource value: 50 + public static int AppCompatTheme_homeAsUpIndicator = 50; + + // aapt resource value: 66 + public static int AppCompatTheme_imageButtonStyle = 66; + + // aapt resource value: 84 + public static int AppCompatTheme_listChoiceBackgroundIndicator = 84; + + // aapt resource value: 45 + public static int AppCompatTheme_listDividerAlertDialog = 45; + + // aapt resource value: 116 + public static int AppCompatTheme_listMenuViewStyle = 116; + + // aapt resource value: 77 + public static int AppCompatTheme_listPopupWindowStyle = 77; + + // aapt resource value: 71 + public static int AppCompatTheme_listPreferredItemHeight = 71; + + // aapt resource value: 73 + public static int AppCompatTheme_listPreferredItemHeightLarge = 73; + + // aapt resource value: 72 + public static int AppCompatTheme_listPreferredItemHeightSmall = 72; + + // aapt resource value: 74 + public static int AppCompatTheme_listPreferredItemPaddingLeft = 74; + + // aapt resource value: 75 + public static int AppCompatTheme_listPreferredItemPaddingRight = 75; + + // aapt resource value: 81 + public static int AppCompatTheme_panelBackground = 81; + + // aapt resource value: 83 + public static int AppCompatTheme_panelMenuListTheme = 83; + + // aapt resource value: 82 + public static int AppCompatTheme_panelMenuListWidth = 82; + + // aapt resource value: 62 + public static int AppCompatTheme_popupMenuStyle = 62; + + // aapt resource value: 63 + public static int AppCompatTheme_popupWindowStyle = 63; + + // aapt resource value: 109 + public static int AppCompatTheme_radioButtonStyle = 109; + + // aapt resource value: 110 + public static int AppCompatTheme_ratingBarStyle = 110; + + // aapt resource value: 111 + public static int AppCompatTheme_ratingBarStyleIndicator = 111; + + // aapt resource value: 112 + public static int AppCompatTheme_ratingBarStyleSmall = 112; + + // aapt resource value: 70 + public static int AppCompatTheme_searchViewStyle = 70; + + // aapt resource value: 113 + public static int AppCompatTheme_seekBarStyle = 113; + + // aapt resource value: 54 + public static int AppCompatTheme_selectableItemBackground = 54; + + // aapt resource value: 55 + public static int AppCompatTheme_selectableItemBackgroundBorderless = 55; + + // aapt resource value: 49 + public static int AppCompatTheme_spinnerDropDownItemStyle = 49; + + // aapt resource value: 114 + public static int AppCompatTheme_spinnerStyle = 114; + + // aapt resource value: 115 + public static int AppCompatTheme_switchStyle = 115; + + // aapt resource value: 40 + public static int AppCompatTheme_textAppearanceLargePopupMenu = 40; + + // aapt resource value: 78 + public static int AppCompatTheme_textAppearanceListItem = 78; + + // aapt resource value: 79 + public static int AppCompatTheme_textAppearanceListItemSecondary = 79; + + // aapt resource value: 80 + public static int AppCompatTheme_textAppearanceListItemSmall = 80; + + // aapt resource value: 42 + public static int AppCompatTheme_textAppearancePopupMenuHeader = 42; + + // aapt resource value: 68 + public static int AppCompatTheme_textAppearanceSearchResultSubtitle = 68; + + // aapt resource value: 67 + public static int AppCompatTheme_textAppearanceSearchResultTitle = 67; + + // aapt resource value: 41 + public static int AppCompatTheme_textAppearanceSmallPopupMenu = 41; + + // aapt resource value: 99 + public static int AppCompatTheme_textColorAlertDialogListItem = 99; + + // aapt resource value: 69 + public static int AppCompatTheme_textColorSearchUrl = 69; + + // aapt resource value: 61 + public static int AppCompatTheme_toolbarNavigationButtonStyle = 61; + + // aapt resource value: 60 + public static int AppCompatTheme_toolbarStyle = 60; + + // aapt resource value: 118 + public static int AppCompatTheme_tooltipForegroundColor = 118; + + // aapt resource value: 117 + public static int AppCompatTheme_tooltipFrameBackground = 117; + + // aapt resource value: 120 + public static int AppCompatTheme_viewInflaterClass = 120; + + // aapt resource value: 2 + public static int AppCompatTheme_windowActionBar = 2; + + // aapt resource value: 4 + public static int AppCompatTheme_windowActionBarOverlay = 4; + + // aapt resource value: 5 + public static int AppCompatTheme_windowActionModeOverlay = 5; + + // aapt resource value: 9 + public static int AppCompatTheme_windowFixedHeightMajor = 9; + + // aapt resource value: 7 + public static int AppCompatTheme_windowFixedHeightMinor = 7; + + // aapt resource value: 6 + public static int AppCompatTheme_windowFixedWidthMajor = 6; + + // aapt resource value: 8 + public static int AppCompatTheme_windowFixedWidthMinor = 8; + + // aapt resource value: 10 + public static int AppCompatTheme_windowMinWidthMajor = 10; + + // aapt resource value: 11 + public static int AppCompatTheme_windowMinWidthMinor = 11; + + // aapt resource value: 3 + public static int AppCompatTheme_windowNoTitle = 3; + + public static int[] ButtonBarLayout = new int[] { + 2130772143}; + + // aapt resource value: 0 + public static int ButtonBarLayout_allowStacking = 0; + + public static int[] ColorStateListItem = new int[] { + 16843173, + 16843551, + 2130772228}; + + // aapt resource value: 2 + public static int ColorStateListItem_alpha = 2; + + // aapt resource value: 1 + public static int ColorStateListItem_android_alpha = 1; + + // aapt resource value: 0 + public static int ColorStateListItem_android_color = 0; + + public static int[] CompoundButton = new int[] { + 16843015, + 2130772144, + 2130772145}; + + // aapt resource value: 0 + public static int CompoundButton_android_button = 0; + + // aapt resource value: 1 + public static int CompoundButton_buttonTint = 1; + + // aapt resource value: 2 + public static int CompoundButton_buttonTintMode = 2; + + public static int[] CoordinatorLayout = new int[] { + 2130772220, + 2130772221}; + + // aapt resource value: 0 + public static int CoordinatorLayout_keylines = 0; + + // aapt resource value: 1 + public static int CoordinatorLayout_statusBarBackground = 1; + + public static int[] CoordinatorLayout_Layout = new int[] { + 16842931, + 2130772222, + 2130772223, + 2130772224, + 2130772225, + 2130772226, + 2130772227}; + + // aapt resource value: 0 + public static int CoordinatorLayout_Layout_android_layout_gravity = 0; + + // aapt resource value: 2 + public static int CoordinatorLayout_Layout_layout_anchor = 2; + + // aapt resource value: 4 + public static int CoordinatorLayout_Layout_layout_anchorGravity = 4; + + // aapt resource value: 1 + public static int CoordinatorLayout_Layout_layout_behavior = 1; + + // aapt resource value: 6 + public static int CoordinatorLayout_Layout_layout_dodgeInsetEdges = 6; + + // aapt resource value: 5 + public static int CoordinatorLayout_Layout_layout_insetEdge = 5; + + // aapt resource value: 3 + public static int CoordinatorLayout_Layout_layout_keyline = 3; + + public static int[] DrawerArrowToggle = new int[] { + 2130772146, + 2130772147, + 2130772148, + 2130772149, + 2130772150, + 2130772151, + 2130772152, + 2130772153}; + + // aapt resource value: 4 + public static int DrawerArrowToggle_arrowHeadLength = 4; + + // aapt resource value: 5 + public static int DrawerArrowToggle_arrowShaftLength = 5; + + // aapt resource value: 6 + public static int DrawerArrowToggle_barLength = 6; + + // aapt resource value: 0 + public static int DrawerArrowToggle_color = 0; + + // aapt resource value: 2 + public static int DrawerArrowToggle_drawableSize = 2; + + // aapt resource value: 3 + public static int DrawerArrowToggle_gapBetweenBars = 3; + + // aapt resource value: 1 + public static int DrawerArrowToggle_spinBars = 1; + + // aapt resource value: 7 + public static int DrawerArrowToggle_thickness = 7; + + public static int[] FontFamily = new int[] { + 2130772229, + 2130772230, + 2130772231, + 2130772232, + 2130772233, + 2130772234}; + + // aapt resource value: 0 + public static int FontFamily_fontProviderAuthority = 0; + + // aapt resource value: 3 + public static int FontFamily_fontProviderCerts = 3; + + // aapt resource value: 4 + public static int FontFamily_fontProviderFetchStrategy = 4; + + // aapt resource value: 5 + public static int FontFamily_fontProviderFetchTimeout = 5; + + // aapt resource value: 1 + public static int FontFamily_fontProviderPackage = 1; + + // aapt resource value: 2 + public static int FontFamily_fontProviderQuery = 2; + + public static int[] FontFamilyFont = new int[] { + 16844082, + 16844083, + 16844095, + 16844143, + 16844144, + 2130772235, + 2130772236, + 2130772237, + 2130772238, + 2130772239}; + + // aapt resource value: 0 + public static int FontFamilyFont_android_font = 0; + + // aapt resource value: 2 + public static int FontFamilyFont_android_fontStyle = 2; + + // aapt resource value: 4 + public static int FontFamilyFont_android_fontVariationSettings = 4; + + // aapt resource value: 1 + public static int FontFamilyFont_android_fontWeight = 1; + + // aapt resource value: 3 + public static int FontFamilyFont_android_ttcIndex = 3; + + // aapt resource value: 6 + public static int FontFamilyFont_font = 6; + + // aapt resource value: 5 + public static int FontFamilyFont_fontStyle = 5; + + // aapt resource value: 8 + public static int FontFamilyFont_fontVariationSettings = 8; + + // aapt resource value: 7 + public static int FontFamilyFont_fontWeight = 7; + + // aapt resource value: 9 + public static int FontFamilyFont_ttcIndex = 9; + + public static int[] GradientColor = new int[] { + 16843165, + 16843166, + 16843169, + 16843170, + 16843171, + 16843172, + 16843265, + 16843275, + 16844048, + 16844049, + 16844050, + 16844051}; + + // aapt resource value: 7 + public static int GradientColor_android_centerColor = 7; + + // aapt resource value: 3 + public static int GradientColor_android_centerX = 3; + + // aapt resource value: 4 + public static int GradientColor_android_centerY = 4; + + // aapt resource value: 1 + public static int GradientColor_android_endColor = 1; + + // aapt resource value: 10 + public static int GradientColor_android_endX = 10; + + // aapt resource value: 11 + public static int GradientColor_android_endY = 11; + + // aapt resource value: 5 + public static int GradientColor_android_gradientRadius = 5; + + // aapt resource value: 0 + public static int GradientColor_android_startColor = 0; + + // aapt resource value: 8 + public static int GradientColor_android_startX = 8; + + // aapt resource value: 9 + public static int GradientColor_android_startY = 9; + + // aapt resource value: 6 + public static int GradientColor_android_tileMode = 6; + + // aapt resource value: 2 + public static int GradientColor_android_type = 2; + + public static int[] GradientColorItem = new int[] { + 16843173, + 16844052}; + + // aapt resource value: 0 + public static int GradientColorItem_android_color = 0; + + // aapt resource value: 1 + public static int GradientColorItem_android_offset = 1; + + public static int[] LinearLayoutCompat = new int[] { + 16842927, + 16842948, + 16843046, + 16843047, + 16843048, + 2130771979, + 2130772154, + 2130772155, + 2130772156}; + + // aapt resource value: 2 + public static int LinearLayoutCompat_android_baselineAligned = 2; + + // aapt resource value: 3 + public static int LinearLayoutCompat_android_baselineAlignedChildIndex = 3; + + // aapt resource value: 0 + public static int LinearLayoutCompat_android_gravity = 0; + + // aapt resource value: 1 + public static int LinearLayoutCompat_android_orientation = 1; + + // aapt resource value: 4 + public static int LinearLayoutCompat_android_weightSum = 4; + + // aapt resource value: 5 + public static int LinearLayoutCompat_divider = 5; + + // aapt resource value: 8 + public static int LinearLayoutCompat_dividerPadding = 8; + + // aapt resource value: 6 + public static int LinearLayoutCompat_measureWithLargestChild = 6; + + // aapt resource value: 7 + public static int LinearLayoutCompat_showDividers = 7; + + public static int[] LinearLayoutCompat_Layout = new int[] { + 16842931, + 16842996, + 16842997, + 16843137}; + + // aapt resource value: 0 + public static int LinearLayoutCompat_Layout_android_layout_gravity = 0; + + // aapt resource value: 2 + public static int LinearLayoutCompat_Layout_android_layout_height = 2; + + // aapt resource value: 3 + public static int LinearLayoutCompat_Layout_android_layout_weight = 3; + + // aapt resource value: 1 + public static int LinearLayoutCompat_Layout_android_layout_width = 1; + + public static int[] ListPopupWindow = new int[] { + 16843436, + 16843437}; + + // aapt resource value: 0 + public static int ListPopupWindow_android_dropDownHorizontalOffset = 0; + + // aapt resource value: 1 + public static int ListPopupWindow_android_dropDownVerticalOffset = 1; + + public static int[] MenuGroup = new int[] { + 16842766, + 16842960, + 16843156, + 16843230, + 16843231, + 16843232}; + + // aapt resource value: 5 + public static int MenuGroup_android_checkableBehavior = 5; + + // aapt resource value: 0 + public static int MenuGroup_android_enabled = 0; + + // aapt resource value: 1 + public static int MenuGroup_android_id = 1; + + // aapt resource value: 3 + public static int MenuGroup_android_menuCategory = 3; + + // aapt resource value: 4 + public static int MenuGroup_android_orderInCategory = 4; + + // aapt resource value: 2 + public static int MenuGroup_android_visible = 2; + + public static int[] MenuItem = new int[] { + 16842754, + 16842766, + 16842960, + 16843014, + 16843156, + 16843230, + 16843231, + 16843233, + 16843234, + 16843235, + 16843236, + 16843237, + 16843375, + 2130772157, + 2130772158, + 2130772159, + 2130772160, + 2130772161, + 2130772162, + 2130772163, + 2130772164, + 2130772165, + 2130772166}; + + // aapt resource value: 16 + public static int MenuItem_actionLayout = 16; + + // aapt resource value: 18 + public static int MenuItem_actionProviderClass = 18; + + // aapt resource value: 17 + public static int MenuItem_actionViewClass = 17; + + // aapt resource value: 13 + public static int MenuItem_alphabeticModifiers = 13; + + // aapt resource value: 9 + public static int MenuItem_android_alphabeticShortcut = 9; + + // aapt resource value: 11 + public static int MenuItem_android_checkable = 11; + + // aapt resource value: 3 + public static int MenuItem_android_checked = 3; + + // aapt resource value: 1 + public static int MenuItem_android_enabled = 1; + + // aapt resource value: 0 + public static int MenuItem_android_icon = 0; + + // aapt resource value: 2 + public static int MenuItem_android_id = 2; + + // aapt resource value: 5 + public static int MenuItem_android_menuCategory = 5; + + // aapt resource value: 10 + public static int MenuItem_android_numericShortcut = 10; + + // aapt resource value: 12 + public static int MenuItem_android_onClick = 12; + + // aapt resource value: 6 + public static int MenuItem_android_orderInCategory = 6; + + // aapt resource value: 7 + public static int MenuItem_android_title = 7; + + // aapt resource value: 8 + public static int MenuItem_android_titleCondensed = 8; + + // aapt resource value: 4 + public static int MenuItem_android_visible = 4; + + // aapt resource value: 19 + public static int MenuItem_contentDescription = 19; + + // aapt resource value: 21 + public static int MenuItem_iconTint = 21; + + // aapt resource value: 22 + public static int MenuItem_iconTintMode = 22; + + // aapt resource value: 14 + public static int MenuItem_numericModifiers = 14; + + // aapt resource value: 15 + public static int MenuItem_showAsAction = 15; + + // aapt resource value: 20 + public static int MenuItem_tooltipText = 20; + + public static int[] MenuView = new int[] { + 16842926, + 16843052, + 16843053, + 16843054, + 16843055, + 16843056, + 16843057, + 2130772167, + 2130772168}; + + // aapt resource value: 4 + public static int MenuView_android_headerBackground = 4; + + // aapt resource value: 2 + public static int MenuView_android_horizontalDivider = 2; + + // aapt resource value: 5 + public static int MenuView_android_itemBackground = 5; + + // aapt resource value: 6 + public static int MenuView_android_itemIconDisabledAlpha = 6; + + // aapt resource value: 1 + public static int MenuView_android_itemTextAppearance = 1; + + // aapt resource value: 3 + public static int MenuView_android_verticalDivider = 3; + + // aapt resource value: 0 + public static int MenuView_android_windowAnimationStyle = 0; + + // aapt resource value: 7 + public static int MenuView_preserveIconSpacing = 7; + + // aapt resource value: 8 + public static int MenuView_subMenuArrow = 8; + + public static int[] PopupWindow = new int[] { + 16843126, + 16843465, + 2130772169}; + + // aapt resource value: 1 + public static int PopupWindow_android_popupAnimationStyle = 1; + + // aapt resource value: 0 + public static int PopupWindow_android_popupBackground = 0; + + // aapt resource value: 2 + public static int PopupWindow_overlapAnchor = 2; + + public static int[] PopupWindowBackgroundState = new int[] { + 2130772170}; + + // aapt resource value: 0 + public static int PopupWindowBackgroundState_state_above_anchor = 0; + + public static int[] RecycleListView = new int[] { + 2130772171, + 2130772172}; + + // aapt resource value: 0 + public static int RecycleListView_paddingBottomNoButtons = 0; + + // aapt resource value: 1 + public static int RecycleListView_paddingTopNoTitle = 1; + + public static int[] SearchView = new int[] { + 16842970, + 16843039, + 16843296, + 16843364, + 2130772173, + 2130772174, + 2130772175, + 2130772176, + 2130772177, + 2130772178, + 2130772179, + 2130772180, + 2130772181, + 2130772182, + 2130772183, + 2130772184, + 2130772185}; + + // aapt resource value: 0 + public static int SearchView_android_focusable = 0; + + // aapt resource value: 3 + public static int SearchView_android_imeOptions = 3; + + // aapt resource value: 2 + public static int SearchView_android_inputType = 2; + + // aapt resource value: 1 + public static int SearchView_android_maxWidth = 1; + + // aapt resource value: 8 + public static int SearchView_closeIcon = 8; + + // aapt resource value: 13 + public static int SearchView_commitIcon = 13; + + // aapt resource value: 7 + public static int SearchView_defaultQueryHint = 7; + + // aapt resource value: 9 + public static int SearchView_goIcon = 9; + + // aapt resource value: 5 + public static int SearchView_iconifiedByDefault = 5; + + // aapt resource value: 4 + public static int SearchView_layout = 4; + + // aapt resource value: 15 + public static int SearchView_queryBackground = 15; + + // aapt resource value: 6 + public static int SearchView_queryHint = 6; + + // aapt resource value: 11 + public static int SearchView_searchHintIcon = 11; + + // aapt resource value: 10 + public static int SearchView_searchIcon = 10; + + // aapt resource value: 16 + public static int SearchView_submitBackground = 16; + + // aapt resource value: 14 + public static int SearchView_suggestionRowLayout = 14; + + // aapt resource value: 12 + public static int SearchView_voiceIcon = 12; + + public static int[] Spinner = new int[] { + 16842930, + 16843126, + 16843131, + 16843362, + 2130771997}; + + // aapt resource value: 3 + public static int Spinner_android_dropDownWidth = 3; + + // aapt resource value: 0 + public static int Spinner_android_entries = 0; + + // aapt resource value: 1 + public static int Spinner_android_popupBackground = 1; + + // aapt resource value: 2 + public static int Spinner_android_prompt = 2; + + // aapt resource value: 4 + public static int Spinner_popupTheme = 4; + + public static int[] StateListDrawable = new int[] { + 16843036, + 16843156, + 16843157, + 16843158, + 16843532, + 16843533}; + + // aapt resource value: 3 + public static int StateListDrawable_android_constantSize = 3; + + // aapt resource value: 0 + public static int StateListDrawable_android_dither = 0; + + // aapt resource value: 4 + public static int StateListDrawable_android_enterFadeDuration = 4; + + // aapt resource value: 5 + public static int StateListDrawable_android_exitFadeDuration = 5; + + // aapt resource value: 2 + public static int StateListDrawable_android_variablePadding = 2; + + // aapt resource value: 1 + public static int StateListDrawable_android_visible = 1; + + public static int[] StateListDrawableItem = new int[] { + 16843161}; + + // aapt resource value: 0 + public static int StateListDrawableItem_android_drawable = 0; + + public static int[] SwitchCompat = new int[] { + 16843044, + 16843045, + 16843074, + 2130772186, + 2130772187, + 2130772188, + 2130772189, + 2130772190, + 2130772191, + 2130772192, + 2130772193, + 2130772194, + 2130772195, + 2130772196}; + + // aapt resource value: 1 + public static int SwitchCompat_android_textOff = 1; + + // aapt resource value: 0 + public static int SwitchCompat_android_textOn = 0; + + // aapt resource value: 2 + public static int SwitchCompat_android_thumb = 2; + + // aapt resource value: 13 + public static int SwitchCompat_showText = 13; + + // aapt resource value: 12 + public static int SwitchCompat_splitTrack = 12; + + // aapt resource value: 10 + public static int SwitchCompat_switchMinWidth = 10; + + // aapt resource value: 11 + public static int SwitchCompat_switchPadding = 11; + + // aapt resource value: 9 + public static int SwitchCompat_switchTextAppearance = 9; + + // aapt resource value: 8 + public static int SwitchCompat_thumbTextPadding = 8; + + // aapt resource value: 3 + public static int SwitchCompat_thumbTint = 3; + + // aapt resource value: 4 + public static int SwitchCompat_thumbTintMode = 4; + + // aapt resource value: 5 + public static int SwitchCompat_track = 5; + + // aapt resource value: 6 + public static int SwitchCompat_trackTint = 6; + + // aapt resource value: 7 + public static int SwitchCompat_trackTintMode = 7; + + public static int[] TextAppearance = new int[] { + 16842901, + 16842902, + 16842903, + 16842904, + 16842906, + 16842907, + 16843105, + 16843106, + 16843107, + 16843108, + 16843692, + 2130772014, + 2130772020}; + + // aapt resource value: 10 + public static int TextAppearance_android_fontFamily = 10; + + // aapt resource value: 6 + public static int TextAppearance_android_shadowColor = 6; + + // aapt resource value: 7 + public static int TextAppearance_android_shadowDx = 7; + + // aapt resource value: 8 + public static int TextAppearance_android_shadowDy = 8; + + // aapt resource value: 9 + public static int TextAppearance_android_shadowRadius = 9; + + // aapt resource value: 3 + public static int TextAppearance_android_textColor = 3; + + // aapt resource value: 4 + public static int TextAppearance_android_textColorHint = 4; + + // aapt resource value: 5 + public static int TextAppearance_android_textColorLink = 5; + + // aapt resource value: 0 + public static int TextAppearance_android_textSize = 0; + + // aapt resource value: 2 + public static int TextAppearance_android_textStyle = 2; + + // aapt resource value: 1 + public static int TextAppearance_android_typeface = 1; + + // aapt resource value: 12 + public static int TextAppearance_fontFamily = 12; + + // aapt resource value: 11 + public static int TextAppearance_textAllCaps = 11; + + public static int[] Toolbar = new int[] { + 16842927, + 16843072, + 2130771971, + 2130771974, + 2130771978, + 2130771990, + 2130771991, + 2130771992, + 2130771993, + 2130771994, + 2130771995, + 2130771997, + 2130772197, + 2130772198, + 2130772199, + 2130772200, + 2130772201, + 2130772202, + 2130772203, + 2130772204, + 2130772205, + 2130772206, + 2130772207, + 2130772208, + 2130772209, + 2130772210, + 2130772211, + 2130772212, + 2130772213}; + + // aapt resource value: 0 + public static int Toolbar_android_gravity = 0; + + // aapt resource value: 1 + public static int Toolbar_android_minHeight = 1; + + // aapt resource value: 21 + public static int Toolbar_buttonGravity = 21; + + // aapt resource value: 23 + public static int Toolbar_collapseContentDescription = 23; + + // aapt resource value: 22 + public static int Toolbar_collapseIcon = 22; + + // aapt resource value: 6 + public static int Toolbar_contentInsetEnd = 6; + + // aapt resource value: 10 + public static int Toolbar_contentInsetEndWithActions = 10; + + // aapt resource value: 7 + public static int Toolbar_contentInsetLeft = 7; + + // aapt resource value: 8 + public static int Toolbar_contentInsetRight = 8; + + // aapt resource value: 5 + public static int Toolbar_contentInsetStart = 5; + + // aapt resource value: 9 + public static int Toolbar_contentInsetStartWithNavigation = 9; + + // aapt resource value: 4 + public static int Toolbar_logo = 4; + + // aapt resource value: 26 + public static int Toolbar_logoDescription = 26; + + // aapt resource value: 20 + public static int Toolbar_maxButtonHeight = 20; + + // aapt resource value: 25 + public static int Toolbar_navigationContentDescription = 25; + + // aapt resource value: 24 + public static int Toolbar_navigationIcon = 24; + + // aapt resource value: 11 + public static int Toolbar_popupTheme = 11; + + // aapt resource value: 3 + public static int Toolbar_subtitle = 3; + + // aapt resource value: 13 + public static int Toolbar_subtitleTextAppearance = 13; + + // aapt resource value: 28 + public static int Toolbar_subtitleTextColor = 28; + + // aapt resource value: 2 + public static int Toolbar_title = 2; + + // aapt resource value: 14 + public static int Toolbar_titleMargin = 14; + + // aapt resource value: 18 + public static int Toolbar_titleMarginBottom = 18; + + // aapt resource value: 16 + public static int Toolbar_titleMarginEnd = 16; + + // aapt resource value: 15 + public static int Toolbar_titleMarginStart = 15; + + // aapt resource value: 17 + public static int Toolbar_titleMarginTop = 17; + + // aapt resource value: 19 + public static int Toolbar_titleMargins = 19; + + // aapt resource value: 12 + public static int Toolbar_titleTextAppearance = 12; + + // aapt resource value: 27 + public static int Toolbar_titleTextColor = 27; + + public static int[] View = new int[] { + 16842752, + 16842970, + 2130772214, + 2130772215, + 2130772216}; + + // aapt resource value: 1 + public static int View_android_focusable = 1; + + // aapt resource value: 0 + public static int View_android_theme = 0; + + // aapt resource value: 3 + public static int View_paddingEnd = 3; + + // aapt resource value: 2 + public static int View_paddingStart = 2; + + // aapt resource value: 4 + public static int View_theme = 4; + + public static int[] ViewBackgroundHelper = new int[] { + 16842964, + 2130772217, + 2130772218}; + + // aapt resource value: 0 + public static int ViewBackgroundHelper_android_background = 0; + + // aapt resource value: 1 + public static int ViewBackgroundHelper_backgroundTint = 1; + + // aapt resource value: 2 + public static int ViewBackgroundHelper_backgroundTintMode = 2; + + public static int[] ViewStubCompat = new int[] { + 16842960, + 16842994, + 16842995}; + + // aapt resource value: 0 + public static int ViewStubCompat_android_id = 0; + + // aapt resource value: 2 + public static int ViewStubCompat_android_inflatedId = 2; + + // aapt resource value: 1 + public static int ViewStubCompat_android_layout = 1; + + static Styleable() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Styleable() + { + } + } + } +} +#pragma warning restore 1591 diff --git a/src/StardewModdingAPI/Resources/values/strings.xml b/src/StardewModdingAPI/Resources/values/strings.xml new file mode 100644 index 00000000..94aa8610 --- /dev/null +++ b/src/StardewModdingAPI/Resources/values/strings.xml @@ -0,0 +1,4 @@ + + Hello World, Click Me! + StardewModdingAPI + diff --git a/src/StardewModdingAPI/SButton.cs b/src/StardewModdingAPI/SButton.cs new file mode 100644 index 00000000..bd1fcfdd --- /dev/null +++ b/src/StardewModdingAPI/SButton.cs @@ -0,0 +1,703 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework.Input; +using StardewValley; + +namespace StardewModdingAPI +{ + /// A unified button constant which includes all controller, keyboard, and mouse buttons. + /// Derived from , , and . + public enum SButton + { + /// No valid key. + None = 0, + + /********* + ** Mouse + *********/ + /// The left mouse button. + MouseLeft = 1000, + + /// The right mouse button. + MouseRight = 1001, + + /// The middle mouse button. + MouseMiddle = 1002, + + /// The first mouse XButton. + MouseX1 = 1003, + + /// The second mouse XButton. + MouseX2 = 1004, + + /********* + ** Controller + *********/ + /// The 'A' button on a controller. + ControllerA = SButtonExtensions.ControllerOffset + Buttons.A, + + /// The 'B' button on a controller. + ControllerB = SButtonExtensions.ControllerOffset + Buttons.B, + + /// The 'X' button on a controller. + ControllerX = SButtonExtensions.ControllerOffset + Buttons.X, + + /// The 'Y' button on a controller. + ControllerY = SButtonExtensions.ControllerOffset + Buttons.Y, + + /// The back button on a controller. + ControllerBack = SButtonExtensions.ControllerOffset + Buttons.Back, + + /// The start button on a controller. + ControllerStart = SButtonExtensions.ControllerOffset + Buttons.Start, + + /// The up button on the directional pad of a controller. + DPadUp = SButtonExtensions.ControllerOffset + Buttons.DPadUp, + + /// The down button on the directional pad of a controller. + DPadDown = SButtonExtensions.ControllerOffset + Buttons.DPadDown, + + /// The left button on the directional pad of a controller. + DPadLeft = SButtonExtensions.ControllerOffset + Buttons.DPadLeft, + + /// The right button on the directional pad of a controller. + DPadRight = SButtonExtensions.ControllerOffset + Buttons.DPadRight, + + /// The left bumper (shoulder) button on a controller. + LeftShoulder = SButtonExtensions.ControllerOffset + Buttons.LeftShoulder, + + /// The right bumper (shoulder) button on a controller. + RightShoulder = SButtonExtensions.ControllerOffset + Buttons.RightShoulder, + + /// The left trigger on a controller. + LeftTrigger = SButtonExtensions.ControllerOffset + Buttons.LeftTrigger, + + /// The right trigger on a controller. + RightTrigger = SButtonExtensions.ControllerOffset + Buttons.RightTrigger, + + /// The left analog stick on a controller (when pressed). + LeftStick = SButtonExtensions.ControllerOffset + Buttons.LeftStick, + + /// The right analog stick on a controller (when pressed). + RightStick = SButtonExtensions.ControllerOffset + Buttons.RightStick, + + /// The 'big button' on a controller. + BigButton = SButtonExtensions.ControllerOffset + Buttons.BigButton, + + /// The left analog stick on a controller (when pushed left). + LeftThumbstickLeft = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickLeft, + + /// The left analog stick on a controller (when pushed right). + LeftThumbstickRight = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickRight, + + /// The left analog stick on a controller (when pushed down). + LeftThumbstickDown = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickDown, + + /// The left analog stick on a controller (when pushed up). + LeftThumbstickUp = SButtonExtensions.ControllerOffset + Buttons.LeftThumbstickUp, + + /// The right analog stick on a controller (when pushed left). + RightThumbstickLeft = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickLeft, + + /// The right analog stick on a controller (when pushed right). + RightThumbstickRight = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickRight, + + /// The right analog stick on a controller (when pushed down). + RightThumbstickDown = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickDown, + + /// The right analog stick on a controller (when pushed up). + RightThumbstickUp = SButtonExtensions.ControllerOffset + Buttons.RightThumbstickUp, + + /********* + ** Keyboard + *********/ + /// The A button on a keyboard. + A = Keys.A, + + /// The Add button on a keyboard. + Add = Keys.Add, + + /// The Applications button on a keyboard. + Apps = Keys.Apps, + + /// The Attn button on a keyboard. + Attn = Keys.Attn, + + /// The B button on a keyboard. + B = Keys.B, + + /// The Backspace button on a keyboard. + Back = Keys.Back, + + /// The Browser Back button on a keyboard in Windows 2000/XP. + BrowserBack = Keys.BrowserBack, + + /// The Browser Favorites button on a keyboard in Windows 2000/XP. + BrowserFavorites = Keys.BrowserFavorites, + + /// The Browser Favorites button on a keyboard in Windows 2000/XP. + BrowserForward = Keys.BrowserForward, + + /// The Browser Home button on a keyboard in Windows 2000/XP. + BrowserHome = Keys.BrowserHome, + + /// The Browser Refresh button on a keyboard in Windows 2000/XP. + BrowserRefresh = Keys.BrowserRefresh, + + /// The Browser Search button on a keyboard in Windows 2000/XP. + BrowserSearch = Keys.BrowserSearch, + + /// The Browser Stop button on a keyboard in Windows 2000/XP. + BrowserStop = Keys.BrowserStop, + + /// The C button on a keyboard. + C = Keys.C, + + /// The Caps Lock button on a keyboard. + CapsLock = Keys.CapsLock, + + /// The Green ChatPad button on a keyboard. + ChatPadGreen = Keys.ChatPadGreen, + + /// The Orange ChatPad button on a keyboard. + ChatPadOrange = Keys.ChatPadOrange, + + /// The CrSel button on a keyboard. + Crsel = Keys.Crsel, + + /// The D button on a keyboard. + D = Keys.D, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D0 = Keys.D0, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D1 = Keys.D1, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D2 = Keys.D2, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D3 = Keys.D3, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D4 = Keys.D4, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D5 = Keys.D5, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D6 = Keys.D6, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D7 = Keys.D7, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D8 = Keys.D8, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + D9 = Keys.D9, + + /// The Decimal button on a keyboard. + Decimal = Keys.Decimal, + + /// The Delete button on a keyboard. + Delete = Keys.Delete, + + /// The Divide button on a keyboard. + Divide = Keys.Divide, + + /// The Down arrow button on a keyboard. + Down = Keys.Down, + + /// The E button on a keyboard. + E = Keys.E, + + /// The End button on a keyboard. + End = Keys.End, + + /// The Enter button on a keyboard. + Enter = Keys.Enter, + + /// The Erase EOF button on a keyboard. + EraseEof = Keys.EraseEof, + + /// The Escape button on a keyboard. + Escape = Keys.Escape, + + /// The Execute button on a keyboard. + Execute = Keys.Execute, + + /// The ExSel button on a keyboard. + Exsel = Keys.Exsel, + + /// The F button on a keyboard. + F = Keys.F, + + /// The F1 button on a keyboard. + F1 = Keys.F1, + + /// The F10 button on a keyboard. + F10 = Keys.F10, + + /// The F11 button on a keyboard. + F11 = Keys.F11, + + /// The F12 button on a keyboard. + F12 = Keys.F12, + + /// The F13 button on a keyboard. + F13 = Keys.F13, + + /// The F14 button on a keyboard. + F14 = Keys.F14, + + /// The F15 button on a keyboard. + F15 = Keys.F15, + + /// The F16 button on a keyboard. + F16 = Keys.F16, + + /// The F17 button on a keyboard. + F17 = Keys.F17, + + /// The F18 button on a keyboard. + F18 = Keys.F18, + + /// The F19 button on a keyboard. + F19 = Keys.F19, + + /// The F2 button on a keyboard. + F2 = Keys.F2, + + /// The F20 button on a keyboard. + F20 = Keys.F20, + + /// The F21 button on a keyboard. + F21 = Keys.F21, + + /// The F22 button on a keyboard. + F22 = Keys.F22, + + /// The F23 button on a keyboard. + F23 = Keys.F23, + + /// The F24 button on a keyboard. + F24 = Keys.F24, + + /// The F3 button on a keyboard. + F3 = Keys.F3, + + /// The F4 button on a keyboard. + F4 = Keys.F4, + + /// The F5 button on a keyboard. + F5 = Keys.F5, + + /// The F6 button on a keyboard. + F6 = Keys.F6, + + /// The F7 button on a keyboard. + F7 = Keys.F7, + + /// The F8 button on a keyboard. + F8 = Keys.F8, + + /// The F9 button on a keyboard. + F9 = Keys.F9, + + /// The G button on a keyboard. + G = Keys.G, + + /// The H button on a keyboard. + H = Keys.H, + + /// The Help button on a keyboard. + Help = Keys.Help, + + /// The Home button on a keyboard. + Home = Keys.Home, + + /// The I button on a keyboard. + I = Keys.I, + + /// The IME Convert button on a keyboard. + ImeConvert = Keys.ImeConvert, + + /// The IME NoConvert button on a keyboard. + ImeNoConvert = Keys.ImeNoConvert, + + /// The INS button on a keyboard. + Insert = Keys.Insert, + + /// The J button on a keyboard. + J = Keys.J, + + /// The K button on a keyboard. + K = Keys.K, + + /// The Kana button on a Japanese keyboard. + Kana = Keys.Kana, + + /// The Kanji button on a Japanese keyboard. + Kanji = Keys.Kanji, + + /// The L button on a keyboard. + L = Keys.L, + + /// The Start Applications 1 button on a keyboard in Windows 2000/XP. + LaunchApplication1 = Keys.LaunchApplication1, + + /// The Start Applications 2 button on a keyboard in Windows 2000/XP. + LaunchApplication2 = Keys.LaunchApplication2, + + /// The Start Mail button on a keyboard in Windows 2000/XP. + LaunchMail = Keys.LaunchMail, + + /// The Left arrow button on a keyboard. + Left = Keys.Left, + + /// The Left Alt button on a keyboard. + LeftAlt = Keys.LeftAlt, + + /// The Left Control button on a keyboard. + LeftControl = Keys.LeftControl, + + /// The Left Shift button on a keyboard. + LeftShift = Keys.LeftShift, + + /// The Left Windows button on a keyboard. + LeftWindows = Keys.LeftWindows, + + /// The M button on a keyboard. + M = Keys.M, + + /// The MediaNextTrack button on a keyboard in Windows 2000/XP. + MediaNextTrack = Keys.MediaNextTrack, + + /// The MediaPlayPause button on a keyboard in Windows 2000/XP. + MediaPlayPause = Keys.MediaPlayPause, + + /// The MediaPreviousTrack button on a keyboard in Windows 2000/XP. + MediaPreviousTrack = Keys.MediaPreviousTrack, + + /// The MediaStop button on a keyboard in Windows 2000/XP. + MediaStop = Keys.MediaStop, + + /// The Multiply button on a keyboard. + Multiply = Keys.Multiply, + + /// The N button on a keyboard. + N = Keys.N, + + /// The Num Lock button on a keyboard. + NumLock = Keys.NumLock, + + /// The Numeric keypad 0 button on a keyboard. + NumPad0 = Keys.NumPad0, + + /// The Numeric keypad 1 button on a keyboard. + NumPad1 = Keys.NumPad1, + + /// The Numeric keypad 2 button on a keyboard. + NumPad2 = Keys.NumPad2, + + /// The Numeric keypad 3 button on a keyboard. + NumPad3 = Keys.NumPad3, + + /// The Numeric keypad 4 button on a keyboard. + NumPad4 = Keys.NumPad4, + + /// The Numeric keypad 5 button on a keyboard. + NumPad5 = Keys.NumPad5, + + /// The Numeric keypad 6 button on a keyboard. + NumPad6 = Keys.NumPad6, + + /// The Numeric keypad 7 button on a keyboard. + NumPad7 = Keys.NumPad7, + + /// The Numeric keypad 8 button on a keyboard. + NumPad8 = Keys.NumPad8, + + /// The Numeric keypad 9 button on a keyboard. + NumPad9 = Keys.NumPad9, + + /// The O button on a keyboard. + O = Keys.O, + + /// A miscellaneous button on a keyboard; can vary by keyboard. + Oem8 = Keys.Oem8, + + /// The OEM Auto button on a keyboard. + OemAuto = Keys.OemAuto, + + /// The OEM Angle Bracket or Backslash button on the RT 102 keyboard in Windows 2000/XP. + OemBackslash = Keys.OemBackslash, + + /// The Clear button on a keyboard. + OemClear = Keys.OemClear, + + /// The OEM Close Bracket button on a US standard keyboard in Windows 2000/XP. + OemCloseBrackets = Keys.OemCloseBrackets, + + /// The ',' button on a keyboard in any country/region in Windows 2000/XP. + OemComma = Keys.OemComma, + + /// The OEM Copy button on a keyboard. + OemCopy = Keys.OemCopy, + + /// The OEM Enlarge Window button on a keyboard. + OemEnlW = Keys.OemEnlW, + + /// The '-' button on a keyboard in any country/region in Windows 2000/XP. + OemMinus = Keys.OemMinus, + + /// The OEM Open Bracket button on a US standard keyboard in Windows 2000/XP. + OemOpenBrackets = Keys.OemOpenBrackets, + + /// The '.' button on a keyboard in any country/region. + OemPeriod = Keys.OemPeriod, + + /// The OEM Pipe button on a US standard keyboard. + OemPipe = Keys.OemPipe, + + /// The '+' button on a keyboard in Windows 2000/XP. + OemPlus = Keys.OemPlus, + + /// The OEM Question Mark button on a US standard keyboard. + OemQuestion = Keys.OemQuestion, + + /// The OEM Single/Double Quote button on a US standard keyboard. + OemQuotes = Keys.OemQuotes, + + /// The OEM Semicolon button on a US standard keyboard. + OemSemicolon = Keys.OemSemicolon, + + /// The OEM Tilde button on a US standard keyboard. + OemTilde = Keys.OemTilde, + + /// The P button on a keyboard. + P = Keys.P, + + /// The PA1 button on a keyboard. + Pa1 = Keys.Pa1, + + /// The Page Down button on a keyboard. + PageDown = Keys.PageDown, + + /// The Page Up button on a keyboard. + PageUp = Keys.PageUp, + + /// The Pause button on a keyboard. + Pause = Keys.Pause, + + /// The Play button on a keyboard. + Play = Keys.Play, + + /// The Print button on a keyboard. + Print = Keys.Print, + + /// The Print Screen button on a keyboard. + PrintScreen = Keys.PrintScreen, + + /// The IME Process button on a keyboard in Windows 95/98/ME/NT 4.0/2000/XP. + ProcessKey = Keys.ProcessKey, + + /// The Q button on a keyboard. + Q = Keys.Q, + + /// The R button on a keyboard. + R = Keys.R, + + /// The Right Arrow button on a keyboard. + Right = Keys.Right, + + /// The Right Alt button on a keyboard. + RightAlt = Keys.RightAlt, + + /// The Right Control button on a keyboard. + RightControl = Keys.RightControl, + + /// The Right Shift button on a keyboard. + RightShift = Keys.RightShift, + + /// The Right Windows button on a keyboard. + RightWindows = Keys.RightWindows, + + /// The S button on a keyboard. + S = Keys.S, + + /// The Scroll Lock button on a keyboard. + Scroll = Keys.Scroll, + + /// The Select button on a keyboard. + Select = Keys.Select, + + /// The Select Media button on a keyboard in Windows 2000/XP. + SelectMedia = Keys.SelectMedia, + + /// The Separator button on a keyboard. + Separator = Keys.Separator, + + /// The Computer Sleep button on a keyboard. + Sleep = Keys.Sleep, + + /// The Space bar on a keyboard. + Space = Keys.Space, + + /// The Subtract button on a keyboard. + Subtract = Keys.Subtract, + + /// The T button on a keyboard. + T = Keys.T, + + /// The Tab button on a keyboard. + Tab = Keys.Tab, + + /// The U button on a keyboard. + U = Keys.U, + + /// The Up Arrow button on a keyboard. + Up = Keys.Up, + + /// The V button on a keyboard. + V = Keys.V, + + /// The Volume Down button on a keyboard in Windows 2000/XP. + VolumeDown = Keys.VolumeDown, + + /// The Volume Mute button on a keyboard in Windows 2000/XP. + VolumeMute = Keys.VolumeMute, + + /// The Volume Up button on a keyboard in Windows 2000/XP. + VolumeUp = Keys.VolumeUp, + + /// The W button on a keyboard. + W = Keys.W, + + /// The X button on a keyboard. + X = Keys.X, + + /// The Y button on a keyboard. + Y = Keys.Y, + + /// The Z button on a keyboard. + Z = Keys.Z, + + /// The Zoom button on a keyboard. + Zoom = Keys.Zoom, + + } + + /// Provides extension methods for . + public static class SButtonExtensions + { + /********* + ** Accessors + *********/ + /// The offset added to values when converting them to to avoid collisions with values. + internal const int ControllerOffset = 2000; + + + /********* + ** Public methods + *********/ + /// Get the equivalent for the given button. + /// The keyboard button to convert. + public static SButton ToSButton(this Keys key) + { + return (SButton)key; + } + + /// Get the equivalent for the given button. + /// The controller button to convert. + public static SButton ToSButton(this Buttons key) + { + return (SButton)(SButtonExtensions.ControllerOffset + key); + } + + /// Get the equivalent for the given button. + /// The Stardew Valley button to convert. + public static SButton ToSButton(this InputButton input) + { + // derived from InputButton constructors + if (input.mouseLeft) + return SButton.MouseLeft; + if (input.mouseRight) + return SButton.MouseRight; + return input.key.ToSButton(); + } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The keyboard equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetKeyboard(this SButton input, out Keys key) + { + if (Enum.IsDefined(typeof(Keys), (int)input)) + { + key = (Keys)input; + return true; + } + + key = Keys.None; + return false; + } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The controller equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetController(this SButton input, out Buttons button) + { + if (Enum.IsDefined(typeof(Buttons), (int)input - SButtonExtensions.ControllerOffset)) + { + button = (Buttons)(input - SButtonExtensions.ControllerOffset); + return true; + } + + button = 0; + return false; + } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The Stardew Valley input button equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetStardewInput(this SButton input, out InputButton button) + { + // keyboard + if (input.TryGetKeyboard(out Keys key)) + { + button = new InputButton(key); + return true; + } + + // mouse + if (input == SButton.MouseLeft || input == SButton.MouseRight) + { + button = new InputButton(mouseLeft: input == SButton.MouseLeft); + return true; + } + + // not valid + button = default(InputButton); + return false; + } + + /// Get whether the given button is equivalent to . + /// The button. + public static bool IsUseToolButton(this SButton input) + { + return input == SButton.ControllerX || Game1.options.useToolButton.Any(p => p.ToSButton() == input); + } + + /// Get whether the given button is equivalent to . + /// The button. + public static bool IsActionButton(this SButton input) + { + return input == SButton.ControllerA || Game1.options.actionButton.Any(p => p.ToSButton() == input); + } + } +} diff --git a/src/StardewModdingAPI/SMainActivity.cs b/src/StardewModdingAPI/SMainActivity.cs new file mode 100644 index 00000000..feea1edc --- /dev/null +++ b/src/StardewModdingAPI/SMainActivity.cs @@ -0,0 +1,870 @@ +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.Graphics; +using Android.OS; +using Android.Provider; +using Android.Runtime; +using Android.Support.V4.App; +using Android.Support.V4.Content; +using Android.Util; +using Android.Views; +using Google.Android.Vending.Expansion.Downloader; +using Google.Android.Vending.Licensing; +using Java.Lang; +using Java.Util; +using Microsoft.AppCenter; +using Microsoft.AppCenter.Analytics; +using Microsoft.AppCenter.Crashes; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using StardewModdingAPI.Framework; +using StardewValley; +using Android.Widget; + +namespace StardewModdingAPI +{ + [Activity(Label = "Stardew Valley", Icon = "@mipmap/ic_launcher", Theme = "@style/Theme.Splash", MainLauncher = false, AlwaysRetainTaskState = true, LaunchMode = LaunchMode.SingleInstance, ScreenOrientation = ScreenOrientation.SensorLandscape, ConfigurationChanges = (ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden | ConfigChanges.Orientation | ConfigChanges.ScreenLayout | ConfigChanges.ScreenSize | ConfigChanges.UiMode))] + public class SMainActivity: MainActivity, ILicenseCheckerCallback, IJavaObject, IDisposable, IDownloaderClient + { + [Service] + public class ExpansionDownloaderService : DownloaderService + { + public override string PublicKey => "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAry4fecehDpCohQk4XhiIZX9ylIGUThWZxfN9qwvQyTh53hvnpQl/lCrjfflKoPz6gz5jJn6JI1PTnoBy/iXVx1+kbO99qBgJE2V8PS5pq+Usbeqqmqqzx4lEzhiYQ2um92v4qkldNYZFwbTODYPIMbSbaLm7eK9ZyemaRbg9ssAl4QYs0EVxzDK1DjuXilRk28WxiK3lNJTz4cT38bfs4q6Zvuk1vWUvnMqcxiugox6c/9j4zZS5C4+k+WY6mHjUMuwssjCY3G+aImWDSwnU3w9G41q8EoPvJ1049PIi7GJXErusTYZITmqfonyejmSFLPt8LHtux9AmJgFSrC3UhwIDAQAB"; + + public override string AlarmReceiverClassName => Class.FromType(typeof(ExpansionDownloaderReceiver)).CanonicalName; + + public override byte[] GetSalt() + { + return new byte[15] + { + 98, + 100, + 12, + 43, + 2, + 8, + 4, + 9, + 5, + 106, + 108, + 33, + 45, + 1, + 84 + }; + } + } + + [BroadcastReceiver(Exported = false)] + public class ExpansionDownloaderReceiver : BroadcastReceiver + { + public override void OnReceive(Android.Content.Context context, Intent intent) + { + DownloaderService.StartDownloadServiceIfRequired(context, intent, typeof(ExpansionDownloaderService)); + } + } + + private const float MIN_TILE_HEIGHT_IN_INCHES = 0.225f; + + private const float OPTIMAL_TILE_HEIGHT_IN_INCHES = 0.3f; + + private const float MIN_VISIBLE_ROWS = 10f; + + private const float MIN_ZOOM_SCALE = 0.5f; + + private const float MAX_ZOOM_SCALE = 5f; + + private const float OPTIMAL_BUTTON_HEIGHT_IN_INCHES = 0.2f; + + private SCore core; + + public const string API_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAry4fecehDpCohQk4XhiIZX9ylIGUThWZxfN9qwvQyTh53hvnpQl/lCrjfflKoPz6gz5jJn6JI1PTnoBy/iXVx1+kbO99qBgJE2V8PS5pq+Usbeqqmqqzx4lEzhiYQ2um92v4qkldNYZFwbTODYPIMbSbaLm7eK9ZyemaRbg9ssAl4QYs0EVxzDK1DjuXilRk28WxiK3lNJTz4cT38bfs4q6Zvuk1vWUvnMqcxiugox6c/9j4zZS5C4+k+WY6mHjUMuwssjCY3G+aImWDSwnU3w9G41q8EoPvJ1049PIi7GJXErusTYZITmqfonyejmSFLPt8LHtux9AmJgFSrC3UhwIDAQAB"; + + private LicenseChecker _licenseChecker; + + private IDownloaderService _expansionDownloaderService; + + private IDownloaderServiceConnection _downloaderServiceConnection; + + private PowerManager.WakeLock _wakeLock; + + private Action _callback; + + public static float ZoomScale + { + get; + private set; + } + + public static float MenuButtonScale + { + get; + private set; + } + + public static string LastSaveGameID + { + get; + private set; + } + + public static int ScreenWidthPixels + { + get; + private set; + } + + public static int ScreenHeightPixels + { + get; + private set; + } + + public bool HasPermissions + { + get + { + if (ContextCompat.CheckSelfPermission(this, "android.permission.ACCESS_NETWORK_STATE") == Permission.Granted && ContextCompat.CheckSelfPermission(this, "android.permission.ACCESS_NETWORK_STATE") == Permission.Granted && ContextCompat.CheckSelfPermission(this, "android.permission.ACCESS_WIFI_STATE") == Permission.Granted && ContextCompat.CheckSelfPermission(this, "android.permission.INTERNET") == Permission.Granted && ContextCompat.CheckSelfPermission(this, "android.permission.READ_EXTERNAL_STORAGE") == Permission.Granted && ContextCompat.CheckSelfPermission(this, "android.permission.VIBRATE") == Permission.Granted && ContextCompat.CheckSelfPermission(this, "android.permission.WAKE_LOCK") == Permission.Granted && ContextCompat.CheckSelfPermission(this, "android.permission.WRITE_EXTERNAL_STORAGE") == Permission.Granted && ContextCompat.CheckSelfPermission(this, "com.android.vending.CHECK_LICENSE") == Permission.Granted) + { + return true; + } + return false; + } + } + + private string[] requiredPermissions => new string[8] + { + "android.permission.ACCESS_NETWORK_STATE", + "android.permission.ACCESS_WIFI_STATE", + "android.permission.INTERNET", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.VIBRATE", + "android.permission.WAKE_LOCK", + "android.permission.WRITE_EXTERNAL_STORAGE", + "com.android.vending.CHECK_LICENSE" + }; + + private string[] deniedPermissionsArray + { + get + { + List list = new List(); + string[] requiredPermissions = this.requiredPermissions; + for (int i = 0; i < requiredPermissions.Length; i++) + { + if (ContextCompat.CheckSelfPermission(this, requiredPermissions[i]) != 0) + { + list.Add(requiredPermissions[i]); + } + } + return list.ToArray(); + } + } + + protected override void OnCreate(Bundle bundle) + { + instance = this; + AppCenter.Start("5677d40e-f7b3-4ccb-bee4-5dca56d86ade", typeof(Analytics), typeof(Crashes)); + this.RequestWindowFeature(WindowFeatures.NoTitle); + if (Build.VERSION.SdkInt >= BuildVersionCodes.P) + { + this.Window.Attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges; + } + this.Window.SetFlags(WindowManagerFlags.Fullscreen, WindowManagerFlags.Fullscreen); + this.Window.SetFlags(WindowManagerFlags.KeepScreenOn, WindowManagerFlags.KeepScreenOn); + PowerManager powerManager = (PowerManager)this.GetSystemService("power"); + this._wakeLock = powerManager.NewWakeLock(WakeLockFlags.Full, "StardewWakeLock"); + this._wakeLock.Acquire(); + base.OnCreate(bundle); + this.CheckAppPermissions(); + } + + public void OnCreatePartTwo() + { + this.SetZoomScaleAndMenuButtonScale(); + this.SetSavesPath(); + this.SetPaddingForMenus(); + Toast.MakeText(context: this, "Starting SMAPI", ToastLength.Long).Show(); + + Program.Main(null); + + this.core = new SCore(System.IO.Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "StardewValley/Mods"), false); + + this.core.RunInteractively(); + this.SetContentView((View)this.core.GameInstance.Services.GetService(typeof(View))); + this.core.GameInstance.Run(); + + //this._game1 = new Game1(); + //SetContentView((View)_game1.Services.GetService(typeof(View))); + //_game1.Run(); + + this.CheckForValidLicence(); + } + + protected override void OnResume() + { + base.OnResume(); + if (this._wakeLock != null && !this._wakeLock.IsHeld) + { + this._wakeLock.Acquire(); + } + if (this._expansionDownloaderService != null) + { + try + { + this._expansionDownloaderService.RequestContinueDownload(); + } + catch (System.Exception exception) + { + Crashes.TrackError(exception); + } + } + this.RequestedOrientation = ScreenOrientation.SensorLandscape; + this.SetImmersive(); + if (this._downloaderServiceConnection != null) + { + this._downloaderServiceConnection.Connect(this); + } + } + + protected override void OnStop() + { + try + { + if (this._wakeLock != null && this._wakeLock.IsHeld) + { + this._wakeLock.Release(); + } + } + catch (System.Exception exception) + { + Crashes.TrackError(exception); + } + base.OnStop(); + if (this._downloaderServiceConnection != null) + { + this._downloaderServiceConnection.Disconnect(this); + } + } + + public override void OnWindowFocusChanged(bool hasFocus) + { + base.OnWindowFocusChanged(hasFocus); + if (hasFocus) + { + this.RequestedOrientation = ScreenOrientation.SensorLandscape; + this.SetImmersive(); + } + } + + protected override void OnPause() + { + try + { + if (this._wakeLock != null && this._wakeLock.IsHeld) + { + this._wakeLock.Release(); + } + } + catch (System.Exception exception) + { + Crashes.TrackError(exception); + } + if (this._expansionDownloaderService != null) + { + try + { + this._expansionDownloaderService.RequestPauseDownload(); + } + catch (System.Exception exception2) + { + Crashes.TrackError(exception2); + } + } + base.OnPause(); + Game1.emergencyBackup(); + } + + protected void SetImmersive() + { + if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat) + { + this.Window.DecorView.SystemUiVisibility = (StatusBarVisibility)5894; + } + } + + private void SetSavesPath() + { + Game1.savesPath = System.IO.Path.Combine((string)(Java.Lang.Object)Android.OS.Environment.ExternalStorageDirectory, "StardewValley"); + Game1.hiddenSavesPath = System.IO.Path.Combine((string)(Java.Lang.Object)Android.OS.Environment.ExternalStorageDirectory, "StardewValley"); + } + + private void SetZoomScaleAndMenuButtonScale() + { + Android.Graphics.Point point = new Android.Graphics.Point(); + this.WindowManager.DefaultDisplay.GetRealSize(point); + float num = point.X; + float num2 = point.Y; + float num3 = System.Math.Min(this.Resources.DisplayMetrics.Xdpi, this.Resources.DisplayMetrics.Ydpi); + if (point.Y > point.X) + { + num = point.Y; + num2 = point.X; + } + ScreenWidthPixels = (int)num; + ScreenHeightPixels = (int)num2; + float num4 = num3 * 0.3f; + float num5 = num2 / num4; + float val = num4 / 64f; + if (num5 < 10f) + { + num4 = num3 * 0.225f; + val = num4 / 64f; + } + ZoomScale = System.Math.Max(0.5f, System.Math.Min(val, 5f)); + MenuButtonScale = num3 * 0.2f / 64f; + MenuButtonScale = System.Math.Max(0.5f, System.Math.Min(MenuButtonScale, 5f)); + Console.WriteLine("MainActivity.SetZoomScale width:" + num + ", height:" + num2 + ", dpi:" + num3 + ", pixelsPerTile:" + num4 + ", ZoomScale:" + ZoomScale + ", MenuButtonScale:" + MenuButtonScale); + } + + public int GetBuild() + { + Android.Content.Context context = Application.Context; + return context.PackageManager.GetPackageInfo(context.PackageName, (PackageInfoFlags)0).VersionCode; + } + + public void SetPaddingForMenus() + { + //("MainActivity.SetPaddingForMenus build:" + GetBuild()); + if (Build.VERSION.SdkInt >= BuildVersionCodes.P && this.Window != null && this.Window.DecorView != null && this.Window.DecorView.RootWindowInsets != null && this.Window.DecorView.RootWindowInsets.DisplayCutout != null) + { + DisplayCutout displayCutout = this.Window.DecorView.RootWindowInsets.DisplayCutout; + //("MainActivity.SetPaddingForMenus DisplayCutout:" + displayCutout); + if (displayCutout.SafeInsetLeft > 0 || displayCutout.SafeInsetRight > 0) + { + Game1.toolbarPaddingX = (Game1.xEdge = System.Math.Max(displayCutout.SafeInsetLeft, displayCutout.SafeInsetRight)); + //("MainActivity.SetPaddingForMenus CUT OUT toolbarPaddingX:" + Game1.toolbarPaddingX + ", xEdge:" + Game1.xEdge); + return; + } + } + string manufacturer = Build.Manufacturer; + string model = Build.Model; + if (manufacturer == "Google" && model == "Pixel 2 XL") + { + Game1.xEdge = 26; + Game1.toolbarPaddingX = 12; + } + else if (manufacturer.ToLower() == "samsung") + { + if (model == "SM-G950U") + { + Game1.xEdge = 25; + Game1.toolbarPaddingX = 40; + } + else if (model == "SM-N960F") + { + Game1.xEdge = 20; + Game1.toolbarPaddingX = 20; + } + } + else + { + DisplayMetrics displayMetrics = new DisplayMetrics(); + this.WindowManager.DefaultDisplay.GetRealMetrics(displayMetrics); + if (displayMetrics.HeightPixels >= 1920 || displayMetrics.WidthPixels >= 1920) + { + Game1.xEdge = 20; + Game1.toolbarPaddingX = 20; + } + } + //("MainActivity.SetPaddingForMenus Manufacturer:" + manufacturer + ", Model:" + model + ", xEdge:" + Game1.xEdge + ", toolbarPaddingX:" + Game1.toolbarPaddingX); + } + + private void CheckForLastSavedGame() + { + string savesPath = Game1.savesPath; + LastSaveGameID = null; + int num = 0; + if (!Directory.Exists(savesPath)) + { + return; + } + string[] array = Directory.EnumerateDirectories(savesPath).ToArray(); + foreach (string path in array) + { + string text = System.IO.Path.Combine(savesPath, path, "SaveGameInfo"); + DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(text); + int num2 = (int)lastWriteTimeUtc.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; + if (num2 > num) + { + num = num2; + string[] array2 = text.Split('/'); + if (array2.Length > 1) + { + LastSaveGameID = array2[array2.Length - 2]; + } + } + //("MainActivity.CheckForLastSavedGame pathToFile:" + text + ", lastModified:" + lastWriteTimeUtc.ToLongDateString() + ", unixTimestamp:" + num2); + } + } + + private void CheckToCopySaveGames() + { + string savesPath = Game1.savesPath; + if (!Directory.Exists(savesPath + "/Ahsoka_119548412")) + { + this.CopySaveGame("Ahsoka_119548412"); + } + if (!Directory.Exists(savesPath + "/Leia_116236289")) + { + this.CopySaveGame("Leia_116236289"); + } + } + + private void CopySaveGame(string saveGameID) + { + //("MainActivity.CopySaveGame... saveGameID:" + saveGameID); + MemoryStream memoryStream = new MemoryStream(131072); + Stream stream = TitleContainer.OpenStream("Content/SaveGames/" + saveGameID + "/SaveGameInfo"); + stream.CopyTo(memoryStream); + memoryStream.Seek(0L, SeekOrigin.Begin); + byte[] buffer = memoryStream.GetBuffer(); + int count = (int)memoryStream.Length; + stream.Close(); + memoryStream = new MemoryStream(2097152); + Stream stream2 = TitleContainer.OpenStream("Content/SaveGames/" + saveGameID + "/" + saveGameID); + stream2.CopyTo(memoryStream); + memoryStream.Seek(0L, SeekOrigin.Begin); + byte[] buffer2 = memoryStream.GetBuffer(); + int count2 = (int)memoryStream.Length; + stream2.Close(); + try + { + string savesPath = Game1.savesPath; + if (!Directory.Exists(savesPath)) + { + Directory.CreateDirectory(savesPath); + } + string text = System.IO.Path.Combine(savesPath, saveGameID); + Directory.CreateDirectory(text); + using (FileStream fileStream = File.OpenWrite(System.IO.Path.Combine(text, "SaveGameInfo"))) + { + fileStream.Write(buffer, 0, count); + } + using (FileStream fileStream2 = File.OpenWrite(System.IO.Path.Combine(text, saveGameID))) + { + fileStream2.Write(buffer2, 0, count2); + } + } + catch (System.Exception ex) + { + //("MainActivity.CopySaveGame ERROR WRITING STREAM:" + ex.Message); + } + } + + public void ShowDiskFullDialogue() + { + //("MainActivity.ShowDiskFullDialogue"); + string message = "Disk full. You need to free up some space to continue."; + if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.de) + { + message = "Festplatte voll. Sie müssen etwas Platz schaffen, um fortzufahren."; + } + else if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.es) + { + message = "Disco lleno. Necesitas liberar algo de espacio para continuar."; + } + else if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.fr) + { + message = "Disque plein. Vous devez libérer de l'espace pour continuer."; + } + else if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.hu) + { + message = "Megtelt a lemez. Szüksége van egy kis hely felszabadítására a folytatáshoz."; + } + else if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.it) + { + message = "Disco pieno. È necessario liberare spazio per continuare."; + } + else if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.ja) + { + message = "ディスクがいっぱいです。続行するにはスペ\u30fcスをいくらか解放する必要があります。"; + } + else if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.ko) + { + message = "디스크 꽉 참. 계속하려면 여유 공간을 확보해야합니다."; + } + else if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.pt) + { + message = "Disco cheio. Você precisa liberar algum espaço para continuar."; + } + else if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.ru) + { + message = "Диск полон. Вам нужно освободить место, чтобы продолжить."; + } + else if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.tr) + { + message = "Disk dolu. Devam etmek için biraz alan boşaltmanız gerekiyor."; + } + else if (LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.zh) + { + message = "磁盘已满。你需要释放一些空间才能继续。"; + } + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.SetMessage(message); + builder.SetPositiveButton("OK", delegate + { + }); + Dialog dialog = builder.Create(); + if (!this.IsFinishing) + { + dialog.Show(); + } + } + + public void PromptForPermissionsIfNecessary(Action callback = null) + { + if (this.HasPermissions) + { + callback?.Invoke(); + return; + } + this._callback = callback; + this.PromptForPermissions(); + } + + private void LogPermissions() + { + //("MainActivity.LogPermissions , AccessNetworkState:" + ContextCompat.CheckSelfPermission(this, "android.permission.ACCESS_NETWORK_STATE") + ", AccessWifiState:" + ContextCompat.CheckSelfPermission(this, "android.permission.ACCESS_WIFI_STATE") + ", Internet:" + ContextCompat.CheckSelfPermission(this, "android.permission.INTERNET") + ", ReadExternalStorage:" + ContextCompat.CheckSelfPermission(this, "android.permission.READ_EXTERNAL_STORAGE") + ", Vibrate:" + ContextCompat.CheckSelfPermission(this, "android.permission.VIBRATE") + ", WakeLock:" + ContextCompat.CheckSelfPermission(this, "android.permission.WAKE_LOCK") + ", WriteExternalStorage:" + ContextCompat.CheckSelfPermission(this, "android.permission.WRITE_EXTERNAL_STORAGE") + ", CheckLicense:" + ContextCompat.CheckSelfPermission(this, "com.android.vending.CHECK_LICENSE")); + } + + public void CheckAppPermissions() + { + this.LogPermissions(); + if (this.HasPermissions) + { + //("MainActivity.CheckAppPermissions permissions already granted."); + this.OnCreatePartTwo(); + } + else + { + this.PromptForPermissions(); + } + } + + public void PromptForPermissions() + { + //("MainActivity.PromptForPermissions requesting permissions..."); + if (!this.IsFinishing) + { + ActivityCompat.RequestPermissions(this, this.deniedPermissionsArray, 0); + } + } + + public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) + { + //("MainActivity.OnRequestPermissionsResult requestCode:" + requestCode + " len:" + permissions.Length); + if (permissions.Length == 0) + { + //("MainActivity.OnRequestPermissionsResult no permissions returned, RETURNING"); + return; + } + string text = Java.Util.Locale.Default.Language.Substring(0, 2); + //("OnRequestPermissionsResult Language Code:" + text); + string message; + string message2; + switch (text) + { + case "de": + message = "Du musst die Erlaubnis zum Lesen/Schreiben auf dem externen Speicher geben, um das Spiel zu speichern und Speicherstände auf andere Plattformen übertragen zu können. Bitte gib diese Genehmigung, um spielen zu können."; + message2 = "Bitte geh in die Handy-Einstellungen > Apps > Stardew Valley > Berechtigungen und aktiviere den Speicher, um das Spiel zu spielen."; + break; + case "es": + message = "Para guardar la partida y transferir partidas guardadas a y desde otras plataformas, se necesita permiso para leer/escribir en almacenamiento externo. Concede este permiso para poder jugar."; + message2 = "En el teléfono, ve a Ajustes > Aplicaciones > Stardew Valley > Permisos y activa Almacenamiento para jugar al juego."; + break; + case "ja": + message = "外部機器への読み込み/書き出しの許可が、ゲ\u30fcムのセ\u30fcブデ\u30fcタの保存や他プラットフォ\u30fcムとの双方向のデ\u30fcタ移行実行に必要です。プレイを続けるには許可をしてください。"; + message2 = "設定 > アプリ > スタ\u30fcデュ\u30fcバレ\u30fc > 許可の順に開いていき、ストレ\u30fcジを有効にしてからゲ\u30fcムをプレイしましょう。"; + break; + case "pt": + message = "Para salvar o jogo e transferir jogos salvos entre plataformas é necessário permissão para ler/gravar em armazenamento externo. Forneça essa permissão para jogar."; + message2 = "Acesse Configurar > Aplicativos > Stardew Valley > Permissões e ative Armazenamento para jogar."; + break; + case "ru": + message = "Для сохранения игры и переноса сохранений с/на другие платформы нужно разрешение на чтение-запись на внешнюю память. Дайте разрешение, чтобы начать играть."; + message2 = "Перейдите в меню Настройки > Приложения > Stardew Valley > Разрешения и дайте доступ к памяти, чтобы начать играть."; + break; + case "ko": + message = "게임을 저장하려면 외부 저장공간에 대한 읽기/쓰기 권한이 필요합니다. 또한 저장 데이터 이전 기능을 허용해 다른 플랫폼에서 게임 진행상황을 가져올 때에도 권한이 필요합니다. 게임을 플레이하려면 권한을 허용해 주십시오."; + message2 = "휴대전화의 설정 > 어플리케이션 > 스타듀 밸리 > 권한 에서 저장공간을 활성화한 뒤 게임을 플레이해 주십시오."; + break; + case "tr": + message = "Oyunu kaydetmek ve kayıtları platformlardan platformlara taşımak için harici depolamada okuma/yazma izni gereklidir. Lütfen oynayabilmek için izin verin."; + message2 = "Lütfen oyunu oynayabilmek için telefonda Ayarlar > Uygulamalar > Stardew Valley > İzinler ve Depolamayı etkinleştir yapın."; + break; + case "fr": + message = "Une autorisation de lecture / écriture sur un stockage externe est requise pour sauvegarder le jeu et vous permettre de transférer des sauvegardes vers et depuis d'autres plateformes. Veuillez donner l'autorisation afin de jouer."; + message2 = "Veuillez aller dans les Paramètres du téléphone> Applications> Stardew Valley> Autorisations, puis activez Stockage pour jouer."; + break; + case "hu": + message = "A játék mentéséhez, és ahhoz, hogy a különböző platformok között hordozhasd a játékmentést, engedélyezned kell a külső tárhely olvasását/írását, Kérjük, a játékhoz engedélyezd ezeket."; + message2 = "Lépje be a telefonodon a Beállítások > Alkalmazások > Stardew Valley > Engedélyek menübe, majd engedélyezd a Tárhelyet a játékhoz."; + break; + case "it": + message = "È necessaria l'autorizzazione a leggere/scrivere su un dispositivo di memorizzazione esterno per salvare la partita e per consentire di trasferire i salvataggi da e su altre piattaforme. Concedi l'autorizzazione per giocare."; + message2 = "Nel telefono, vai su Impostazioni > Applicazioni > Stardew Valley > Autorizzazioni e attiva Memoria archiviazione per giocare."; + break; + case "zh": + message = "保存游戏进度,以及授权与其它平台交换游戏进度文件,都需要对外部存储器进行读 / 写的权限。要正常游戏,请授予权限。"; + message2 = "请转到手机的设置 > 应用 > Stardew Valley > 权限里,启用“存储”,以正常游戏。"; + break; + default: + message = "Read/write to external storage permission is required to save the game, and to allow to you transfer saves to and from other platforms. Please give permission in order to play."; + message2 = "Please go into phone Settings > Apps > Stardew Valley > Permissions, and enable Storage to play the game."; + break; + } + int num = 0; + if (requestCode == 0) + { + for (int i = 0; i < grantResults.Length; i++) + { + //("MainActivity.OnRequestPermissionsResult permission:" + permissions[i] + ", granted:" + grantResults[i]); + if (grantResults[i] == Permission.Granted) + { + num++; + } + else if (grantResults[i] == Permission.Denied) + { + //("MainActivity.OnRequestPermissionsResult PERMISSION " + permissions[i] + " DENIED!"); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + if (ActivityCompat.ShouldShowRequestPermissionRationale(this, permissions[i])) + { + builder.SetMessage(message); + builder.SetPositiveButton("OK", delegate + { + this.PromptForPermissions(); + }); + } + else + { + builder.SetMessage(message2); + builder.SetPositiveButton("OK", delegate + { + this.FinishAffinity(); + }); + } + Dialog dialog = builder.Create(); + if (!this.IsFinishing) + { + dialog.Show(); + } + return; + } + } + } + if (num == permissions.Length) + { + if (this._callback != null) + { + //("MainActivity.OnRequestPermissionsResult permissions granted, calling callback"); + this._callback(); + this._callback = null; + } + else + { + //("MainActivity.OnRequestPermissionsResult " + num + "/" + permissions.Length + " granted, check for licence..."); + this.OnCreatePartTwo(); + } + } + } + + private void CheckUsingServerManagedPolicy() + { + //("MainActivity.CheckUsingServerManagedPolicy"); + byte[] salt = new byte[15] + { + 46, + 65, + 30, + 128, + 103, + 57, + 74, + 64, + 51, + 88, + 95, + 45, + 77, + 117, + 36 + }; + string packageName = this.PackageName; + string @string = Settings.Secure.GetString(this.ContentResolver, "android_id"); + AESObfuscator obfuscator = new AESObfuscator(salt, packageName, @string); + ServerManagedPolicy policy = new ServerManagedPolicy(this, obfuscator); + this._licenseChecker = new LicenseChecker(this, policy, "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAry4fecehDpCohQk4XhiIZX9ylIGUThWZxfN9qwvQyTh53hvnpQl/lCrjfflKoPz6gz5jJn6JI1PTnoBy/iXVx1+kbO99qBgJE2V8PS5pq+Usbeqqmqqzx4lEzhiYQ2um92v4qkldNYZFwbTODYPIMbSbaLm7eK9ZyemaRbg9ssAl4QYs0EVxzDK1DjuXilRk28WxiK3lNJTz4cT38bfs4q6Zvuk1vWUvnMqcxiugox6c/9j4zZS5C4+k+WY6mHjUMuwssjCY3G+aImWDSwnU3w9G41q8EoPvJ1049PIi7GJXErusTYZITmqfonyejmSFLPt8LHtux9AmJgFSrC3UhwIDAQAB"); + this._licenseChecker.CheckAccess(this); + } + + private void CheckForValidLicence() + { + //("MainActivity.CheckForValidLicence"); + StrictPolicy policy = new StrictPolicy(); + this._licenseChecker = new LicenseChecker(this, policy, "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAry4fecehDpCohQk4XhiIZX9ylIGUThWZxfN9qwvQyTh53hvnpQl/lCrjfflKoPz6gz5jJn6JI1PTnoBy/iXVx1+kbO99qBgJE2V8PS5pq+Usbeqqmqqzx4lEzhiYQ2um92v4qkldNYZFwbTODYPIMbSbaLm7eK9ZyemaRbg9ssAl4QYs0EVxzDK1DjuXilRk28WxiK3lNJTz4cT38bfs4q6Zvuk1vWUvnMqcxiugox6c/9j4zZS5C4+k+WY6mHjUMuwssjCY3G+aImWDSwnU3w9G41q8EoPvJ1049PIi7GJXErusTYZITmqfonyejmSFLPt8LHtux9AmJgFSrC3UhwIDAQAB"); + this._licenseChecker.CheckAccess(this); + } + + public void Allow(PolicyResponse response) + { + //("MainActivity.Allow response:" + response.ToString()); + this.CheckToDownloadExpansion(); + } + + public void DontAllow(PolicyResponse response) + { + //("MainActivity.DontAllow response:" + response.ToString()); + switch (response) + { + case PolicyResponse.Retry: + this.WaitThenCheckForValidLicence(); + break; + case PolicyResponse.Licensed: + this.CheckToDownloadExpansion(); + break; + } + } + + private async void WaitThenCheckForValidLicence() + { + await Task.Delay(TimeSpan.FromSeconds(30.0)); + this.CheckForValidLicence(); + } + + public void ApplicationError(LicenseCheckerErrorCode errorCode) + { + //("MainActivity.ApplicationError errorCode:" + errorCode.ToString()); + } + + private void CheckToDownloadExpansion() + { + if (this.ExpansionAlreadyDownloaded()) + { + //("MainActivity.CheckToDownloadExpansion Expansion already downloaded"); + this.OnExpansionDowloaded(); + } + else + { + //("MainActivity.CheckToDownloadExpansion Need to download expansion"); + this.StartExpansionDownload(); + } + } + + private bool ExpansionAlreadyDownloaded() + { + DownloadInfo[] downloads = DownloadsDB.GetDB().GetDownloads(); + if (downloads == null || !downloads.Any()) + { + return false; + } + if (downloads != null) + { + DownloadInfo[] array = downloads; + foreach (DownloadInfo downloadInfo in array) + { + if (!Helpers.DoesFileExist(this, downloadInfo.FileName, downloadInfo.TotalBytes, deleteFileOnMismatch: false)) + { + return false; + } + } + } + return true; + } + + private void OnExpansionDowloaded() + { + if (this.core.GameInstance != null) + { + this.core.GameInstance.CreateMusicWaveBank(); + } + } + + private void StartExpansionDownload() + { + //("MainActivity.StartExpansionDownload"); + Intent intent = this.Intent; + Intent intent2 = new Intent(this, typeof(SMainActivity)); + intent2.SetFlags(ActivityFlags.ClearTop | ActivityFlags.NewTask); + intent2.SetAction(intent.Action); + if (intent.Categories != null) + { + foreach (string category in intent.Categories) + { + intent2.AddCategory(category); + } + } + PendingIntent activity = PendingIntent.GetActivity(this, 0, intent2, PendingIntentFlags.UpdateCurrent); + try + { + DownloaderServiceRequirement downloaderServiceRequirement = DownloaderService.StartDownloadServiceIfRequired(this, activity, typeof(ExpansionDownloaderService)); + if (downloaderServiceRequirement != 0) + { + //("MainActivity.StartExpansionDownload A startResult:" + downloaderServiceRequirement); + this._downloaderServiceConnection = DownloaderClientMarshaller.CreateStub(this, typeof(ExpansionDownloaderService)); + this._downloaderServiceConnection.Connect(this); + //("MainActivity.StartExpansionDownload B startResult:" + downloaderServiceRequirement); + } + else + { + //("MainActivity.StartExpansionDownload - all files have finished downloading already"); + this.OnExpansionDowloaded(); + } + } + catch (IllegalStateException ex) + { + //("MainActivity.StartExpansionDownload ERROR exception:" + ex); + Crashes.TrackError(ex); + } + } + + public void OnServiceConnected(Messenger messenger) + { + //("MainActivity.OnServiceConnected messenger:" + messenger.ToString()); + this._expansionDownloaderService = DownloaderServiceMarshaller.CreateProxy(messenger); + this._expansionDownloaderService.OnClientUpdated(this._downloaderServiceConnection.GetMessenger()); + } + + public void OnDownloadProgress(DownloadProgressInfo progress) + { + //("MainActivity.OnDownloadProgress OverallProgress:" + progress.OverallProgress + ", OverallTotal:" + progress.OverallTotal + ", TimeRemaining:" + progress.TimeRemaining + ", CurrentSpeed:" + progress.CurrentSpeed); + } + + public void OnDownloadStateChanged(DownloaderClientState downloaderClientState) + { + //("MainActivity.OnDownloadStateChanged downloaderClientState:" + downloaderClientState.ToString()); + switch (downloaderClientState) + { + case DownloaderClientState.PausedWifiDisabledNeedCellularPermission: + case DownloaderClientState.PausedNeedCellularPermission: + this._expansionDownloaderService.SetDownloadFlags(DownloaderServiceFlags.DownloadOverCellular); + this._expansionDownloaderService.RequestContinueDownload(); + break; + case DownloaderClientState.Completed: + if (this._expansionDownloaderService != null) + { + this._expansionDownloaderService.Dispose(); + this._expansionDownloaderService = null; + } + this.OnExpansionDowloaded(); + break; + } + } + } +} diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs new file mode 100644 index 00000000..ec2d9e40 --- /dev/null +++ b/src/StardewModdingAPI/SemanticVersion.cs @@ -0,0 +1,170 @@ +using System; +using Newtonsoft.Json; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI +{ + /// A semantic version with an optional release tag. + public class SemanticVersion : ISemanticVersion + { + /********* + ** Fields + *********/ + /// The underlying semantic version implementation. + private readonly ISemanticVersion Version; + + + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + public int MajorVersion => this.Version.MajorVersion; + + /// The minor version incremented for backwards-compatible changes. + public int MinorVersion => this.Version.MinorVersion; + + /// The patch version for backwards-compatible bug fixes. + public int PatchVersion => this.Version.PatchVersion; + +#if !SMAPI_3_0_STRICT + /// An optional build tag. + [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] + public string Build + { + get + { + SCore.DeprecationManager?.Warn($"{nameof(ISemanticVersion)}.{nameof(ISemanticVersion.Build)}", "2.8", DeprecationLevel.PendingRemoval); + return this.Version.PrereleaseTag; + } + } +#endif + + /// An optional prerelease tag. + public string PrereleaseTag => this.Version.PrereleaseTag; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible bug fixes. + /// An optional build tag. + [JsonConstructor] + public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) + : this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, build)) { } + + /// Construct an instance. + /// The semantic version string. + /// The is null. + /// The is not a valid semantic version. + public SemanticVersion(string version) + : this(new Toolkit.SemanticVersion(version)) { } + + /// Construct an instance. + /// The assembly version. + /// The is null. + public SemanticVersion(Version version) + : this(new Toolkit.SemanticVersion(version)) { } + + /// Construct an instance. + /// The underlying semantic version implementation. + internal SemanticVersion(ISemanticVersion version) + { + this.Version = version; + } + + /// Whether this is a pre-release version. + public bool IsPrerelease() + { + return this.Version.IsPrerelease(); + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The version to compare with this instance. + /// The value is null. + /// The implementation is defined by Semantic Version 2.0 (https://semver.org/). + public int CompareTo(ISemanticVersion other) + { + return this.Version.CompareTo(other); + } + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + public bool IsOlderThan(ISemanticVersion other) + { + return this.Version.IsOlderThan(other); + } + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsOlderThan(string other) + { + return this.Version.IsOlderThan(other); + } + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + public bool IsNewerThan(ISemanticVersion other) + { + return this.Version.IsNewerThan(other); + } + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsNewerThan(string other) + { + return this.Version.IsNewerThan(other); + } + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + public bool IsBetween(ISemanticVersion min, ISemanticVersion max) + { + return this.Version.IsBetween(min, max); + } + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// One of the specified versions is not a valid semantic version. + public bool IsBetween(string min, string max) + { + return this.Version.IsBetween(min, max); + } + + /// Indicates whether the current object is equal to another object of the same type. + /// true if the current object is equal to the parameter; otherwise, false. + /// An object to compare with this object. + public bool Equals(ISemanticVersion other) + { + return other != null && this.CompareTo(other) == 0; + } + + /// Get a string representation of the version. + public override string ToString() + { + return this.Version.ToString(); + } + + /// Parse a version string without throwing an exception if it fails. + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + internal static bool TryParse(string version, out ISemanticVersion parsed) + { + if (Toolkit.SemanticVersion.TryParse(version, out ISemanticVersion versionImpl)) + { + parsed = new SemanticVersion(versionImpl); + return true; + } + + parsed = null; + return false; + } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj new file mode 100644 index 00000000..c678ed1d --- /dev/null +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -0,0 +1,414 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {9898B56E-51EB-40CF-8B1F-ACEB4B6397A7} + {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {9ef11e43-1701-4396-8835-8392d57abb70} + Library + Properties + StardewModdingAPI + StardewModdingAPI + 512 + Resources\Resource.designer.cs + Off + false + v9.0 + + + true + portable + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\..\..\..\..\..\SteamLibrary\steamapps\common\Stardew Valley\smapi-internal\0Harmony.dll + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\BmFont.dll + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\Google.Android.Vending.Expansion.Downloader.dll + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\Google.Android.Vending.Expansion.ZipFile.dll + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\Google.Android.Vending.Licensing.dll + + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\Microsoft.AppCenter.dll + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\Microsoft.AppCenter.Analytics.dll + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\Microsoft.AppCenter.Analytics.Android.Bindings.dll + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\Microsoft.AppCenter.Android.Bindings.dll + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\Microsoft.AppCenter.Crashes.dll + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\Microsoft.AppCenter.Crashes.Android.Bindings.dll + + + + + ..\..\..\..\..\..\..\SteamLibrary\steamapps\common\Stardew Valley\smapi-internal\Mono.Cecil.dll + + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\MonoGame.Framework.dll + + + ..\..\..\..\..\Downloads\MonoMod.RuntimeDetour.dll + + + ..\..\..\..\..\Downloads\MonoMod.Utils.dll + + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\StardewValley.dll + + + + + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\Xamarin.Android.Support.v4.dll + + + ..\..\..\..\..\Downloads\Stardew-Valley-v1-25_mod\unknown\assemblies\xTile.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {d5cfd923-37f1-4bc3-9be8-e506e202ac28} + StardewModdingAPI.Toolkit.CoreInterfaces + + + {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} + StardewModdingAPI.Toolkit + + + + + + \ No newline at end of file diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs new file mode 100644 index 00000000..abcdb336 --- /dev/null +++ b/src/StardewModdingAPI/Translation.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI +{ + /// A translation string with a fluent API to customise it. + public class Translation + { + /********* + ** Fields + *********/ + /// The placeholder text when the translation is null or empty, where {0} is the translation key. + internal const string PlaceholderText = "(no translation:{0})"; + + /// The name of the relevant mod for error messages. + private readonly string ModName; + + /// The locale for which the translation was fetched. + private readonly string Locale; + + /// The underlying translation text. + private readonly string Text; + + /// The value to return if the translations is undefined. + private readonly string Placeholder; + + + /********* + ** Accessors + *********/ + /// The original translation key. + public string Key { get; } + + + /********* + ** Public methods + *********/ + /// Construct an isntance. + /// The name of the relevant mod for error messages. + /// The locale for which the translation was fetched. + /// The translation key. + /// The underlying translation text. + internal Translation(string modName, string locale, string key, string text) + : this(modName, locale, key, text, string.Format(Translation.PlaceholderText, key)) { } + + /// Construct an isntance. + /// The name of the relevant mod for error messages. + /// The locale for which the translation was fetched. + /// The translation key. + /// The underlying translation text. + /// The value to return if the translations is undefined. + internal Translation(string modName, string locale, string key, string text, string placeholder) + { + this.ModName = modName; + this.Locale = locale; + this.Key = key; + this.Text = text; + this.Placeholder = placeholder; + } + + /// Throw an exception if the translation text is null or empty. + /// There's no available translation matching the requested key and locale. + public Translation Assert() + { + if (!this.HasValue()) + throw new KeyNotFoundException($"The '{this.ModName}' mod doesn't have a translation with key '{this.Key}' for the '{this.Locale}' locale or its fallbacks."); + return this; + } + + /// Replace the text if it's null or empty. If you set a null or empty value, the translation will show the fallback "no translation" placeholder (see if you want to disable that). Returns a new instance if changed. + /// The default value. + public Translation Default(string @default) + { + return this.HasValue() + ? this + : new Translation(this.ModName, this.Locale, this.Key, @default); + } + + /// Whether to return a "no translation" placeholder if the translation is null or empty. Returns a new instance. + /// Whether to return a placeholder. + public Translation UsePlaceholder(bool use) + { + return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? string.Format(Translation.PlaceholderText, this.Key) : null); + } + + /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. + /// The argument is null. + public Translation Tokens(object tokens) + { + if (string.IsNullOrWhiteSpace(this.Text) || tokens == null) + return this; + + // get dictionary of tokens + IDictionary tokenLookup = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + { + // from dictionary + if (tokens is IDictionary inputLookup) + { + foreach (DictionaryEntry entry in inputLookup) + { + string key = entry.Key?.ToString().Trim(); + if (key != null) + tokenLookup[key] = entry.Value?.ToString(); + } + } + + // from object properties + else + { + Type type = tokens.GetType(); + foreach (PropertyInfo prop in type.GetProperties()) + tokenLookup[prop.Name] = prop.GetValue(tokens)?.ToString(); + foreach (FieldInfo field in type.GetFields()) + tokenLookup[field.Name] = field.GetValue(tokens)?.ToString(); + } + } + + // format translation + string text = Regex.Replace(this.Text, @"{{([ \w\.\-]+)}}", match => + { + string key = match.Groups[1].Value.Trim(); + return tokenLookup.TryGetValue(key, out string value) + ? value + : match.Value; + }); + return new Translation(this.ModName, this.Locale, this.Key, text); + } + + /// Get whether the translation has a defined value. + public bool HasValue() + { + return !string.IsNullOrEmpty(this.Text); + } + + /// Get the translation text. Calling this method isn't strictly necessary, since you can assign a value directly to a string. + public override string ToString() + { + return this.Placeholder != null && !this.HasValue() + ? this.Placeholder + : this.Text; + } + + /// Get a string representation of the given translation. + /// The translation key. + public static implicit operator string(Translation translation) + { + return translation?.ToString(); + } + } +} diff --git a/src/StardewModdingAPI/Utilities/SDate.cs b/src/StardewModdingAPI/Utilities/SDate.cs new file mode 100644 index 00000000..ec54f84a --- /dev/null +++ b/src/StardewModdingAPI/Utilities/SDate.cs @@ -0,0 +1,268 @@ +using System; +using System.Linq; +using StardewValley; + +namespace StardewModdingAPI.Utilities +{ + /// Represents a Stardew Valley date. + public class SDate : IEquatable + { + /********* + ** Fields + *********/ + /// The internal season names in order. + private readonly string[] Seasons = { "spring", "summer", "fall", "winter" }; + + /// The number of seasons in a year. + private int SeasonsInYear => this.Seasons.Length; + + /// The number of days in a season. + private readonly int DaysInSeason = 28; + + + /********* + ** Accessors + *********/ + /// The day of month. + public int Day { get; } + + /// The season name. + public string Season { get; } + + /// The year. + public int Year { get; } + + /// The day of week. + public DayOfWeek DayOfWeek { get; } + + /// The number of days since the game began (starting at 1 for the first day of spring in Y1). + public int DaysSinceStart { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The day of month. + /// The season name. + /// One of the arguments has an invalid value (like day 35). + public SDate(int day, string season) + : this(day, season, Game1.year) { } + + /// Construct an instance. + /// The day of month. + /// The season name. + /// The year. + /// One of the arguments has an invalid value (like day 35). + public SDate(int day, string season, int year) + : this(day, season, year, allowDayZero: false) { } + + /// Get the current in-game date. + public static SDate Now() + { + return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year, allowDayZero: true); + } + + /// Get a new date with the given number of days added. + /// The number of days to add. + /// Returns the resulting date. + /// The offset would result in an invalid date (like year 0). + public SDate AddDays(int offset) + { + // get new hash code + int hashCode = this.DaysSinceStart + offset; + if (hashCode < 1) + throw new ArithmeticException($"Adding {offset} days to {this} would result in a date before 01 spring Y1."); + + // get day + int day = hashCode % 28; + if (day == 0) + day = 28; + + // get season index + int seasonIndex = hashCode / 28; + if (seasonIndex > 0 && hashCode % 28 == 0) + seasonIndex -= 1; + seasonIndex %= 4; + + // get year + int year = hashCode / (this.Seasons.Length * this.DaysInSeason) + 1; + + // create date + return new SDate(day, this.Seasons[seasonIndex], year); + } + + /// Get a string representation of the date. This is mainly intended for debugging or console messages. + public override string ToString() + { + return $"{this.Day:00} {this.Season} Y{this.Year}"; + } + + /**** + ** IEquatable + ****/ + /// Get whether this instance is equal to another. + /// The other value to compare. + public bool Equals(SDate other) + { + return this == other; + } + + /// Get whether this instance is equal to another. + /// The other value to compare. + public override bool Equals(object obj) + { + return obj is SDate other && this == other; + } + + /// Get a hash code which uniquely identifies a date. + public override int GetHashCode() + { + return this.DaysSinceStart; + } + + /**** + ** Operators + ****/ + /// Get whether one date is equal to another. + /// The base date to compare. + /// The other date to compare. + /// The equality of the dates + public static bool operator ==(SDate date, SDate other) + { + return date?.DaysSinceStart == other?.DaysSinceStart; + } + + /// Get whether one date is not equal to another. + /// The base date to compare. + /// The other date to compare. + public static bool operator !=(SDate date, SDate other) + { + return date?.DaysSinceStart != other?.DaysSinceStart; + } + + /// Get whether one date is more than another. + /// The base date to compare. + /// The other date to compare. + public static bool operator >(SDate date, SDate other) + { + return date?.DaysSinceStart > other?.DaysSinceStart; + } + + /// Get whether one date is more than or equal to another. + /// The base date to compare. + /// The other date to compare. + public static bool operator >=(SDate date, SDate other) + { + return date?.DaysSinceStart >= other?.DaysSinceStart; + } + + /// Get whether one date is less than or equal to another. + /// The base date to compare. + /// The other date to compare. + public static bool operator <=(SDate date, SDate other) + { + return date?.DaysSinceStart <= other?.DaysSinceStart; + } + + /// Get whether one date is less than another. + /// The base date to compare. + /// The other date to compare. + public static bool operator <(SDate date, SDate other) + { + return date?.DaysSinceStart < other?.DaysSinceStart; + } + + + /********* + ** Private methods + *********/ + /// Construct an instance. + /// The day of month. + /// The season name. + /// The year. + /// Whether to allow 0 spring Y1 as a valid date. + /// One of the arguments has an invalid value (like day 35). + private SDate(int day, string season, int year, bool allowDayZero) + { + // validate + if (season == null) + throw new ArgumentNullException(nameof(season)); + if (!this.Seasons.Contains(season)) + throw new ArgumentException($"Unknown season '{season}', must be one of [{string.Join(", ", this.Seasons)}]."); + if (day < 0 || day > this.DaysInSeason) + throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); + if(day == 0 && !(allowDayZero && this.IsDayZero(day, season, year))) + throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); + if (year < 1) + throw new ArgumentException($"Invalid year '{year}', must be at least 1."); + + // initialise + this.Day = day; + this.Season = season; + this.Year = year; + this.DayOfWeek = this.GetDayOfWeek(day); + this.DaysSinceStart = this.GetDaysSinceStart(day, season, year); + + } + + /// Get whether a date represents 0 spring Y1, which is the date during the in-game intro. + /// The day of month. + /// The season name. + /// The year. + private bool IsDayZero(int day, string season, int year) + { + return day == 0 && season == "spring" && year == 1; + } + + /// Get the day of week for a given date. + /// The day of month. + private DayOfWeek GetDayOfWeek(int day) + { + switch (day % 7) + { + 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; + } + } + + /// Get the number of days since the game began (starting at 1 for the first day of spring in Y1). + /// The day of month. + /// The season name. + /// The year. + private int GetDaysSinceStart(int day, string season, int year) + { + // return the number of days since 01 spring Y1 (inclusively) + int yearIndex = year - 1; + return + yearIndex * this.DaysInSeason * this.SeasonsInYear + + this.GetSeasonIndex(season) * this.DaysInSeason + + day; + } + + /// Get a season index. + /// The season name. + /// The current season wasn't recognised. + private int GetSeasonIndex(string season) + { + int index = Array.IndexOf(this.Seasons, season); + if (index == -1) + throw new InvalidOperationException($"The season '{season}' wasn't recognised."); + return index; + } + } +} diff --git a/src/StardewModdingAPI/icon.ico b/src/StardewModdingAPI/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..587a6e74864c94bd941f86113965473bf6bd6ce1 GIT binary patch literal 15086 zcmeHOd3;aD`aThnO%_s0HH1(!VoOqM8?}~NeyY9Yx^#1^O|+Vus;vmEEgBTDq=<-J zY*n?@zEiE#z6G^Z(4uHst-SYn=A8LDiG+k!?;pQ>K989*-k|*xja#CpdZ>0YtX(P(hVqgC+u~z=kdF9jpmcOmTNXzJR9^E zm*<8mxHTJ^TJ@jPM-jCfjPl9e4*k~xB5OVq+dcTD=~8&aKJo6cLyX}k#7IsTx5+2O zZ%nF`?Y>+fW0akTYe+GZNY~{vg{w(xl&(_VGLAos^UE_JT9wv+}Aqcb+DmuS}G-pQlUz zj^US#l9N2yv?wHKKgSi4r;Ih)g^1o0J%TT8t%^g>{T| z*A?gDbAKMqM)4f{Zy6+wJsNKCg=E6O+L7ZPuyXjJ3#t-7W z`KlP--W0dXx5R1l4RP6WMFRS*l3EQvRk~;U>_ES+RXZkc@E@}2RGFA{M_s?UCcd+d zNr|_=x>$M8m#GBm1Z8h$f5z|ru>XN2YxmkxBKD|Ms=rVI>h`1k*COn7bIQ}Eyn~1A zDE`zcDb?bN_*Rd&BRr>iEHey|2Laca0H6V& z*O?f#R-wT5mz(^&mUB!%G}e-mNQ*G<8WLGAuk9bSo{|?DDM-nq0>J#>2kken?IA1L zlKGd*hmHCG75tux<~TKmn(`ys77B@IWN}gBM_qp+uVcAaJ@gd`2;@`8cY~I{7xnkv ztKWP5jy||Q^{w>c%q9MBER)iWV&(CtW5l^ra*lZ9#mVbfzs!KO=>+rGo1VUxoZNm# zdp|(Rc3!OZt)RBkC8XH|a}VP_mJOx;y@KnYxUWc8U>@k3TmSZQ*U$)e=ytD0Q2oKw z;X(FM_JdVwuXBw%B5wV@%PN<95cWZ$;MyxnuG42{#C^zq#RcuD2lkb#JwRp|*U)ZZ zZndMu>*=0StZ9F#*lwa!Yo8?5T27Wy{WqF2CY%u?7UPabs@7F~(qz)UtI-ov{AGpVV(PUYtsO?9wusV%*h$!IfY8>~M{4vn8z88fiFehmv1s z$$5G5i;GgQ_t)YYv04gto@LtHt24xTz!C?%qP~!-2`8oazF)-oxHW!q*!7!|?KA0+ zVDFNeZ70V;zS?6o2aLM|RM2v*u^)A>-zVLx#aMZE$X4k%Jx!7i-;|-7F01~^j!6^GPtw%5Df{DPhh@Uf z3wNOR!oyOsL$Y`#9~5JsHU23-h(SOP1!w&ruKo84&XA|TVUgy*ar-okkO!`Dp!5N`8q1%<1?uO6RQGd)^)ywb86I`{?iLe8V%&CmS!QJ_;utzCHFN zoD&Jo1J8@Fks2)%vdlmG4D2vQ$r`u&x}==ArSe+;+WxrKurBdvi$SW+u+3K-@)N$x zP-W%U;EbkiFNZX{{h_~T%w2L&m#NZx&?@=I={-()4f_sR=uY}J!6e|mkJb^b#!seI(76hB~cD%X3J1idj=q4>m9RlnS!n?g{& z>-5v&)pHl^e;4FVvfIDi{y1i015=U8$wY2UHo4{-<+h*`UBVfD1BC@OUn=ENO)ATRC+BzLYt3~ux9aMVD1a7 z|5?bv33_ndc3GPBijj)bmaG1ij%~8}Tz6Q!)QuUV+WJSW)^nbH{gDRmO;ux#UUpvo zG3AKE&p0>O|0Jx*IMa5t{ZYoq7h^MQdZ+m==Jqpe{f1@h zS`T>da$lIH_~X55Fl6%_;jHIgr#L%5TPP%QB>}lhb-7Nhc2f4urD9C}K|E)j6z>J# zq2(Ygbn%wBUVvWKTo>1+>Ebi@xVY@gkir|&^|ErtRO`%Ie`Av9Q^J4 z5yL5q{VWW339t*=h8>aW9om|M9?JGZBVXaO;vX8R_@fUm_CYY89~*tpx{#R&2>4q* z3477@LEEwA!9UA0#-aG5Jb-*R7QU~{7pJJT_rw!z6L+(ZDzDDo z;WOvB5X`d-7@Aid=C>aDE~WKV?49*u3_~mkaTJyje%&{j??(C^9lsmH3A}5o)Xd{d zjQXP_==Hgdb35}a;~4VG%U&P;6$l#$Ctw#UEnhDt9DRqQ?{=20O0iyB#7H|K&pGlRMmy2AV&xgj-2<*!VCQez6#1Kdo3?1pMV|OW*RJ5X0lMDc z(O}?duVy2~x8)dVGGwDd`6>IvYvC#JnsY+MO&B|Ii92T61Y^ClZE{b+JGf;Jj&vsO zWErDk$}v#?dJY&vvg&&b+i&(gB-NA!dBOjh!PpS*1l2o~-OlZKtaa|@`WGRd4L=s2 zRtfS*yQwN>!`PuC4!Fa7C%)~I*>t@EccBh`7S}|?+~^C8v(s)AtlxB=S zmxd~{_ODi-ej^>Ai+Y?iK|#i93V@o7>tE=_y^+6?thL`~l*pR7H3PXu>cq~iI0aoeT95$GH-SLG+0 z5Z|U>5X@svoL5{H&)(lC1h<~LALG~sjL2#?=2^w_*^iVS8Ru>f?&PoI_>4K5HgsId zpH5Nhl)ibMA@_x+GG!6*eRd#rtI9NZ7;Ui}31{K+wQ{-#YS{_Gp~ z7AF7OK1(~w&R_FZDfD9UKQ4Fh=NZD{>ex5*cpy*n#~8BnSFsJh#ve}Ny>78)G4f>B z`3jAPZd1DIG~!}{;kXO}o7B3*V`oEDrAmzY%Xfp7bK@spGS zr_%6GJBsYg5Fgw{x$naMr*#aT^ksc7r2LxP8BdRg9LRHBd|usM7oV`_D#pP3YNCSn zWz4}~Wdn$F>NUMq-*t+A=b4so!WN!e0REOwslAl>W!7HH^Beb9g8Q#spEj;{=$HI) z_rg0EeGjARtigFnouMFOBgTSyMP?k)e!w&soI4Qib$c#yuisZZ!~03ofnO=Snz&y+ zSe`C@@GY5X2bm^+@*9D>FLjRzJGQ?2Qg>fTcqXVj>QKS7VJ|p?-_!$H;IfJ1#C_Ny z+}+^2D}s47H(Y$nJN9+ zq5sjir?BhS!Bum%IM~W7c3Z5@tB~!`A@?ZNPxij}O#M?=n1j2mpB#f;|H+@cxt6Q- zoV*oxKHK@@I|GMJnEG|-+fkl%4CHU;&b;Dpo1-j!DEqlPf35#2Zzf6W_9bkaPi-^We$@VJ zo!y$gwMgku-7{Hp&U+ru%uF=~t-truf06mP2i5nSy8QN>S$4*`LO;B-<-IAvJj?X? z$C|U;&Oho5(6D!k@NS&G$~#)3-{(i9`?ReRIc1B~>G`>dm#3KgTTa5dgKrP8_7pty ztGcV=Jea!vLz;ZCMIvU-mG&vit$DKea-Dbhgq6n_N^U)`>a$JBCFYvu6Yu!Ohca{I zZ^mXKrPIje%2)ZFL;EE2%*<~O)cjyy(&r5Q{(Jj7C)lT6KdDR2ty2&49+r2lx!Jd) zKE^Q%@B6&BRd=_zPleAiCS$$_=l=dnUK~TGxZC{AbMtW_f5y(ZuWFD#=Tq^w)&?~X zsn)@pLpDapI?Qat2wca z=WF;m>jO`8nWy>Hz?KS<5^ z73SUK&oesWpE#@B)<3w@KD5tngsoe#Q1WLV)CzxB|+GvG&rH6K5d z-|`?mVJlm}vf#Joi0AK_3Y7+aslNFMdk%H+-HAfuv52FN@_VX9{PSV0P!2F}^^@RMf=z3`)#?p{UxCFIC*4Irb31G8>CA0S@LAfslwkZX#Roh!$OYVvEDDg zytcz$jToL@gUyuxpFn>L;a_FHlrFtVN|sw}?(5t;L(Kc5yvq11>w!CCkmgy#+Dpja zzh$O51{;61udA{C{rA`4JPF9~sNOTfzw|m$sDBo`@&9RB-jved@83%BUCd|L6W=9v zqdvdaamU%^j_Wywx!CI tKdbLI?$zJJ{vAA({pd5cey8A^XjtdLKb^n%J&@r4MDg$c(}%5x{{m{<