From c8ce4aa3edae57b0d2b4be3333b68083805da2ba Mon Sep 17 00:00:00 2001 From: Matthew Messinger Date: Fri, 15 Jan 2016 02:49:17 -0500 Subject: [PATCH] Update LiteLoader --- .gitmodules | 3 + build.gradle | 38 +- gradle/wrapper/gradle-wrapper.properties | 2 +- liteloader/.factorypath | 3 + liteloader/.gitignore | 78 + liteloader/build.gradle | 290 ++++ liteloader/checkstyle.xml | 109 ++ liteloader/extra/formatter.xml | 295 ++++ liteloader/extra/liteloader.importorder | 7 + liteloader/gradle.properties | 10 + .../liteloader-1.8-SNAPSHOT-sources.jar | Bin 366656 -> 0 bytes .../liteloader-1.8-SNAPSHOT-srgnames.jar | Bin 647352 -> 0 bytes .../com/mumfrey/liteloader/ChatFilter.java | 27 + .../com/mumfrey/liteloader/ChatListener.java | 20 + .../liteloader/ChatRenderListener.java | 15 + .../liteloader/EntityRenderListener.java | 39 + .../liteloader/FrameBufferListener.java | 34 + .../mumfrey/liteloader/GameLoopListener.java | 18 + .../mumfrey/liteloader/HUDRenderListener.java | 13 + .../liteloader/InitCompleteListener.java | 24 + .../mumfrey/liteloader/JoinGameListener.java | 29 + .../liteloader/OutboundChatFilter.java | 17 + .../liteloader/OutboundChatListener.java | 20 + .../mumfrey/liteloader/PostLoginListener.java | 21 + .../liteloader/PostRenderListener.java | 23 + .../mumfrey/liteloader/PreRenderListener.java | 57 + .../mumfrey/liteloader/RenderListener.java | 36 + .../liteloader/ScreenshotListener.java | 28 + .../java/com/mumfrey/liteloader/Tickable.java | 22 + .../mumfrey/liteloader/ViewportListener.java | 10 + .../client/ClientPluginChannelsClient.java | 121 ++ .../liteloader/client/ClientProxy.java | 183 +++ .../liteloader/client/GameEngineClient.java | 159 ++ .../client/LiteLoaderCoreProviderClient.java | 111 ++ .../client/LiteLoaderEventBrokerClient.java | 567 ++++++++ .../client/LiteLoaderPanelManager.java | 294 ++++ .../liteloader/client/PacketEventsClient.java | 262 ++++ .../liteloader/client/ResourceObserver.java | 117 ++ .../liteloader/client/ResourcesClient.java | 95 ++ .../client/SoundHandlerReloadInhibitor.java | 129 ++ .../mumfrey/liteloader/client/Translator.java | 30 + .../api/LiteLoaderBrandingProvider.java | 141 ++ .../client/api/LiteLoaderCoreAPIClient.java | 161 ++ .../api/LiteLoaderModInfoDecorator.java | 138 ++ .../client/api/ObjectFactoryClient.java | 164 +++ .../client/ducks/IClientNetLoginHandler.java | 8 + .../liteloader/client/ducks/IFramebuffer.java | 8 + .../client/ducks/INamespacedRegistry.java | 6 + .../client/ducks/IObjectIntIdentityMap.java | 11 + .../client/ducks/IRegistrySimple.java | 8 + .../liteloader/client/ducks/IReloadable.java | 10 + .../client/ducks/IRenderManager.java | 11 + .../ducks/ITileEntityRendererDispatcher.java | 11 + .../liteloader/client/gui/GuiCheckbox.java | 51 + .../liteloader/client/gui/GuiHoverLabel.java | 55 + .../client/gui/GuiLiteLoaderPanel.java | 815 +++++++++++ .../liteloader/client/gui/GuiPanel.java | 209 +++ .../liteloader/client/gui/GuiPanelAbout.java | 251 ++++ .../client/gui/GuiPanelConfigContainer.java | 274 ++++ .../liteloader/client/gui/GuiPanelError.java | 161 ++ .../client/gui/GuiPanelLiteLoaderLog.java | 406 ++++++ .../liteloader/client/gui/GuiPanelMods.java | 295 ++++ .../client/gui/GuiPanelSettings.java | 152 ++ .../client/gui/GuiPanelUpdateCheck.java | 232 +++ .../liteloader/client/gui/GuiScrollPanel.java | 208 +++ .../client/gui/GuiSimpleScrollBar.java | 153 ++ .../client/gui/ScrollPanelContent.java | 14 + .../client/gui/modlist/GuiModInfoPanel.java | 155 ++ .../client/gui/modlist/GuiModListPanel.java | 271 ++++ .../gui/modlist/GuiModListPanelInvalid.java | 53 + .../client/gui/modlist/ModList.java | 273 ++++ .../client/gui/modlist/ModListContainer.java | 18 + .../client/gui/modlist/ModListEntry.java | 334 +++++ .../client/gui/startup/LoadingBar.java | 440 ++++++ .../client/mixin/MixinEntityPlayerSP.java | 26 + .../client/mixin/MixinEntityRenderer.java | 115 ++ .../client/mixin/MixinFramebuffer.java | 43 + .../client/mixin/MixinGuiIngame.java | 39 + .../client/mixin/MixinIntegratedServer.java | 39 + .../client/mixin/MixinMinecraft.java | 85 ++ .../mixin/MixinNetHandlerLoginClient.java | 21 + .../mixin/MixinObjectIntIdentityMap.java | 32 + .../client/mixin/MixinRealmsMainScreen.java | 26 + .../client/mixin/MixinRegistryNamespaced.java | 23 + .../client/mixin/MixinRegistrySimple.java | 23 + .../client/mixin/MixinRenderManager.java | 39 + .../client/mixin/MixinScreenShotHelper.java | 32 + .../liteloader/client/mixin/MixinSession.java | 31 + .../MixinSimpleReloadableResourceManager.java | 23 + .../MixinTileEntityRendererDispatcher.java | 24 + .../client/overlays/IEntityRenderer.java | 29 + .../client/overlays/IGuiTextField.java | 36 + .../client/overlays/IMinecraft.java | 60 + .../client/overlays/ISoundHandler.java | 14 + .../transformers/CrashReportTransformer.java | 75 + .../transformers/MinecraftTransformer.java | 91 ++ .../client/util/PrivateFieldsClient.java | 22 + .../client/util/render/IconAbsolute.java | 128 ++ .../util/render/IconAbsoluteClickable.java | 20 + .../client/util/render/IconTiled.java | 140 ++ .../java/com/mumfrey/liteloader/gl/GL.java | 1296 +++++++++++++++++ .../liteloader/gl/GLClippingPlanes.java | 193 +++ .../resources/InternalResourcePack.java | 119 ++ .../liteloader/resources/ModResourcePack.java | 59 + .../resources/ModResourcePackDir.java | 59 + .../mumfrey/liteloader/util/InputManager.java | 361 +++++ .../mumfrey/liteloader/util/ModUtilities.java | 253 ++++ .../resources/mixins.liteloader.client.json | 23 + .../liteloader/debug/LoginManager.java | 458 ++++++ .../mumfrey/liteloader/debug/LoginPanel.java | 366 +++++ .../com/mumfrey/liteloader/debug/Start.java | 204 +++ .../debug/resources/obfuscation.properties | 69 + .../com/mumfrey/liteloader/Configurable.java | 20 + .../java/com/mumfrey/liteloader/LiteMod.java | 40 + .../com/mumfrey/liteloader/PacketHandler.java | 32 + .../com/mumfrey/liteloader/Permissible.java | 55 + .../liteloader/PlayerInteractionListener.java | 58 + .../liteloader/PlayerMoveListener.java | 27 + .../liteloader/PluginChannelListener.java | 22 + .../liteloader/PreJoinGameListener.java | 24 + .../java/com/mumfrey/liteloader/Priority.java | 22 + .../mumfrey/liteloader/ServerChatFilter.java | 22 + .../liteloader/ServerCommandProvider.java | 21 + .../liteloader/ServerPlayerListener.java | 51 + .../ServerPluginChannelListener.java | 25 + .../mumfrey/liteloader/ServerTickable.java | 18 + .../mumfrey/liteloader/ShutdownListener.java | 13 + .../liteloader/api/BrandingProvider.java | 116 ++ .../liteloader/api/ContainerRegistry.java | 98 ++ .../mumfrey/liteloader/api/CoreProvider.java | 63 + .../liteloader/api/CustomisationProvider.java | 11 + .../liteloader/api/EnumerationObserver.java | 64 + .../liteloader/api/EnumeratorModule.java | 68 + .../liteloader/api/EnumeratorPlugin.java | 37 + .../liteloader/api/GenericObserver.java | 13 + .../liteloader/api/InterfaceObserver.java | 11 + .../liteloader/api/InterfaceProvider.java | 32 + .../com/mumfrey/liteloader/api/Listener.java | 22 + .../com/mumfrey/liteloader/api/LiteAPI.java | 120 ++ .../liteloader/api/MixinConfigProvider.java | 27 + .../liteloader/api/ModClassValidator.java | 14 + .../liteloader/api/ModInfoDecorator.java | 53 + .../liteloader/api/ModLoadObserver.java | 65 + .../com/mumfrey/liteloader/api/Observer.java | 15 + .../liteloader/api/PostRenderObserver.java | 14 + .../liteloader/api/ShutdownObserver.java | 15 + .../mumfrey/liteloader/api/TickObserver.java | 13 + .../liteloader/api/TranslationProvider.java | 21 + .../mumfrey/liteloader/api/WorldObserver.java | 16 + .../api/exceptions/APIException.java | 27 + .../api/exceptions/InvalidAPIException.java | 16 + .../exceptions/InvalidAPIStateException.java | 11 + .../exceptions/InvalidProviderException.java | 16 + .../liteloader/api/manager/APIAdapter.java | 86 ++ .../liteloader/api/manager/APIProvider.java | 50 + .../api/manager/APIProviderBasic.java | 340 +++++ .../liteloader/api/manager/APIRegistry.java | 182 +++ .../mumfrey/liteloader/common/GameEngine.java | 78 + .../liteloader/common/LoadingProgress.java | 66 + .../mumfrey/liteloader/common/Resources.java | 28 + .../liteloader/common/ducks/IChatPacket.java | 10 + .../common/ducks/IPacketClientSettings.java | 6 + .../mixin/MixinC15PacketClientSettings.java | 20 + .../common/mixin/MixinItemInWorldManager.java | 42 + .../common/mixin/MixinMinecraftServer.java | 20 + .../mixin/MixinNetHandlerPlayServer.java | 91 ++ .../common/mixin/MixinS02PacketChat.java | 27 + .../MixinServerConfigurationManager.java | 68 + .../LiteLoaderPacketTransformer.java | 24 + .../common/transformers/PacketEvent.java | 65 + .../common/transformers/PacketEventInfo.java | 23 + .../liteloader/core/BadContainerInfo.java | 64 + .../liteloader/core/ClientPluginChannels.java | 196 +++ .../core/CommonPluginChannelListener.java | 22 + .../mumfrey/liteloader/core/Containers.java | 172 +++ .../liteloader/core/EnabledModsList.java | 252 ++++ .../mumfrey/liteloader/core/IEventState.java | 9 + .../core/InterfaceRegistrationDelegate.java | 93 ++ .../mumfrey/liteloader/core/LiteLoader.java | 1023 +++++++++++++ .../liteloader/core/LiteLoaderBootstrap.java | 775 ++++++++++ .../liteloader/core/LiteLoaderEnumerator.java | 832 +++++++++++ .../core/LiteLoaderEventBroker.java | 548 +++++++ .../core/LiteLoaderInterfaceManager.java | 526 +++++++ .../liteloader/core/LiteLoaderMods.java | 795 ++++++++++ .../liteloader/core/LiteLoaderUpdateSite.java | 171 +++ .../liteloader/core/LiteLoaderVersion.java | 138 ++ .../java/com/mumfrey/liteloader/core/Mod.java | 205 +++ .../com/mumfrey/liteloader/core/ModInfo.java | 230 +++ .../com/mumfrey/liteloader/core/NonMod.java | 57 + .../mumfrey/liteloader/core/PacketEvents.java | 317 ++++ .../liteloader/core/PlayerEventState.java | 139 ++ .../liteloader/core/PluginChannels.java | 237 +++ .../com/mumfrey/liteloader/core/Proxy.java | 140 ++ .../liteloader/core/ServerPluginChannels.java | 262 ++++ .../core/api/DefaultClassValidator.java | 48 + .../core/api/DefaultEnumeratorPlugin.java | 190 +++ .../core/api/EnumeratorModuleClassPath.java | 143 ++ .../core/api/EnumeratorModuleFolder.java | 411 ++++++ .../core/api/LiteLoaderCoreAPI.java | 176 +++ .../core/api/LoadableModClassPath.java | 91 ++ .../liteloader/core/api/LoadableModFile.java | 598 ++++++++ .../liteloader/core/event/Cancellable.java | 28 + .../event/EventCancellationException.java | 25 + .../liteloader/core/event/EventProxy.java | 212 +++ .../liteloader/core/event/HandlerList.java | 1112 ++++++++++++++ .../core/event/IHandlerListDecorator.java | 63 + .../core/event/ProfilingHandlerList.java | 237 +++ .../exceptions/OutdatedLoaderException.java | 21 + .../ProfilerCrossThreadAccessException.java | 18 + .../ProfilerStackCorruptionException.java | 17 + .../UnregisteredChannelException.java | 11 + .../liteloader/core/runtime/Methods.java | 91 ++ .../mumfrey/liteloader/core/runtime/Obf.java | 432 ++++++ .../liteloader/core/runtime/Packets.java | 321 ++++ .../crashreport/CallableLaunchWrapper.java | 47 + .../crashreport/CallableLiteLoaderBrand.java | 35 + .../crashreport/CallableLiteLoaderMods.java | 33 + .../liteloader/interfaces/FastIterable.java | 29 + .../interfaces/FastIterableDeque.java | 14 + .../liteloader/interfaces/Injectable.java | 40 + .../interfaces/InterfaceRegistry.java | 20 + .../liteloader/interfaces/Loadable.java | 100 ++ .../liteloader/interfaces/LoadableFile.java | 507 +++++++ .../liteloader/interfaces/LoadableMod.java | 355 +++++ .../interfaces/LoaderEnumerator.java | 97 ++ .../liteloader/interfaces/MixinContainer.java | 42 + .../interfaces/ModularEnumerator.java | 56 + .../liteloader/interfaces/ObjectFactory.java | 45 + .../liteloader/interfaces/PanelManager.java | 86 ++ .../liteloader/interfaces/TweakContainer.java | 47 + .../liteloader/launch/ClassPathUtilities.java | 478 ++++++ .../launch/ClassTransformerManager.java | 235 +++ .../liteloader/launch/GameEnvironment.java | 28 + .../liteloader/launch/InjectionStrategy.java | 145 ++ .../launch/InvalidTransformerException.java | 25 + .../launch/LiteLoaderTransformer.java | 65 + .../liteloader/launch/LiteLoaderTweaker.java | 692 +++++++++ .../launch/LiteLoaderTweakerServer.java | 42 + .../liteloader/launch/LoaderBootstrap.java | 48 + .../liteloader/launch/LoaderEnvironment.java | 83 ++ .../liteloader/launch/LoaderProperties.java | 103 ++ .../launch/NonDelegatingClassLoader.java | 145 ++ .../liteloader/launch/StartupEnvironment.java | 235 +++ .../mumfrey/liteloader/messaging/Message.java | 184 +++ .../liteloader/messaging/MessageBus.java | 269 ++++ .../liteloader/messaging/Messenger.java | 62 + .../modconfig/AdvancedExposable.java | 39 + .../liteloader/modconfig/ConfigManager.java | 234 +++ .../liteloader/modconfig/ConfigPanel.java | 95 ++ .../liteloader/modconfig/ConfigPanelHost.java | 42 + .../liteloader/modconfig/ConfigStrategy.java | 33 + .../liteloader/modconfig/Exposable.java | 10 + .../modconfig/ExposableConfigWriter.java | 305 ++++ .../modconfig/ExposableOptions.java | 32 + .../permissions/LocalPermissions.java | 24 + .../permissions/PermissibleAllMods.java | 70 + .../liteloader/permissions/Permission.java | 219 +++ .../liteloader/permissions/Permissions.java | 38 + .../permissions/PermissionsManager.java | 51 + .../permissions/PermissionsManagerClient.java | 526 +++++++ .../permissions/PermissionsManagerServer.java | 66 + .../permissions/ReplicatedPermissions.java | 32 + .../permissions/ServerPermissions.java | 154 ++ .../liteloader/transformers/AppendInsns.java | 19 + .../transformers/ByteCodeUtilities.java | 864 +++++++++++ .../liteloader/transformers/Callback.java | 281 ++++ .../CallbackInjectionTransformer.java | 361 +++++ .../transformers/ClassOverlayTransformer.java | 537 +++++++ .../transformers/ClassTransformer.java | 63 + .../InjectedCallbackCollisionError.java | 26 + .../transformers/InvalidOverlayException.java | 25 + .../transformers/IsolatedClassWriter.java | 22 + .../liteloader/transformers/ObfProvider.java | 21 + .../liteloader/transformers/Obfuscated.java | 19 + .../transformers/PacketHandlerException.java | 30 + .../mumfrey/liteloader/transformers/Stub.java | 18 + .../transformers/access/Accessor.java | 19 + .../access/AccessorTransformer.java | 584 ++++++++ .../transformers/access/Invoker.java | 18 + .../transformers/access/ObfTableClass.java | 21 + .../liteloader/transformers/event/Event.java | 728 +++++++++ .../event/EventAlreadyInjectedException.java | 21 + .../transformers/event/EventInfo.java | 133 ++ .../event/EventInjectionTransformer.java | 114 ++ .../event/EventProxyTransformer.java | 77 + .../transformers/event/EventTransformer.java | 417 ++++++ .../transformers/event/InjectionPoint.java | 331 +++++ .../liteloader/transformers/event/Jump.java | 54 + .../transformers/event/MethodInfo.java | 375 +++++ .../transformers/event/ReadOnlyInsnList.java | 147 ++ .../transformers/event/ReturnEventInfo.java | 140 ++ .../event/inject/BeforeFieldAccess.java | 99 ++ .../event/inject/BeforeInvoke.java | 387 +++++ .../transformers/event/inject/BeforeNew.java | 83 ++ .../event/inject/BeforeReturn.java | 61 + .../event/inject/BeforeStringInvoke.java | 77 + .../event/inject/JumpInsnPoint.java | 69 + .../transformers/event/inject/MethodHead.java | 28 + .../event/json/InvalidEventJsonException.java | 25 + .../event/json/JsonDescriptor.java | 93 ++ .../transformers/event/json/JsonEvent.java | 159 ++ .../transformers/event/json/JsonEvents.java | 205 +++ .../event/json/JsonInjection.java | 188 +++ .../event/json/JsonInjectionShiftType.java | 7 + .../event/json/JsonInjectionType.java | 11 + .../transformers/event/json/JsonMethods.java | 87 ++ .../transformers/event/json/JsonObf.java | 61 + .../event/json/JsonObfuscationTable.java | 182 +++ .../json/ModEventInjectionTransformer.java | 87 ++ .../transformers/event/json/ModEvents.java | 104 ++ .../mumfrey/liteloader/update/UpdateSite.java | 421 ++++++ .../liteloader/util/ChatUtilities.java | 132 ++ .../liteloader/util/EntityUtilities.java | 29 + .../com/mumfrey/liteloader/util/Input.java | 93 ++ .../mumfrey/liteloader/util/InputEvent.java | 136 ++ .../mumfrey/liteloader/util/InputHandler.java | 36 + .../liteloader/util/ObfuscationUtilities.java | 87 ++ .../com/mumfrey/liteloader/util/Position.java | 58 + .../liteloader/util/PrivateFields.java | 125 ++ .../liteloader/util/SortableValue.java | 46 + .../util/jinput/ComponentRegistry.java | 285 ++++ .../liteloader/util/log/LiteLoaderLogger.java | 314 ++++ .../util/net/HttpStringRetriever.java | 232 +++ .../util/net/LiteLoaderLogUpload.java | 126 ++ .../liteloader/util/net/PastebinUpload.java | 135 ++ .../mumfrey/liteloader/util/render/Icon.java | 14 + .../liteloader/util/render/IconClickable.java | 16 + .../liteloader/util/render/IconTextured.java | 31 + .../ReplicatedPermissionsContainer.java | 129 ++ .../assets/liteloader/lang/en_US.lang | 89 ++ .../assets/liteloader/lang/pt_BR.lang | 67 + .../assets/liteloader/textures/gui/about.png | Bin 0 -> 43567 bytes .../src/main/resources/liteloader.properties | 5 + .../resources/mixins.liteloader.core.json | 14 + settings.gradle | 2 +- voxellib/build.gradle | 2 +- .../common/runtime/PrivateClasses.java | 6 +- .../common/runtime/PrivateFields.java | 5 +- .../common/runtime/PrivateMethods.java | 5 +- .../common/runtime/Reflection.java | 9 +- 340 files changed, 44115 insertions(+), 25 deletions(-) create mode 100644 .gitmodules create mode 100644 liteloader/.factorypath create mode 100644 liteloader/.gitignore create mode 100644 liteloader/build.gradle create mode 100644 liteloader/checkstyle.xml create mode 100644 liteloader/extra/formatter.xml create mode 100644 liteloader/extra/liteloader.importorder create mode 100644 liteloader/gradle.properties delete mode 100644 liteloader/liteloader-1.8-SNAPSHOT-sources.jar delete mode 100644 liteloader/liteloader-1.8-SNAPSHOT-srgnames.jar create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/ChatFilter.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/ChatListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/ChatRenderListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/EntityRenderListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/FrameBufferListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/GameLoopListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/HUDRenderListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/InitCompleteListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/JoinGameListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/OutboundChatFilter.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/OutboundChatListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/PostLoginListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/PostRenderListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/PreRenderListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/RenderListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/ScreenshotListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/Tickable.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/ViewportListener.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ClientPluginChannelsClient.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ClientProxy.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/GameEngineClient.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderCoreProviderClient.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderEventBrokerClient.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderPanelManager.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/PacketEventsClient.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ResourceObserver.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ResourcesClient.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/SoundHandlerReloadInhibitor.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/Translator.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderBrandingProvider.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderCoreAPIClient.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderModInfoDecorator.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/api/ObjectFactoryClient.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IClientNetLoginHandler.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IFramebuffer.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/INamespacedRegistry.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IObjectIntIdentityMap.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IRegistrySimple.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IReloadable.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IRenderManager.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/ITileEntityRendererDispatcher.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiCheckbox.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiHoverLabel.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiLiteLoaderPanel.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanel.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelAbout.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelConfigContainer.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelError.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelLiteLoaderLog.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelMods.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelSettings.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelUpdateCheck.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiScrollPanel.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiSimpleScrollBar.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/ScrollPanelContent.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModInfoPanel.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModListPanel.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModListPanelInvalid.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModList.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModListContainer.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModListEntry.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/gui/startup/LoadingBar.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinEntityPlayerSP.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinEntityRenderer.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinFramebuffer.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinGuiIngame.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinIntegratedServer.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinMinecraft.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinNetHandlerLoginClient.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinObjectIntIdentityMap.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRealmsMainScreen.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRegistryNamespaced.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRegistrySimple.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRenderManager.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinScreenShotHelper.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinSession.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinSimpleReloadableResourceManager.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinTileEntityRendererDispatcher.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IEntityRenderer.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IGuiTextField.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IMinecraft.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/ISoundHandler.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/transformers/CrashReportTransformer.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/transformers/MinecraftTransformer.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/util/PrivateFieldsClient.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconAbsolute.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconAbsoluteClickable.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconTiled.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/gl/GL.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/gl/GLClippingPlanes.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/resources/InternalResourcePack.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/resources/ModResourcePack.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/resources/ModResourcePackDir.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/util/InputManager.java create mode 100644 liteloader/src/client/java/com/mumfrey/liteloader/util/ModUtilities.java create mode 100644 liteloader/src/client/resources/mixins.liteloader.client.json create mode 100644 liteloader/src/debug/java/com/mumfrey/liteloader/debug/LoginManager.java create mode 100644 liteloader/src/debug/java/com/mumfrey/liteloader/debug/LoginPanel.java create mode 100644 liteloader/src/debug/java/com/mumfrey/liteloader/debug/Start.java create mode 100644 liteloader/src/debug/resources/obfuscation.properties create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/Configurable.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/LiteMod.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/PacketHandler.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/Permissible.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/PlayerInteractionListener.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/PlayerMoveListener.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/PluginChannelListener.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/PreJoinGameListener.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/Priority.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/ServerChatFilter.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/ServerCommandProvider.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/ServerPlayerListener.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/ServerPluginChannelListener.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/ServerTickable.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/ShutdownListener.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/BrandingProvider.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/ContainerRegistry.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/CoreProvider.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/CustomisationProvider.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/EnumerationObserver.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/EnumeratorModule.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/EnumeratorPlugin.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/GenericObserver.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/InterfaceObserver.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/InterfaceProvider.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/Listener.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/LiteAPI.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/MixinConfigProvider.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/ModClassValidator.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/ModInfoDecorator.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/ModLoadObserver.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/Observer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/PostRenderObserver.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/ShutdownObserver.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/TickObserver.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/TranslationProvider.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/WorldObserver.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/APIException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidAPIException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidAPIStateException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidProviderException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIAdapter.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIProvider.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIProviderBasic.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIRegistry.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/GameEngine.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/LoadingProgress.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/Resources.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/ducks/IChatPacket.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/ducks/IPacketClientSettings.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinC15PacketClientSettings.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinItemInWorldManager.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinMinecraftServer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinNetHandlerPlayServer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinS02PacketChat.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinServerConfigurationManager.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/LiteLoaderPacketTransformer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/PacketEvent.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/PacketEventInfo.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/BadContainerInfo.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/ClientPluginChannels.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/CommonPluginChannelListener.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/Containers.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/EnabledModsList.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/IEventState.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/InterfaceRegistrationDelegate.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoader.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderBootstrap.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderEnumerator.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderEventBroker.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderInterfaceManager.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderMods.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderUpdateSite.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderVersion.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/Mod.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/ModInfo.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/NonMod.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/PacketEvents.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/PlayerEventState.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/PluginChannels.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/Proxy.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/ServerPluginChannels.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/api/DefaultClassValidator.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/api/DefaultEnumeratorPlugin.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/api/EnumeratorModuleClassPath.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/api/EnumeratorModuleFolder.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/api/LiteLoaderCoreAPI.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/api/LoadableModClassPath.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/api/LoadableModFile.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/event/Cancellable.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/event/EventCancellationException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/event/EventProxy.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/event/HandlerList.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/event/IHandlerListDecorator.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/event/ProfilingHandlerList.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/OutdatedLoaderException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/ProfilerCrossThreadAccessException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/ProfilerStackCorruptionException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/UnregisteredChannelException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Methods.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Obf.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Packets.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLaunchWrapper.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLiteLoaderBrand.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLiteLoaderMods.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/FastIterable.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/FastIterableDeque.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/Injectable.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/InterfaceRegistry.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/Loadable.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoadableFile.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoadableMod.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoaderEnumerator.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/MixinContainer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/ModularEnumerator.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/ObjectFactory.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/PanelManager.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/interfaces/TweakContainer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/ClassPathUtilities.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/ClassTransformerManager.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/GameEnvironment.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/InjectionStrategy.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/InvalidTransformerException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTransformer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTweaker.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTweakerServer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderBootstrap.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderEnvironment.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderProperties.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/NonDelegatingClassLoader.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/launch/StartupEnvironment.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/messaging/Message.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/messaging/MessageBus.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/messaging/Messenger.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/modconfig/AdvancedExposable.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigManager.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigPanel.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigPanelHost.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigStrategy.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/modconfig/Exposable.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ExposableConfigWriter.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ExposableOptions.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/permissions/LocalPermissions.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissibleAllMods.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/permissions/Permission.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/permissions/Permissions.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManager.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManagerClient.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManagerServer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/permissions/ReplicatedPermissions.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/permissions/ServerPermissions.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/AppendInsns.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/ByteCodeUtilities.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/Callback.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/CallbackInjectionTransformer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/ClassOverlayTransformer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/ClassTransformer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/InjectedCallbackCollisionError.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/InvalidOverlayException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/IsolatedClassWriter.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/ObfProvider.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/Obfuscated.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/PacketHandlerException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/Stub.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/Accessor.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/AccessorTransformer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/Invoker.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/ObfTableClass.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/Event.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventAlreadyInjectedException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventInfo.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventInjectionTransformer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventProxyTransformer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventTransformer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/InjectionPoint.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/Jump.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/MethodInfo.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/ReadOnlyInsnList.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/ReturnEventInfo.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeFieldAccess.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeInvoke.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeNew.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeReturn.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeStringInvoke.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/JumpInsnPoint.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/MethodHead.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/InvalidEventJsonException.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonDescriptor.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonEvent.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonEvents.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjection.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjectionShiftType.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjectionType.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonMethods.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonObf.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonObfuscationTable.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/ModEventInjectionTransformer.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/ModEvents.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/update/UpdateSite.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/ChatUtilities.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/EntityUtilities.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/Input.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/InputEvent.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/InputHandler.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/ObfuscationUtilities.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/Position.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/PrivateFields.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/SortableValue.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/jinput/ComponentRegistry.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/log/LiteLoaderLogger.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/net/HttpStringRetriever.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/net/LiteLoaderLogUpload.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/net/PastebinUpload.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/render/Icon.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/render/IconClickable.java create mode 100644 liteloader/src/main/java/com/mumfrey/liteloader/util/render/IconTextured.java create mode 100644 liteloader/src/main/java/net/eq2online/permissions/ReplicatedPermissionsContainer.java create mode 100644 liteloader/src/main/resources/assets/liteloader/lang/en_US.lang create mode 100644 liteloader/src/main/resources/assets/liteloader/lang/pt_BR.lang create mode 100644 liteloader/src/main/resources/assets/liteloader/textures/gui/about.png create mode 100644 liteloader/src/main/resources/liteloader.properties create mode 100644 liteloader/src/main/resources/mixins.liteloader.core.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..7087613c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "LiteLoader"] + path = LiteLoader + url = ./LiteLoader diff --git a/build.gradle b/build.gradle index 49b39492..be844a38 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,23 @@ -plugins { - id 'net.minecraftforge.gradle.tweaker-client' version '2.0.2' +buildscript { + repositories { + jcenter() + maven { + name 'forge' + url 'http://files.minecraftforge.net/maven' + } + } + dependencies { + classpath 'net.minecraftforge.gradle:ForgeGradle:2.0-SNAPSHOT' + } } +plugins { +} + ext.voxellib = project ':voxellib' ext.revision = 186 +apply plugin: 'net.minecraftforge.gradle.tweaker-client' + archivesBaseName = "MineLittlePony" group = 'com.brohoof.minelp' version = '1.8' @@ -31,6 +45,7 @@ sourceSets { } project('forge') { apply plugin: 'net.minecraftforge.gradle.forge' + version = '0' minecraft { version = '1.8-11.14.3.1543' mappings = rootProject.minecraft.mappings @@ -41,7 +56,10 @@ project('forge') { provided rootProject } } - +project('LiteLoader'){ + apply plugin: 'mnm.gradle.ap-ide' + mcMappings = rootProject.minecraft.mappings +} processResources { def props = [ version: version, @@ -57,12 +75,16 @@ processResources { exclude 'litemod.json' } } -repositories.flatDir { - dir 'liteloader' -} +allprojects {repositories{ + maven { + name 'sponge' + url 'http://repo.spongepowered.org/maven' + } +}} dependencies { - deobfProvided 'com.mumfrey:liteloader:1.8-SNAPSHOT:srgnames' + provided project('LiteLoader') provided voxellib + compile sourceSets.common.output compile sourceSets.hdskins.output hdskinsCompile sourceSets.common.output @@ -83,7 +105,7 @@ task standaloneJar(type: Jar, dependsOn: [{voxellib.reobfObfJar}, {project('forg artifacts { archives standaloneJar } -reobf{ +reobf { jar.task.enabled = false standaloneJar{} } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09e711f1..cf3a9673 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-bin.zip diff --git a/liteloader/.factorypath b/liteloader/.factorypath new file mode 100644 index 00000000..470f864b --- /dev/null +++ b/liteloader/.factorypath @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/liteloader/.gitignore b/liteloader/.gitignore new file mode 100644 index 00000000..63e8c15f --- /dev/null +++ b/liteloader/.gitignore @@ -0,0 +1,78 @@ +# Build # +######### +MANIFEST.MF +dependency-reduced-pom.xml + +# Compiled # +############ +bin +build +dist +lib +out +run +target +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Databases # +############# +*.db +*.sql +*.sqlite + +# Packages # +############ +*.7z +*.dmg +*.gz +*.iso +*.rar +*.tar +*.zip + +# Repository # +############## +.git + +# Logging # +########### +/logs +*.log + +# Misc # +######## +*.bak + +# System # +########## +.DS_Store +ehthumbs.db +Thumbs.db +*.bat +*.sh + +# Project # +########### +.checkstyle +.classpath +.externalToolBuilders +.gradle +.nb-gradle +.idea +.project +.settings +eclipse +nbproject +atlassian-ide-plugin.xml +build.xml +nb-configuration.xml +*.iml +*.ipr +*.iws +*.launch + diff --git a/liteloader/build.gradle b/liteloader/build.gradle new file mode 100644 index 00000000..58bbfffe --- /dev/null +++ b/liteloader/build.gradle @@ -0,0 +1,290 @@ +buildscript { + repositories { + jcenter() + mavenLocal() + mavenCentral() + maven { + name = "forge" + url = "http://files.minecraftforge.net/maven" + } + maven { + name = "sonatype" + url = "https://oss.sonatype.org/content/repositories/snapshots/" + } + maven { + name = 'sponge' + url = 'http://repo.spongepowered.org/maven' + } + } + dependencies { + classpath 'net.minecraftforge.gradle:ForgeGradle:2.0-SNAPSHOT' + classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.0' + classpath 'org.spongepowered:mixingradle:0.1-SNAPSHOT' + } +} + +apply plugin: 'net.minecraftforge.gradle.tweaker-client' +apply plugin: 'checkstyle' +apply plugin: 'maven' +apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'org.spongepowered.mixin' + +// Default tasks +defaultTasks 'build' + +ext { + // Artefact details + buildNumber = project.hasProperty("buildNumber") ? buildNumber : '0' + buildVersion = project.hasProperty("buildVersion") ? buildVersion : '0.0' + ciSystem = project.hasProperty("ciSystem") ? ciSystem : 'unknown' + commit = project.hasProperty("commit") ? commit : 'unknown' + classifier = project.hasProperty("buildType") ? buildType : 'SNAPSHOT' + isReleaseBuild = "RELEASE".equals(project.classifier.toUpperCase()) + mavenRepo = project.isReleaseBuild ? "mavenUrl" : "mavenSnapshotUrl" + + // Extended project information + projectName = 'LiteLoader' + inceptionYear = '2012' + packaging = 'jar' + + startClass = 'com.mumfrey.liteloader.debug.Start' + tweakClass = 'com.mumfrey.liteloader.launch.LiteLoaderTweaker' +} + +// Basic project information +group = "com.mumfrey" +archivesBaseName = "liteloader" +version = buildVersion + (project.isReleaseBuild ? '' : '-' + project.classifier) + +// Minimum version of Java required +sourceCompatibility = '1.6' +targetCompatibility = '1.6' + +repositories { + maven { + name = 'sponge' + url = 'https://repo.spongepowered.org/maven/' + } +} + +dependencies { + compile 'org.spongepowered:mixin:0.4.11-SNAPSHOT' + compile 'com.google.guava:guava:17.0' + compile 'com.google.code.gson:gson:2.2.4' +} + +minecraft { + version = project.mcVersion + mappings = project.mcMappings + runDir = "run" + tweakClass = project.tweakClass +} + +sourceSets { + main { + refMap = "mixins.liteloader.core.refmap.json" + } + client { + compileClasspath += main.compileClasspath + main.output + //refMap = "mixins.liteloader.client.refmap.json" + } + debug { + compileClasspath += client.compileClasspath + client.output + } +} + +mixin { + defaultObfuscationEnv notch +} + +checkstyle { + configProperties = [ + "name" : project.name, + "organization": project.organization, + "url" : project.url, + "year" : project.inceptionYear + ] + configFile = file("checkstyle.xml") + toolVersion = '6.13' +} + +javadoc { + source sourceSets.client.allJava + source sourceSets.debug.allJava +} + +afterEvaluate { + logger.lifecycle '=================================================' + logger.lifecycle ' LiteLoader' + logger.lifecycle ' Copyright (C) 2011-2016 Adam Mummery-Smith' + logger.lifecycle ' Running in {} mode', (project.isReleaseBuild ? "RELEASE" : "SNAPSHOT") + logger.lifecycle '=================================================' + + makeEclipseCleanRunClient { + arguments = "" + jvmArguments = "-Dliteloader.debug=true -Dmixin.debug.verbose=true -Dmixin.debug.verify=true" + } + + // hacks for run configs + def mc = plugins.getPlugin 'net.minecraftforge.gradle.tweaker-client' + mc.replacer.putReplacement '{RUN_CLIENT_MAIN}', project.startClass + mc.replacer.putReplacement '{RUN_CLIENT_TWEAKER}', minecraft.tweakClass +} + +// manifest entries for all jars +def jarManifest = { + mainAttributes ( + 'Built-By': System.properties['user.name'], + 'Created-By': System.properties['java.vm.version'] + " (" + System.properties['java.vm.vendor'] + ")", + 'Implementation-Title': name, + 'Implementation-Version': version + "+" + ciSystem + "-b" + buildNumber + ".git-" + commit, + 'Implementation-Vendor': url + ) +} + +jar { + doFirst { + // Seriously forge? + ant.replace( + file: sourceSets.main.refMapFile, + token: "func_72355_a(Lnet/minecraft/network/NetworkManager;Lnet/minecraft/entity/player/EntityPlayerMP;)V", + value: "initializeConnectionToPlayer(Lnet/minecraft/network/NetworkManager;Lnet/minecraft/entity/player/EntityPlayerMP;Lnet/minecraft/network/NetHandlerPlayServer;)V" + ) + } + from sourceSets.client.output + from sourceSets.debug.output + manifest jarManifest +} + +task releaseJar(type: Jar) { + from sourceSets.main.output + from sourceSets.client.output + + manifest jarManifest + classifier = 'staging' +} + +shadowJar { + manifest jarManifest + dependsOn 'reobfReleaseJar' + + from sourceSets.main.output + from sourceSets.client.output + + exclude 'META-INF/*.DSA' + exclude 'META-INF/*.RSA' + exclude 'dummyThing' + exclude 'LICENSE.txt' + + dependencies { + include(dependency('org.spongepowered:mixin')) + } + + classifier = 'release' +} + +sourceJar { + dependsOn retromapReplacedDebug + dependsOn retromapReplacedClient + + from zipTree(tasks.retromapReplacedDebug.out) + from zipTree(tasks.retromapReplacedClient.out) +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + from javadoc.destinationDir + classifier = 'javadoc' +} + +// Hey @AbrarSyed why can't we just turn this off >:( +task runClient(type: JavaExec, overwrite: true) { + doFirst { + println "Do not use runClient, it is not compatible with Mixin" + System.exit(-1) + } +} + +tasks.withType(JavaCompile) { + options.compilerArgs += [ + '-Xlint:all', + '-Xlint:-path', + '-Xlint:-rawtypes', + '-Xlint:-processing' + ] + options.deprecation = true + options.encoding = 'utf8' +} + +if (JavaVersion.current().isJava8Compatible()) { + tasks.withType(Javadoc) { + // disable the crazy super-strict doclint tool in Java 8 + options.addStringOption('Xdoclint:none', '-quiet') + } +} + +reobf { + jar { + mappingType = 'SEARGE' + } + releaseJar { + mappingType = 'NOTCH' + classpath = sourceSets.main.compileClasspath + } + shadowJar { + mappingType = 'NOTCH' + classpath = sourceSets.main.compileClasspath + } +} + +build.dependsOn {[ + 'reobfReleaseJar', + 'reobfShadowJar' +]} + +artifacts { + if (project.isReleaseBuild) { + archives jar + } + archives shadowJar + archives sourceJar + archives javadocJar +} + +task deploy(type: Copy, dependsOn: build) { + def libraryDir = new File(new File(System.env.APPDATA), ".minecraft/libraries") + from shadowJar.outputs.files[0] + into new File(libraryDir, sprintf('%1$s%4$s%2$s%4$s%3$s', project.group.replace('.', File.separator), archivesBaseName, buildVersion, File.separatorChar)) + rename shadowJar.outputs.files[0].name, sprintf("%s-%s.jar", archivesBaseName, buildVersion) +} + +uploadArchives { + repositories { + mavenDeployer { + if (project.hasProperty(project.mavenRepo)) { + repository(url: project.getProperty(project.mavenRepo)) { + authentication(userName: project.mavenUsername, password: project.mavenPassword) + } + } + pom { + groupId = project.group + version = project.version + artifactId = project.archivesBaseName + project { + name project.archivesBaseName + packaging 'jar' + description 'LiteLoader' + url 'http://www.liteloader.com/' + scm { + url 'http://develop.liteloader.com/liteloader/LiteLoader' + connection 'scm:git:http://develop.liteloader.com/liteloader/LiteLoader.git' + developerConnection 'scm:git:http://develop.liteloader.com/liteloader/LiteLoader.git' + } + issueManagement { + system 'GitLab Issues' + url 'http://develop.liteloader.com/liteloader/LiteLoader/issues' + } + } + } + } + } +} diff --git a/liteloader/checkstyle.xml b/liteloader/checkstyle.xml new file mode 100644 index 00000000..12c30c37 --- /dev/null +++ b/liteloader/checkstyle.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/liteloader/extra/formatter.xml b/liteloader/extra/formatter.xml new file mode 100644 index 00000000..9f60f653 --- /dev/null +++ b/liteloader/extra/formatter.xml @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/liteloader/extra/liteloader.importorder b/liteloader/extra/liteloader.importorder new file mode 100644 index 00000000..5189fd16 --- /dev/null +++ b/liteloader/extra/liteloader.importorder @@ -0,0 +1,7 @@ +#Organize Import Order +#Wed Dec 02 11:57:04 GMT 2015 +4=com +3=org +2=javax +1=java +0=\# diff --git a/liteloader/gradle.properties b/liteloader/gradle.properties new file mode 100644 index 00000000..6de692c9 --- /dev/null +++ b/liteloader/gradle.properties @@ -0,0 +1,10 @@ +name=LiteLoader +inceptionYear=2012 +packaging=jar +description=LiteLoader +url=http://www.liteloader.com +organization=LiteLoader +buildType=SNAPSHOT +buildVersion=1.8 +mcVersion=1.8 +mcMappings=snapshot_20151124 \ No newline at end of file diff --git a/liteloader/liteloader-1.8-SNAPSHOT-sources.jar b/liteloader/liteloader-1.8-SNAPSHOT-sources.jar deleted file mode 100644 index 29224102df6c8a197b49cde8bee044664da3ca06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 366656 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%0AZRdd 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↱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 nxn(j8`W`rMXZ`+l*+KY diff --git a/liteloader/liteloader-1.8-SNAPSHOT-srgnames.jar b/liteloader/liteloader-1.8-SNAPSHOT-srgnames.jar deleted file mode 100644 index 341ebbd0d9118fc6a02fb575cb1ed98fee7834cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 647352 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

    qpba^4BwJ(n;ToK%Evl<`gnsdo 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 z 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

      D|*k?R# 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 diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/ChatFilter.java b/liteloader/src/client/java/com/mumfrey/liteloader/ChatFilter.java new file mode 100644 index 00000000..31d54f7c --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/ChatFilter.java @@ -0,0 +1,27 @@ +package com.mumfrey.liteloader; + +import com.mumfrey.liteloader.core.LiteLoaderEventBroker.ReturnValue; + +import net.minecraft.util.IChatComponent; + + +/** + * Interface for mods which can filter inbound chat + * + * @author Adam Mummery-Smith + */ +public interface ChatFilter extends LiteMod +{ + /** + * Chat filter function, return false to filter this packet, true to pass + * the packet. + * + * @param chat ChatMessageComponent parsed from the chat packet + * @param message Chat message parsed from the chat message component + * @param newMessage If you wish to mutate the message, set the value using + * newMessage.set() + * + * @return True to keep the packet, false to discard + */ + public abstract boolean onChat(IChatComponent chat, String message, ReturnValue newMessage); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/ChatListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/ChatListener.java new file mode 100644 index 00000000..dd631c46 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/ChatListener.java @@ -0,0 +1,20 @@ +package com.mumfrey.liteloader; + +import net.minecraft.util.IChatComponent; + + +/** + * Interface for mods which receive inbound chat + * + * @author Adam Mummery-Smith + */ +public interface ChatListener extends LiteMod +{ + /** + * Handle an inbound message + * + * @param chat IChatComponent parsed from the chat packet + * @param message Chat message parsed from the chat message component + */ + public abstract void onChat(IChatComponent chat, String message); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/ChatRenderListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/ChatRenderListener.java new file mode 100644 index 00000000..3e1d89ca --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/ChatRenderListener.java @@ -0,0 +1,15 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.gui.GuiNewChat; + +/** + * Interface for mods which want to alter the chat display + * + * @author Adam Mummery-Smith + */ +public interface ChatRenderListener extends LiteMod +{ + public abstract void onPreRenderChat(int screenWidth, int screenHeight, GuiNewChat chat); + + public abstract void onPostRenderChat(int screenWidth, int screenHeight, GuiNewChat chat); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/EntityRenderListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/EntityRenderListener.java new file mode 100644 index 00000000..bb2c3c93 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/EntityRenderListener.java @@ -0,0 +1,39 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.entity.Entity; + +/** + * Interface for mods which want to receive callbacks when entities are rendered + * into the world. + * + * @author Adam Mummery-Smith + */ +public interface EntityRenderListener extends LiteMod +{ + /** + * Called immediately prior to an entity being rendered + * + * @param render + * @param entity + * @param xPos + * @param yPos + * @param zPos + * @param yaw + * @param partialTicks + */ + public abstract void onRenderEntity(Render render, Entity entity, double xPos, double yPos, double zPos, float yaw, float partialTicks); + + /** + * Called immediately following an entity being rendered + * + * @param render + * @param entity + * @param xPos + * @param yPos + * @param zPos + * @param yaw + * @param partialTicks + */ + public abstract void onPostRenderEntity(Render render, Entity entity, double xPos, double yPos, double zPos, float yaw, float partialTicks); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/FrameBufferListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/FrameBufferListener.java new file mode 100644 index 00000000..a8b8caf6 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/FrameBufferListener.java @@ -0,0 +1,34 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.shader.Framebuffer; + +/** + * Interface for mods which want to interact with Minecraft's main Frame Buffer + * Object. + * + * @author Adam Mummery-Smith + */ +public interface FrameBufferListener extends LiteMod +{ + /** + * Called before the FBO is rendered. Useful if you want to interact with + * the FBO before it is drawn to the screen. + */ + public abstract void preRenderFBO(Framebuffer fbo); + + /** + * Called immediately before the FBO is rendered to the screen, after the + * appropriate IGL modes and matrix transforms have been set but before the + * FBO is actually rendered into the main output buffer. + * + * @param fbo FBO instance + * @param width FBO width + * @param height FBO height + */ + public abstract void onRenderFBO(Framebuffer fbo, int width, int height); + + /** + * Called after the FBO is rendered whilst still inside the FBO transform + */ + public abstract void postRenderFBO(Framebuffer fbo); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/GameLoopListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/GameLoopListener.java new file mode 100644 index 00000000..d2a848ad --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/GameLoopListener.java @@ -0,0 +1,18 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.Minecraft; + +/** + * Interface for mods which want a frame notification every single game loop + * + * @author Adam Mummery-Smith + */ +public interface GameLoopListener extends LiteMod +{ + /** + * Called every frame, before the world is ticked + * + * @param minecraft + */ + public abstract void onRunGameLoop(Minecraft minecraft); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/HUDRenderListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/HUDRenderListener.java new file mode 100644 index 00000000..e1a69257 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/HUDRenderListener.java @@ -0,0 +1,13 @@ +package com.mumfrey.liteloader; + +/** + * Interface for mods which want callbacks when the HUD is rendered + * + * @author Adam Mummery-Smith + */ +public interface HUDRenderListener extends LiteMod +{ + public abstract void onPreRenderHUD(int screenWidth, int screenHeight); + + public abstract void onPostRenderHUD(int screenWidth, int screenHeight); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/InitCompleteListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/InitCompleteListener.java new file mode 100644 index 00000000..23cde02d --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/InitCompleteListener.java @@ -0,0 +1,24 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.Minecraft; + +import com.mumfrey.liteloader.core.LiteLoader; + +/** + * Interface for mods which need to initialise stuff once the game + * initialisation is completed, for example mods which need to register new + * renderers. + * + * @author Adam Mummery-Smith + */ +public interface InitCompleteListener extends Tickable +{ + /** + * Called as soon as the game is initialised and the main game loop is + * running. + * + * @param minecraft Minecraft instance + * @param loader LiteLoader instance + */ + public abstract void onInitCompleted(Minecraft minecraft, LiteLoader loader); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/JoinGameListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/JoinGameListener.java new file mode 100644 index 00000000..6ab33808 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/JoinGameListener.java @@ -0,0 +1,29 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.network.INetHandler; +import net.minecraft.network.play.server.S01PacketJoinGame; + +import com.mojang.realmsclient.dto.RealmsServer; + + +/** + * Interface for mods which wish to be notified when the player connects to a + * server (or local game). + * + * @author Adam Mummery-Smith + */ +public interface JoinGameListener extends LiteMod +{ + /** + * Called on join game + * + * @param netHandler Net handler + * @param joinGamePacket Join game packet + * @param serverData ServerData object representing the server being + * connected to + * @param realmsServer If connecting to a realm, a reference to the + * RealmsServer object + */ + public abstract void onJoinGame(INetHandler netHandler, S01PacketJoinGame joinGamePacket, ServerData serverData, RealmsServer realmsServer); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/OutboundChatFilter.java b/liteloader/src/client/java/com/mumfrey/liteloader/OutboundChatFilter.java new file mode 100644 index 00000000..266d3c96 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/OutboundChatFilter.java @@ -0,0 +1,17 @@ +package com.mumfrey.liteloader; + +/** + * Interface for mods which want to filter outbound chat + * + * @author Adam Mummery-Smith + */ +public interface OutboundChatFilter extends LiteMod +{ + /** + * Raised when a chat message is being sent, return false to filter this + * message or true to allow it to be sent. + * + * @param message + */ + public abstract boolean onSendChatMessage(String message); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/OutboundChatListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/OutboundChatListener.java new file mode 100644 index 00000000..f575cf9c --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/OutboundChatListener.java @@ -0,0 +1,20 @@ +package com.mumfrey.liteloader; + +import net.minecraft.network.play.client.C01PacketChatMessage; + +/** + * Interface for mods which want to monitor outbound chat + * + * @author Adam Mummery-Smith + */ +public interface OutboundChatListener extends LiteMod +{ + /** + * Raised when a new chat packet is created (not necessarily transmitted, + * something could be trolling us). + * + * @param packet + * @param message + */ + public abstract void onSendChatMessage(C01PacketChatMessage packet, String message); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/PostLoginListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/PostLoginListener.java new file mode 100644 index 00000000..952343d5 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/PostLoginListener.java @@ -0,0 +1,21 @@ +package com.mumfrey.liteloader; + +import net.minecraft.network.login.INetHandlerLoginClient; +import net.minecraft.network.login.server.S02PacketLoginSuccess; + +/** + * + * @author Adam Mummery-Smith + */ +public interface PostLoginListener extends LiteMod +{ + /** + * Called immediately after login, before the player has properly joined the + * game. Note that this event is raised in the network thread and is + * not marshalled to the main thread as other packet-generated events are. + * + * @param netHandler + * @param packet + */ + public abstract void onPostLogin(INetHandlerLoginClient netHandler, S02PacketLoginSuccess packet); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/PostRenderListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/PostRenderListener.java new file mode 100644 index 00000000..2a872f01 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/PostRenderListener.java @@ -0,0 +1,23 @@ +package com.mumfrey.liteloader; + +/** + * Render callback that gets called AFTER entities are rendered + * + * @author Adam Mummery-Smith + */ +public interface PostRenderListener extends LiteMod +{ + /** + * Called after entities are rendered but before particles + * + * @param partialTicks + */ + public abstract void onPostRenderEntities(float partialTicks); + + /** + * Called after all world rendering is completed + * + * @param partialTicks + */ + public abstract void onPostRender(float partialTicks); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/PreRenderListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/PreRenderListener.java new file mode 100644 index 00000000..20cf9c6b --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/PreRenderListener.java @@ -0,0 +1,57 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.renderer.RenderGlobal; + +/** + * Render callbacks that get called before certain render events + * + * @author Adam Mummery-Smith + */ +public interface PreRenderListener extends LiteMod +{ + /** + * Called immediately before rendering of the world (including the sky) is + * started. + * + * @param partialTicks + */ + public abstract void onRenderWorld(float partialTicks); + + /** + * Called after the world camera transform is initialised, may be + * called more than once per frame if anaglyph is enabled. + * + * @param partialTicks + * @param pass + * @param timeSlice + */ + public abstract void onSetupCameraTransform(float partialTicks, int pass, long timeSlice); + + /** + * Called when the sky is rendered, may be called more than once per frame + * if anaglyph is enabled. + * + * @param partialTicks + * @param pass + */ + public abstract void onRenderSky(float partialTicks, int pass); + + /** + * Called immediately before the clouds are rendered, may be called more + * than once per frame if anaglyph is enabled. + * + * @param renderGlobal + * @param partialTicks + * @param pass + */ + public abstract void onRenderClouds(float partialTicks, int pass, RenderGlobal renderGlobal); + + /** + * Called before the terrain is rendered, may be called more than once per + * frame if anaglyph is enabled. + * + * @param partialTicks + * @param pass + */ + public abstract void onRenderTerrain(float partialTicks, int pass); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/RenderListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/RenderListener.java new file mode 100644 index 00000000..a39953da --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/RenderListener.java @@ -0,0 +1,36 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.gui.GuiScreen; + +/** + * Interface for objects which want a pre-render callback + * + * @author Adam Mummery-Smith + */ +public interface RenderListener extends LiteMod +{ + /** + * Callback when a frame is rendered + */ + public abstract void onRender(); + + /** + * Called immediately before the current GUI is rendered + * + * @param currentScreen Current screen (if any) + */ + public abstract void onRenderGui(GuiScreen currentScreen); + + /** + * Called when the world is rendered + * + * @deprecated Use PreRenderListener::onRenderWorld(F)V instead + */ + @Deprecated + public abstract void onRenderWorld(); + + /** + * Called immediately after the world/camera transform is initialised + */ + public abstract void onSetupCameraTransform(); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/ScreenshotListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/ScreenshotListener.java new file mode 100644 index 00000000..0ea38fc7 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/ScreenshotListener.java @@ -0,0 +1,28 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.shader.Framebuffer; +import net.minecraft.util.IChatComponent; + +import com.mumfrey.liteloader.core.LiteLoaderEventBroker.ReturnValue; + +/** + * Interface for mods which want to handle or inhibit the saving of screenshots + * + * @author Adam Mummery-Smith + */ +public interface ScreenshotListener extends LiteMod +{ + /** + * Called when a screenshot is taken, mods should return FALSE to suspend + * further processing, or TRUE to allow processing to continue normally + * + * @param screenshotName + * @param width + * @param height + * @param fbo + * @param message Message to return if the event is cancelled + * @return FALSE to suspend further processing, or TRUE to allow processing + * to continue normally + */ + public boolean onSaveScreenshot(String screenshotName, int width, int height, Framebuffer fbo, ReturnValue message); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/Tickable.java b/liteloader/src/client/java/com/mumfrey/liteloader/Tickable.java new file mode 100644 index 00000000..21eca4f9 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/Tickable.java @@ -0,0 +1,22 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.Minecraft; + +/** + * Interface for mods which want tick events + * + * @author Adam Mummery-Smith + */ +public interface Tickable extends LiteMod +{ + /** + * Called every frame + * + * @param minecraft Minecraft instance + * @param partialTicks Partial tick value + * @param inGame True if in-game, false if in the menu + * @param clock True if this is a new tick, otherwise false if it's a + * regular frame + */ + public abstract void onTick(Minecraft minecraft, float partialTicks, boolean inGame, boolean clock); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/ViewportListener.java b/liteloader/src/client/java/com/mumfrey/liteloader/ViewportListener.java new file mode 100644 index 00000000..73c44171 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/ViewportListener.java @@ -0,0 +1,10 @@ +package com.mumfrey.liteloader; + +import net.minecraft.client.gui.ScaledResolution; + +public interface ViewportListener extends LiteMod +{ + public abstract void onViewportResized(ScaledResolution resolution, int displayWidth, int displayHeight); + + public abstract void onFullScreenToggled(boolean fullScreen); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ClientPluginChannelsClient.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ClientPluginChannelsClient.java new file mode 100644 index 00000000..9f2371f0 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ClientPluginChannelsClient.java @@ -0,0 +1,121 @@ +package com.mumfrey.liteloader.client; + +import com.mumfrey.liteloader.client.ducks.IClientNetLoginHandler; +import com.mumfrey.liteloader.core.ClientPluginChannels; +import com.mumfrey.liteloader.core.exceptions.UnregisteredChannelException; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.INetHandler; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.PacketBuffer; +import net.minecraft.network.login.INetHandlerLoginClient; +import net.minecraft.network.login.server.S02PacketLoginSuccess; +import net.minecraft.network.play.INetHandlerPlayClient; +import net.minecraft.network.play.client.C17PacketCustomPayload; +import net.minecraft.network.play.server.S01PacketJoinGame; +import net.minecraft.network.play.server.S3FPacketCustomPayload; + +/** + * Handler for client plugin channels + * + * @author Adam Mummery-Smith + */ +public class ClientPluginChannelsClient extends ClientPluginChannels +{ + /** + * @param netHandler + * @param loginPacket + */ + void onPostLogin(INetHandlerLoginClient netHandler, S02PacketLoginSuccess loginPacket) + { + this.clearPluginChannels(netHandler); + } + + /** + * @param netHandler + * @param loginPacket + */ + void onJoinGame(INetHandler netHandler, S01PacketJoinGame loginPacket) + { + this.sendRegisteredPluginChannels(netHandler); + } + + /** + * Callback for the plugin channel hook + * + * @param customPayload + */ + @Override + public void onPluginChannelMessage(S3FPacketCustomPayload customPayload) + { + if (customPayload != null && customPayload.getChannelName() != null) + { + String channel = customPayload.getChannelName(); + PacketBuffer data = customPayload.getBufferData(); + + this.onPluginChannelMessage(channel, data); + } + } + + /** + * @param netHandler + * @param registrationData + */ + @Override + protected void sendRegistrationData(INetHandler netHandler, PacketBuffer registrationData) + { + if (netHandler instanceof INetHandlerLoginClient) + { + NetworkManager networkManager = ((IClientNetLoginHandler)netHandler).getNetMgr(); + networkManager.sendPacket(new C17PacketCustomPayload(CHANNEL_REGISTER, registrationData)); + } + else if (netHandler instanceof INetHandlerPlayClient) + { + ClientPluginChannelsClient.dispatch(new C17PacketCustomPayload(CHANNEL_REGISTER, registrationData)); + } + } + + /** + * Send a message to the server on a plugin channel + * + * @param channel Channel to send, must not be a reserved channel name + * @param data + */ + @Override + protected boolean send(String channel, PacketBuffer data, ChannelPolicy policy) + { + if (channel == null || channel.length() > 16 || CHANNEL_REGISTER.equals(channel) || CHANNEL_UNREGISTER.equals(channel)) + { + throw new RuntimeException("Invalid channel name specified"); + } + + if (!policy.allows(this, channel)) + { + if (policy.isSilent()) return false; + throw new UnregisteredChannelException(channel); + } + + C17PacketCustomPayload payload = new C17PacketCustomPayload(channel, data); + return ClientPluginChannelsClient.dispatch(payload); + } + + /** + * @param payload + */ + static boolean dispatch(C17PacketCustomPayload payload) + { + try + { + Minecraft minecraft = Minecraft.getMinecraft(); + + if (minecraft.thePlayer != null && minecraft.thePlayer.sendQueue != null) + { + minecraft.thePlayer.sendQueue.addToSendQueue(payload); + return true; + } + } + catch (Exception ex) {} + + return false; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ClientProxy.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ClientProxy.java new file mode 100644 index 00000000..a3eca16a --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ClientProxy.java @@ -0,0 +1,183 @@ +package com.mumfrey.liteloader.client; + +import java.io.File; + +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.mumfrey.liteloader.client.ducks.IFramebuffer; +import com.mumfrey.liteloader.core.Proxy; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiNewChat; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.client.shader.Framebuffer; +import net.minecraft.entity.Entity; +import net.minecraft.server.integrated.IntegratedServer; +import net.minecraft.util.IChatComponent; +import net.minecraft.world.WorldSettings; + +/** + * Proxy class which handles the redirected calls from the injected callbacks + * and routes them to the relevant liteloader handler classes. We do this rather + * than patching a bunch of bytecode into the packet classes themselves because + * this is easier to maintain. + * + * @author Adam Mummery-Smith + */ +public abstract class ClientProxy extends Proxy +{ + private static LiteLoaderEventBrokerClient broker; + + private ClientProxy() {} + + public static void onStartupComplete() + { + Proxy.onStartupComplete(); + + ClientProxy.broker = LiteLoaderEventBrokerClient.getInstance(); + + if (ClientProxy.broker == null) + { + throw new RuntimeException("LiteLoader failed to start up properly." + + " The game is in an unstable state and must shut down now. Check the developer log for startup errors"); + } + + ClientProxy.broker.onStartupComplete(); + } + + public static void onTimerUpdate() + { + ClientProxy.broker.onTimerUpdate(); + } + + public static void newTick() + { + } + + public static void onTick() + { + ClientProxy.broker.onTick(); + } + + public static void onRender() + { + ClientProxy.broker.onRender(); + } + + public static void preRenderGUI(float partialTicks) + { + ClientProxy.broker.preRenderGUI(partialTicks); + } + + public static void onSetupCameraTransform(int pass, float partialTicks, long timeSlice) + { + ClientProxy.broker.onSetupCameraTransform(pass, partialTicks, timeSlice); + } + + public static void postRenderEntities(int pass, float partialTicks, long timeSlice) + { + ClientProxy.broker.postRenderEntities(partialTicks, timeSlice); + } + + public static void postRender(float partialTicks, long timeSlice) + { + ClientProxy.broker.postRender(partialTicks, timeSlice); + } + + public static void onRenderHUD(float partialTicks) + { + ClientProxy.broker.onRenderHUD(partialTicks); + } + + public static void onRenderChat(GuiNewChat chatGui, float partialTicks) + { + ClientProxy.broker.onRenderChat(chatGui, partialTicks); + } + + public static void postRenderChat(GuiNewChat chatGui, float partialTicks) + { + ClientProxy.broker.postRenderChat(chatGui, partialTicks); + } + + public static void postRenderHUD(float partialTicks) + { + ClientProxy.broker.postRenderHUD(partialTicks); + } + + public static void onCreateIntegratedServer(IntegratedServer server, String folderName, String worldName, WorldSettings worldSettings) + { + ClientProxy.broker.onStartServer(server, folderName, worldName, worldSettings); + } + + public static void onOutboundChat(CallbackInfo e, String message) + { + ClientProxy.broker.onSendChatMessage(e, message); + } + + public static void onResize(Minecraft mc) + { + if (ClientProxy.broker == null) return; + ClientProxy.broker.onResize(mc); + } + + public static void preRenderFBO(Framebuffer frameBufferMc) + { + if (ClientProxy.broker == null) return; + if (frameBufferMc instanceof IFramebuffer) + { + ((IFramebuffer)frameBufferMc).setDispatchRenderEvent(true); + } + ClientProxy.broker.preRenderFBO(frameBufferMc); + } + + public static void postRenderFBO(Framebuffer frameBufferMc) + { + if (ClientProxy.broker == null) return; + ClientProxy.broker.postRenderFBO(frameBufferMc); + } + + public static void renderFBO(Framebuffer frameBufferMc, int width, int height, boolean flag) + { + if (ClientProxy.broker == null) return; + ClientProxy.broker.onRenderFBO(frameBufferMc, width, height); + } + + public static void onRenderWorld(float partialTicks, long timeSlice) + { + ClientProxy.broker.onRenderWorld(partialTicks, timeSlice); + } + + public static void onRenderSky(int pass, float partialTicks, long timeSlice) + { + ClientProxy.broker.onRenderSky(partialTicks, pass, timeSlice); + } + + public static void onRenderClouds(RenderGlobal renderGlobalIn, float partialTicks, int pass) + { + ClientProxy.broker.onRenderClouds(partialTicks, pass, renderGlobalIn); + } + + public static void onRenderTerrain(int pass, float partialTicks, long timeSlice) + { + ClientProxy.broker.onRenderTerrain(partialTicks, pass, timeSlice); + } + + public static void onSaveScreenshot(CallbackInfoReturnable ci, File gameDir, String name, int width, int height, + Framebuffer fbo) + { + ClientProxy.broker.onScreenshot(ci, name, width, height, fbo); + } + + public static void onRenderEntity(RenderManager source, Render render, Entity entity, double x, double y, double z, float yaw, float pTicks) + { + ClientProxy.broker.onRenderEntity(source, entity, x, y, z, yaw, pTicks, render); + } + + public static void onPostRenderEntity(RenderManager source, Render render, Entity entity, double x, double y, double z, float yaw, float pTicks) + { + ClientProxy.broker.onPostRenderEntity(source, entity, x, y, z, yaw, pTicks, render); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/GameEngineClient.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/GameEngineClient.java new file mode 100644 index 00000000..72f19ba3 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/GameEngineClient.java @@ -0,0 +1,159 @@ +package com.mumfrey.liteloader.client; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.audio.SoundHandler; +import net.minecraft.client.gui.GuiNewChat; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.settings.GameSettings; +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.profiler.Profiler; +import net.minecraft.server.integrated.IntegratedServer; + +import com.mumfrey.liteloader.client.overlays.IMinecraft; +import com.mumfrey.liteloader.common.GameEngine; +import com.mumfrey.liteloader.common.Resources; + +/** + * + * @author Adam Mummery-Smith + */ +public class GameEngineClient implements GameEngine +{ + private final Minecraft engine = Minecraft.getMinecraft(); + + private final Resources resources = new ResourcesClient(); + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#getProfiler() + */ + @Override + public Profiler getProfiler() + { + return this.engine.mcProfiler; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#isClient() + */ + @Override + public boolean isClient() + { + return true; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#isServer() + */ + @Override + public boolean isServer() + { + return this.isSinglePlayer(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#isInGame() + */ + @Override + public boolean isInGame() + { + return this.engine.thePlayer != null && this.engine.theWorld != null && this.engine.theWorld.isRemote; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#isRunning() + */ + @Override + public boolean isRunning() + { + return ((IMinecraft)this.engine).isRunning(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#isSingleplayer() + */ + @Override + public boolean isSinglePlayer() + { + return this.engine.isSingleplayer(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#getClient() + */ + @Override + public Minecraft getClient() + { + return this.engine; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#getServer() + */ + @Override + public IntegratedServer getServer() + { + return this.engine.getIntegratedServer(); + } + + @Override + public Resources getResources() + { + return this.resources; + } + + public GameSettings getGameSettings() + { + return this.engine.gameSettings; + } + + public ScaledResolution getScaledResolution() + { + return new ScaledResolution(this.engine, this.engine.displayWidth, this.engine.displayHeight); + } + + public GuiNewChat getChatGUI() + { + return this.engine.ingameGUI.getChatGUI(); + } + + public GuiScreen getCurrentScreen() + { + return this.engine.currentScreen; + } + + public boolean hideGUI() + { + return this.engine.gameSettings.hideGUI; + } + + public SoundHandler getSoundHandler() + { + return this.engine.getSoundHandler(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#getKeyBindings() + */ + @Override + public List getKeyBindings() + { + LinkedList keyBindings = new LinkedList(); + keyBindings.addAll(Arrays.asList(this.engine.gameSettings.keyBindings)); + return keyBindings; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine + * #setKeyBindings(java.util.List) + */ + @Override + public void setKeyBindings(List keyBindings) + { + this.engine.gameSettings.keyBindings = keyBindings.toArray(new KeyBinding[0]); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderCoreProviderClient.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderCoreProviderClient.java new file mode 100644 index 00000000..7c1bd03d --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderCoreProviderClient.java @@ -0,0 +1,111 @@ +package com.mumfrey.liteloader.client; + +import net.minecraft.client.audio.SoundHandler; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.IResourcePack; +import net.minecraft.client.resources.SimpleReloadableResourceManager; +import net.minecraft.network.INetHandler; +import net.minecraft.network.play.server.S01PacketJoinGame; +import net.minecraft.world.World; + +import com.mumfrey.liteloader.api.CoreProvider; +import com.mumfrey.liteloader.common.GameEngine; +import com.mumfrey.liteloader.common.Resources; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.core.LiteLoaderMods; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.resources.InternalResourcePack; + +/** + * CoreProvider which fixes SoundManager derping up at startup + * + * @author Adam Mummery-Smith + */ +public class LiteLoaderCoreProviderClient implements CoreProvider +{ + /** + * Loader Properties adapter + */ + private final LoaderProperties properties; + + /** + * Read from the properties file, if true we will inhibit the sound manager + * reload during startup to avoid getting in trouble with OpenAL. + */ + private boolean inhibitSoundManagerReload = true; + + /** + * If inhibit is enabled, this object is used to reflectively inhibit the + * sound manager's reload process during startup by removing it from the + * reloadables list. + */ + private SoundHandlerReloadInhibitor soundHandlerReloadInhibitor; + + public LiteLoaderCoreProviderClient(LoaderProperties properties) + { + this.properties = properties; + } + + @Override + public void onInit() + { + this.inhibitSoundManagerReload = this.properties.getAndStoreBooleanProperty(LoaderProperties.OPTION_SOUND_MANAGER_FIX, true); + } + + @SuppressWarnings("unchecked") + @Override + public void onPostInit(GameEngine engine) + { + SimpleReloadableResourceManager resourceManager = (SimpleReloadableResourceManager)engine.getResources().getResourceManager(); + SoundHandler soundHandler = ((GameEngineClient)engine).getSoundHandler(); + this.soundHandlerReloadInhibitor = new SoundHandlerReloadInhibitor(resourceManager, soundHandler); + + if (this.inhibitSoundManagerReload) + { + this.soundHandlerReloadInhibitor.inhibit(); + } + + // Add self as a resource pack for texture/lang resources + Resources resources = (Resources)LiteLoader.getGameEngine().getResources(); + resources.registerResourcePack(new InternalResourcePack("LiteLoader", LiteLoader.class, "liteloader")); + } + + @Override + public void onPostInitComplete(LiteLoaderMods mods) + { + } + + @Override + public void onStartupComplete() + { + if (this.soundHandlerReloadInhibitor != null && this.soundHandlerReloadInhibitor.isInhibited()) + { + this.soundHandlerReloadInhibitor.unInhibit(true); + } + } + + @Override + public void onJoinGame(INetHandler netHandler, S01PacketJoinGame loginPacket) + { + } + + @Override + public void onPostRender(int mouseX, int mouseY, float partialTicks) + { + } + + @Override + public void onTick(boolean clock, float partialTicks, boolean inGame) + { + } + + @Override + public void onWorldChanged(World world) + { + } + + @Override + public void onShutDown() + { + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderEventBrokerClient.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderEventBrokerClient.java new file mode 100644 index 00000000..7eab71f3 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderEventBrokerClient.java @@ -0,0 +1,567 @@ +package com.mumfrey.liteloader.client; + +import org.lwjgl.input.Mouse; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.mumfrey.liteloader.*; +import com.mumfrey.liteloader.client.overlays.IEntityRenderer; +import com.mumfrey.liteloader.client.overlays.IMinecraft; +import com.mumfrey.liteloader.common.LoadingProgress; +import com.mumfrey.liteloader.core.InterfaceRegistrationDelegate; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.core.LiteLoaderEventBroker; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.core.event.HandlerList.ReturnLogicOp; +import com.mumfrey.liteloader.core.event.ProfilingHandlerList; +import com.mumfrey.liteloader.interfaces.FastIterableDeque; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiNewChat; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.IResourceManagerReloadListener; +import net.minecraft.client.shader.Framebuffer; +import net.minecraft.entity.Entity; +import net.minecraft.network.play.client.C01PacketChatMessage; +import net.minecraft.server.integrated.IntegratedServer; +import net.minecraft.util.IChatComponent; +import net.minecraft.util.Timer; + +public class LiteLoaderEventBrokerClient extends LiteLoaderEventBroker implements IResourceManagerReloadListener +{ + /** + * Singleton + */ + private static LiteLoaderEventBrokerClient instance; + + /** + * Reference to the game + */ + protected final GameEngineClient engineClient; + + /** + * Current screen width + */ + private int screenWidth = 854; + + /** + * Current screen height + */ + private int screenHeight = 480; + + /** + * + */ + private boolean wasFullScreen = false; + + /** + * Hash code of the current world. We don't store the world reference + * here because we don't want to mess with world GC by mistake. + */ + private int worldHashCode = 0; + + private FastIterableDeque tickListeners; + private FastIterableDeque loopListeners = new HandlerList(GameLoopListener.class); + private FastIterableDeque renderListeners = new HandlerList(RenderListener.class); + private FastIterableDeque preRenderListeners = new HandlerList(PreRenderListener.class); + private FastIterableDeque postRenderListeners = new HandlerList(PostRenderListener.class); + private FastIterableDeque hudRenderListeners = new HandlerList(HUDRenderListener.class); + private FastIterableDeque chatRenderListeners = new HandlerList(ChatRenderListener.class); + private FastIterableDeque outboundChatListeners = new HandlerList(OutboundChatListener.class); + private FastIterableDeque viewportListeners = new HandlerList(ViewportListener.class); + private FastIterableDeque frameBufferListeners = new HandlerList(FrameBufferListener.class); + private FastIterableDeque initListeners = new HandlerList(InitCompleteListener.class); + private FastIterableDeque outboundChatFilters = new HandlerList(OutboundChatFilter.class, + ReturnLogicOp.AND); + private FastIterableDeque screenshotListeners = new HandlerList(ScreenshotListener.class, + ReturnLogicOp.AND_BREAK_ON_FALSE); + private FastIterableDeque entityRenderListeners = new HandlerList(EntityRenderListener.class); + + @SuppressWarnings("cast") + public LiteLoaderEventBrokerClient(LiteLoader loader, GameEngineClient engine, LoaderProperties properties) + { + super(loader, engine, properties); + + LiteLoaderEventBrokerClient.instance = this; + + this.engineClient = (GameEngineClient)engine; + this.tickListeners = new ProfilingHandlerList(Tickable.class, this.engineClient.getProfiler()); + } + + public static LiteLoaderEventBrokerClient getInstance() + { + return LiteLoaderEventBrokerClient.instance; + } + + @Override + public void onResourceManagerReload(IResourceManager resourceManager) + { + LoadingProgress.setMessage("Reloading Resources..."); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#registerInterfaces( + * com.mumfrey.liteloader.core.InterfaceRegistrationDelegate) + */ + @Override + public void registerInterfaces(InterfaceRegistrationDelegate delegate) + { + super.registerInterfaces(delegate); + + delegate.registerInterface(Tickable.class); + delegate.registerInterface(GameLoopListener.class); + delegate.registerInterface(RenderListener.class); + delegate.registerInterface(PreRenderListener.class); + delegate.registerInterface(PostRenderListener.class); + delegate.registerInterface(HUDRenderListener.class); + delegate.registerInterface(ChatRenderListener.class); + delegate.registerInterface(OutboundChatListener.class); + delegate.registerInterface(ViewportListener.class); + delegate.registerInterface(FrameBufferListener.class); + delegate.registerInterface(InitCompleteListener.class); + delegate.registerInterface(OutboundChatFilter.class); + delegate.registerInterface(ScreenshotListener.class); + delegate.registerInterface(EntityRenderListener.class); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#initProvider() + */ + @Override + public void initProvider() + { + } + + /** + * @param tickable + */ + public void addTickListener(Tickable tickable) + { + this.tickListeners.add(tickable); + } + + /** + * @param loopListener + */ + public void addLoopListener(GameLoopListener loopListener) + { + this.loopListeners.add(loopListener); + } + + /** + * @param initCompleteListener + */ + public void addInitListener(InitCompleteListener initCompleteListener) + { + this.initListeners.add(initCompleteListener); + } + + /** + * @param renderListener + */ + public void addRenderListener(RenderListener renderListener) + { + this.renderListeners.add(renderListener); + } + + /** + * @param preRenderListener + */ + public void addPreRenderListener(PreRenderListener preRenderListener) + { + this.preRenderListeners.add(preRenderListener); + } + + /** + * @param postRenderListener + */ + public void addPostRenderListener(PostRenderListener postRenderListener) + { + this.postRenderListeners.add(postRenderListener); + } + + /** + * @param chatRenderListener + */ + public void addChatRenderListener(ChatRenderListener chatRenderListener) + { + this.chatRenderListeners.add(chatRenderListener); + } + + /** + * @param hudRenderListener + */ + public void addHUDRenderListener(HUDRenderListener hudRenderListener) + { + this.hudRenderListeners.add(hudRenderListener); + } + + /** + * @param outboundChatListener + */ + public void addOutboundChatListener(OutboundChatListener outboundChatListener) + { + this.outboundChatListeners.add(outboundChatListener); + } + + /** + * @param outboundChatFilter + */ + public void addOutboundChatFiler(OutboundChatFilter outboundChatFilter) + { + this.outboundChatFilters.add(outboundChatFilter); + } + + /** + * @param viewportListener + */ + public void addViewportListener(ViewportListener viewportListener) + { + this.viewportListeners.add(viewportListener); + } + + /** + * @param frameBufferListener + */ + public void addFrameBufferListener(FrameBufferListener frameBufferListener) + { + this.frameBufferListeners.add(frameBufferListener); + } + + /** + * @param screenshotListener + */ + public void addScreenshotListener(ScreenshotListener screenshotListener) + { + this.screenshotListeners.add(screenshotListener); + } + + /** + * @param entityRenderListener + */ + public void addEntityRenderListener(EntityRenderListener entityRenderListener) + { + this.entityRenderListeners.add(entityRenderListener); + } + + /** + * Late initialisation callback + */ + @Override + protected void onStartupComplete() + { + this.engine.getResources().refreshResources(false); + + for (InitCompleteListener initMod : this.initListeners) + { + try + { + LoadingProgress.setMessage("Calling late init for mod %s...", initMod.getName()); + LiteLoaderLogger.info("Calling late init for mod %s", initMod.getName()); + initMod.onInitCompleted(this.engine.getClient(), this.loader); + } + catch (Throwable th) + { + this.mods.onLateInitFailed(initMod, th); + LiteLoaderLogger.warning(th, "Error calling late init for mod %s", initMod.getName()); + } + } + + this.onResize(this.engineClient.getClient()); + + super.onStartupComplete(); + } + + public void onResize(Minecraft minecraft) + { + ScaledResolution currentResolution = this.engineClient.getScaledResolution(); + this.screenWidth = currentResolution.getScaledWidth(); + this.screenHeight = currentResolution.getScaledHeight(); + + if (this.wasFullScreen != minecraft.isFullScreen()) + { + this.viewportListeners.all().onFullScreenToggled(minecraft.isFullScreen()); + } + + this.wasFullScreen = minecraft.isFullScreen(); + this.viewportListeners.all().onViewportResized(currentResolution, minecraft.displayWidth, minecraft.displayHeight); + } + + /** + * Callback from the tick hook, pre render + */ + void onRender() + { + this.renderListeners.all().onRender(); + } + + /** + * Callback from the tick hook, post render entities + * + * @param partialTicks + * @param timeSlice + */ + void postRenderEntities(float partialTicks, long timeSlice) + { + this.postRenderListeners.all().onPostRenderEntities(partialTicks); + } + + /** + * Callback from the tick hook, post render + * + * @param partialTicks + * @param timeSlice + */ + void postRender(float partialTicks, long timeSlice) + { + ((IEntityRenderer)this.engineClient.getClient().entityRenderer).setupCamera(partialTicks, 0); + this.postRenderListeners.all().onPostRender(partialTicks); + } + + /** + * Called immediately before the current GUI is rendered + */ + void preRenderGUI(float partialTicks) + { + this.renderListeners.all().onRenderGui(this.engineClient.getCurrentScreen()); + } + + /** + * Called immediately after the world/camera transform is initialised + * + * @param pass + * @param timeSlice + * @param partialTicks + */ + void onSetupCameraTransform(int pass, float partialTicks, long timeSlice) + { + this.renderListeners.all().onSetupCameraTransform(); + this.preRenderListeners.all().onSetupCameraTransform(partialTicks, pass, timeSlice); + } + + /** + * Called immediately before the chat log is rendered + * + * @param chatGui + * @param partialTicks + */ + void onRenderChat(GuiNewChat chatGui, float partialTicks) + { + this.chatRenderListeners.all().onPreRenderChat(this.screenWidth, this.screenHeight, chatGui); + } + + /** + * Called immediately after the chat log is rendered + * + * @param chatGui + * @param partialTicks + */ + void postRenderChat(GuiNewChat chatGui, float partialTicks) + { + this.chatRenderListeners.all().onPostRenderChat(this.screenWidth, this.screenHeight, chatGui); + } + + /** + * Callback when about to render the HUD + */ + void onRenderHUD(float partialTicks) + { + this.hudRenderListeners.all().onPreRenderHUD(this.screenWidth, this.screenHeight); + } + + /** + * Callback when the HUD has just been rendered + */ + void postRenderHUD(float partialTicks) + { + this.hudRenderListeners.all().onPostRenderHUD(this.screenWidth, this.screenHeight); + } + + /** + * Callback from the tick hook, called every frame when the timer is updated + */ + void onTimerUpdate() + { + Minecraft minecraft = this.engine.getClient(); + this.loopListeners.all().onRunGameLoop(minecraft); + } + + /** + * Callback from the tick hook, ticks all tickable mods + */ + void onTick() + { + this.profiler.endStartSection("litemods"); + + Timer minecraftTimer = ((IMinecraft)this.engine.getClient()).getTimer(); + float partialTicks = minecraftTimer.renderPartialTicks; + boolean clock = minecraftTimer.elapsedTicks > 0; + + Minecraft minecraft = this.engine.getClient(); + + // Flag indicates whether we are in game at the moment + Entity renderViewEntity = minecraft.getRenderViewEntity(); // TODO OBF MCPTEST func_175606_aa - getRenderViewEntity + boolean inGame = renderViewEntity != null && renderViewEntity.worldObj != null; + + this.profiler.startSection("loader"); + super.onTick(clock, partialTicks, inGame); + + int mouseX = Mouse.getX() * this.screenWidth / minecraft.displayWidth; + int mouseY = this.screenHeight - Mouse.getY() * this.screenHeight / minecraft.displayHeight - 1; + this.profiler.endStartSection("postrender"); + super.onPostRender(mouseX, mouseY, partialTicks); + this.profiler.endSection(); + + // Iterate tickable mods + this.tickListeners.all().onTick(minecraft, partialTicks, inGame, clock); + + // Detected world change + int worldHashCode = (minecraft.theWorld != null) ? minecraft.theWorld.hashCode() : 0; + if (worldHashCode != this.worldHashCode) + { + this.worldHashCode = worldHashCode; + super.onWorldChanged(minecraft.theWorld); + } + } + + /** + * @param packet + * @param message + */ + void onSendChatMessage(C01PacketChatMessage packet, String message) + { + this.outboundChatListeners.all().onSendChatMessage(packet, message); + } + + /** + * @param message + */ + void onSendChatMessage(CallbackInfo e, String message) + { + if (!this.outboundChatFilters.all().onSendChatMessage(message)) + { + e.cancel(); + } + } + + /** + * @param framebuffer + */ + void preRenderFBO(Framebuffer framebuffer) + { + this.frameBufferListeners.all().preRenderFBO(framebuffer); + } + + /** + * @param framebuffer + * @param width + * @param height + */ + void onRenderFBO(Framebuffer framebuffer, int width, int height) + { + this.frameBufferListeners.all().onRenderFBO(framebuffer, width, height); + } + + /** + * @param framebuffer + */ + void postRenderFBO(Framebuffer framebuffer) + { + this.frameBufferListeners.all().postRenderFBO(framebuffer); + } + + /** + * @param partialTicks + * @param timeSlice + */ + void onRenderWorld(float partialTicks, long timeSlice) + { + this.preRenderListeners.all().onRenderWorld(partialTicks); + this.renderListeners.all().onRenderWorld(); + } + + /** + * @param partialTicks + * @param pass + * @param timeSlice + */ + void onRenderSky(float partialTicks, int pass, long timeSlice) + { + this.preRenderListeners.all().onRenderSky(partialTicks, pass); + } + + /** + * @param partialTicks + * @param pass + * @param renderGlobal + */ + void onRenderClouds(float partialTicks, int pass, RenderGlobal renderGlobal) + { + this.preRenderListeners.all().onRenderClouds(partialTicks, pass, renderGlobal); + } + + /** + * @param partialTicks + * @param pass + * @param timeSlice + */ + void onRenderTerrain(float partialTicks, int pass, long timeSlice) + { + this.preRenderListeners.all().onRenderTerrain(partialTicks, pass); + } + + /** + * @param e + * @param name + * @param width + * @param height + * @param fbo + */ + void onScreenshot(CallbackInfoReturnable ci, String name, int width, int height, Framebuffer fbo) + { + ReturnValue ret = new ReturnValue(ci.getReturnValue()); + + if (!this.screenshotListeners.all().onSaveScreenshot(name, width, height, fbo, ret)) + { + ci.setReturnValue(ret.get()); + } + } + + /** + * @param source + * @param entity + * @param xPos + * @param yPos + * @param zPos + * @param yaw + * @param partialTicks + * @param render + */ + public void onRenderEntity(RenderManager source, Entity entity, double xPos, double yPos, double zPos, float yaw, float partialTicks, + Render render) + { + this.entityRenderListeners.all().onRenderEntity(render, entity, xPos, yPos, zPos, yaw, partialTicks); + } + + /** + * @param source + * @param entity + * @param xPos + * @param yPos + * @param zPos + * @param yaw + * @param partialTicks + * @param render + */ + public void onPostRenderEntity(RenderManager source, Entity entity, double xPos, double yPos, double zPos, float yaw, float partialTicks, + Render render) + { + this.entityRenderListeners.all().onPostRenderEntity(render, entity, xPos, yPos, zPos, yaw, partialTicks); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderPanelManager.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderPanelManager.java new file mode 100644 index 00000000..cf517cec --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/LiteLoaderPanelManager.java @@ -0,0 +1,294 @@ +package com.mumfrey.liteloader.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiIngameMenu; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiOptions; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +import org.lwjgl.input.Keyboard; + +import com.mumfrey.liteloader.client.gui.GuiLiteLoaderPanel; +import com.mumfrey.liteloader.common.GameEngine; +import com.mumfrey.liteloader.core.LiteLoaderMods; +import com.mumfrey.liteloader.core.LiteLoaderUpdateSite; +import com.mumfrey.liteloader.core.LiteLoaderVersion; +import com.mumfrey.liteloader.interfaces.PanelManager; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.modconfig.ConfigManager; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Observer which handles the display of the mod panel + * + * @author Adam Mummery-Smith + */ +public class LiteLoaderPanelManager implements PanelManager +{ + private final LoaderEnvironment environment; + + /** + * Loader Properties adapter + */ + private final LoaderProperties properties; + + private LiteLoaderMods mods; + + private ConfigManager configManager; + + private Minecraft minecraft; + + /** + * Setting which determines whether we show the "mod info" screen tab in the + * main menu. + */ + private boolean displayModInfoScreenTab = true; + + /** + * Don't hide t + */ + private boolean tabAlwaysExpanded = false; + + /** + * Override for the "mod info" tab setting, so that mods which want to + * handle the mod info themselves can temporarily disable the function + * without having to change the underlying property. + */ + private boolean hideModInfoScreenTab = false; + + private boolean checkForUpdate = false; + + private String notification; + + /** + * Active "mod info" screen, drawn as an overlay when in the main menu and + * made the active screen if the user clicks the tab. + */ + private GuiLiteLoaderPanel panelHost; + + /** + * @param environment + * @param properties + */ + @SuppressWarnings("unchecked") + public LiteLoaderPanelManager(GameEngine engine, LoaderEnvironment environment, LoaderProperties properties) + { + this.environment = environment; + this.properties = properties; + this.minecraft = ((GameEngine)engine).getClient(); + + this.displayModInfoScreenTab = this.properties.getAndStoreBooleanProperty(LoaderProperties.OPTION_MOD_INFO_SCREEN, true); + this.tabAlwaysExpanded = this.properties.getAndStoreBooleanProperty(LoaderProperties.OPTION_NO_HIDE_TAB, false); + + if (this.properties.getAndStoreBooleanProperty(LoaderProperties.OPTION_FORCE_UPDATE, false)) + { + int updateCheckInterval = this.properties.getIntegerProperty(LoaderProperties.OPTION_UPDATE_CHECK_INTR) + 1; + LiteLoaderLogger.debug("Force update is TRUE, updateCheckInterval = %d", updateCheckInterval); + + if (updateCheckInterval > 10) + { + LiteLoaderLogger.debug("Forcing update check!"); + this.checkForUpdate = true; + updateCheckInterval = 0; + } + + this.properties.setIntegerProperty(LoaderProperties.OPTION_UPDATE_CHECK_INTR, updateCheckInterval); + this.properties.writeProperties(); + } + } + + @Override + public void init(LiteLoaderMods mods, ConfigManager configManager) + { + this.mods = mods; + this.configManager = configManager; + } + + @Override + public void onStartupComplete() + { + if (this.checkForUpdate) + { + LiteLoaderVersion.getUpdateSite().beginUpdateCheck(); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.TickObserver + * #onTick(boolean, float, boolean) + */ + @Override + public void onTick(boolean clock, float partialTicks, boolean inGame) + { + if (clock && this.panelHost != null && this.minecraft.currentScreen != this.panelHost) + { + this.panelHost.updateScreen(); + } + + if (clock && this.checkForUpdate) + { + LiteLoaderUpdateSite updateSite = LiteLoaderVersion.getUpdateSite(); + if (!updateSite.isCheckInProgress() && updateSite.isCheckComplete()) + { + LiteLoaderLogger.debug("Scheduled update check completed, success=%s", updateSite.isCheckSucceess()); + this.checkForUpdate = false; + if (updateSite.isCheckSucceess() && updateSite.isUpdateAvailable()) + { + this.setNotification(I18n.format("gui.notifications.updateavailable")); + } + } + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.PostRenderObserver + * #onPostRender(int, int, float) + */ + @Override + public void onPostRender(int mouseX, int mouseY, float partialTicks) + { + if (this.mods == null) return; + + boolean tabHidden = this.isTabHidden() && this.minecraft.currentScreen instanceof GuiMainMenu; + + if (this.isPanelSupportedOnScreen(this.minecraft.currentScreen) + && ((this.displayModInfoScreenTab && !tabHidden) || (this.panelHost != null && this.panelHost.isOpen()))) + { + // If we're at the main menu, prepare the overlay + if (this.panelHost == null || this.panelHost.getScreen() != this.minecraft.currentScreen) + { + this.panelHost = new GuiLiteLoaderPanel(this.minecraft, this.minecraft.currentScreen, this.mods, this.environment, this.properties, + this.configManager, !tabHidden); + if (this.notification != null) + { + this.panelHost.setNotification(this.notification); + } + } + + this.minecraft.entityRenderer.setupOverlayRendering(); + this.panelHost.drawScreen(mouseX, mouseY, partialTicks, this.tabAlwaysExpanded); + } + else if (this.minecraft.currentScreen != this.panelHost && this.panelHost != null) + { + // If we're in any other screen, kill the overlay + this.panelHost.release(); + this.panelHost = null; + } + else if (this.isPanelSupportedOnScreen(this.minecraft.currentScreen) + && Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) + && Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) + && Keyboard.isKeyDown(Keyboard.KEY_TAB)) + { + this.displayLiteLoaderPanel(this.minecraft.currentScreen); + } + } + + /** + * Set the "mod info" screen tab to hidden, regardless of the property + * setting. + */ + @Override + public void hideTab() + { + this.hideModInfoScreenTab = true; + } + + private boolean isTabHidden() + { + return this.hideModInfoScreenTab && this.getStartupErrorCount() == 0 && this.notification == null; + } + + /** + * Set whether the "mod info" screen tab should be shown in the main menu + */ + @Override + public void setTabVisible(boolean show) + { + this.displayModInfoScreenTab = show; + this.properties.setBooleanProperty(LoaderProperties.OPTION_MOD_INFO_SCREEN, show); + this.properties.writeProperties(); + } + + /** + * Get whether the "mod info" screen tab is shown in the main menu + */ + @Override + public boolean isTabVisible() + { + return this.displayModInfoScreenTab; + } + + @Override + public void setTabAlwaysExpanded(boolean expand) + { + this.tabAlwaysExpanded = expand; + this.properties.setBooleanProperty(LoaderProperties.OPTION_NO_HIDE_TAB, expand); + this.properties.writeProperties(); + } + + @Override + public boolean isTabAlwaysExpanded() + { + return this.tabAlwaysExpanded; + } + + @Override + public void setForceUpdateEnabled(boolean forceUpdate) + { + this.properties.setBooleanProperty(LoaderProperties.OPTION_FORCE_UPDATE, forceUpdate); + this.properties.writeProperties(); + } + + @Override + public boolean isForceUpdateEnabled() + { + return this.properties.getBooleanProperty(LoaderProperties.OPTION_FORCE_UPDATE); + } + + /** + * Display the liteloader panel over the specified GUI + * + * @param parentScreen + */ + @Override + public void displayLiteLoaderPanel(GuiScreen parentScreen) + { + if (this.isPanelSupportedOnScreen(parentScreen)) + { + this.panelHost = new GuiLiteLoaderPanel(this.minecraft, parentScreen, this.mods, this.environment, this.properties, + this.configManager, !this.isTabHidden()); + this.minecraft.displayGuiScreen(this.panelHost); + } + } + + @Override + public int getStartupErrorCount() + { + return this.mods.getStartupErrorCount(); + } + + @Override + public int getCriticalErrorCount() + { + return this.mods.getCriticalErrorCount(); + } + + @Override + public void setNotification(String notification) + { + LiteLoaderLogger.debug("Setting notification: " + notification); + this.notification = notification; + + if (this.panelHost != null) + { + this.panelHost.setNotification(notification); + } + } + + private boolean isPanelSupportedOnScreen(GuiScreen guiScreen) + { + return (guiScreen instanceof GuiMainMenu || guiScreen instanceof GuiIngameMenu || guiScreen instanceof GuiOptions); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/PacketEventsClient.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/PacketEventsClient.java new file mode 100644 index 00000000..fdd634c8 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/PacketEventsClient.java @@ -0,0 +1,262 @@ +package com.mumfrey.liteloader.client; + +import com.mojang.realmsclient.dto.RealmsServer; +import com.mumfrey.liteloader.*; +import com.mumfrey.liteloader.common.ducks.IChatPacket; +import com.mumfrey.liteloader.common.transformers.PacketEventInfo; +import com.mumfrey.liteloader.core.ClientPluginChannels; +import com.mumfrey.liteloader.core.InterfaceRegistrationDelegate; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.core.LiteLoaderEventBroker.ReturnValue; +import com.mumfrey.liteloader.core.PacketEvents; +import com.mumfrey.liteloader.core.event.EventCancellationException; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.core.event.HandlerList.ReturnLogicOp; +import com.mumfrey.liteloader.core.runtime.Packets; +import com.mumfrey.liteloader.interfaces.FastIterableDeque; +import com.mumfrey.liteloader.util.ChatUtilities; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.INetHandler; +import net.minecraft.network.Packet; +import net.minecraft.network.login.INetHandlerLoginClient; +import net.minecraft.network.login.server.S02PacketLoginSuccess; +import net.minecraft.network.play.INetHandlerPlayClient; +import net.minecraft.network.play.server.S01PacketJoinGame; +import net.minecraft.network.play.server.S02PacketChat; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.IChatComponent; +import net.minecraft.util.IThreadListener; + +/** + * Client-side packet event handlers + * + * @author Adam Mummery-Smith + */ +public class PacketEventsClient extends PacketEvents +{ + private static RealmsServer joiningRealm; + + private FastIterableDeque joinGameListeners = new HandlerList(JoinGameListener.class); + private FastIterableDeque chatListeners = new HandlerList(ChatListener.class); + private FastIterableDeque chatFilters = new HandlerList(ChatFilter.class, + ReturnLogicOp.AND_BREAK_ON_FALSE); + private FastIterableDeque preJoinGameListeners = new HandlerList(PreJoinGameListener.class, + ReturnLogicOp.AND_BREAK_ON_FALSE); + private FastIterableDeque postLoginListeners = new HandlerList(PostLoginListener.class); + + @Override + public void registerInterfaces(InterfaceRegistrationDelegate delegate) + { + super.registerInterfaces(delegate); + + delegate.registerInterface(JoinGameListener.class); + delegate.registerInterface(ChatListener.class); + delegate.registerInterface(ChatFilter.class); + delegate.registerInterface(PreJoinGameListener.class); + delegate.registerInterface(PostLoginListener.class); + } + + /** + * @param joinGameListener + */ + public void registerJoinGameListener(JoinGameListener joinGameListener) + { + this.joinGameListeners.add(joinGameListener); + } + + /** + * @param chatFilter + */ + public void registerChatFilter(ChatFilter chatFilter) + { + this.chatFilters.add(chatFilter); + } + + /** + * @param chatListener + */ + public void registerChatListener(ChatListener chatListener) + { + if (chatListener instanceof ChatFilter) + { + LiteLoaderLogger.warning("Interface error initialising mod '%1s'. A mod implementing ChatFilter and ChatListener is not supported! " + + "Remove one of these interfaces", chatListener.getName()); + } + else + { + this.chatListeners.add(chatListener); + } + } + + /** + * @param joinGameListener + */ + public void registerPreJoinGameListener(PreJoinGameListener joinGameListener) + { + this.preJoinGameListeners.add(joinGameListener); + } + + /** + * @param postLoginListener + */ + public void registerPostLoginListener(PostLoginListener postLoginListener) + { + this.postLoginListeners.add(postLoginListener); + } + + public static void onJoinRealm(long serverId, RealmsServer server) + { + PacketEventsClient.joiningRealm = server; + } + + @Override + protected IThreadListener getPacketContextListener(Packets.Context context) + { + if (context == Packets.Context.SERVER) + { + return MinecraftServer.getServer(); + } + + return Minecraft.getMinecraft(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.PacketEvents#handlePacket( + * com.mumfrey.liteloader.common.transformers.PacketEventInfo, + * net.minecraft.network.INetHandler, + * net.minecraft.network.play.server.S01PacketJoinGame) + */ + @Override + protected void handlePacket(PacketEventInfo e, INetHandler netHandler, S01PacketJoinGame packet) + { + if (this.preJoinGame(e, netHandler, packet)) + { + return; + } + + ((INetHandlerPlayClient)netHandler).handleJoinGame(packet); + super.handlePacket(e, netHandler, packet); + + this.postJoinGame(e, netHandler, packet); + } + + /** + * @param e + * @param netHandler + * @param packet + * @throws EventCancellationException + */ + private boolean preJoinGame(PacketEventInfo e, INetHandler netHandler, S01PacketJoinGame packet) throws EventCancellationException + { + if (!(netHandler instanceof INetHandlerPlayClient)) + { + return true; + } + + e.cancel(); + + return !this.preJoinGameListeners.all().onPreJoinGame(netHandler, packet); + } + + /** + * @param e + * @param netHandler + * @param packet + */ + private void postJoinGame(PacketEventInfo e, INetHandler netHandler, S01PacketJoinGame packet) + { + this.joinGameListeners.all().onJoinGame(netHandler, packet, Minecraft.getMinecraft().getCurrentServerData(), PacketEventsClient.joiningRealm); + PacketEventsClient.joiningRealm = null; + + ClientPluginChannels clientPluginChannels = LiteLoader.getClientPluginChannels(); + if (clientPluginChannels instanceof ClientPluginChannelsClient) + { + ((ClientPluginChannelsClient)clientPluginChannels).onJoinGame(netHandler, packet); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.PacketEvents#handlePacket( + * com.mumfrey.liteloader.common.transformers.PacketEventInfo, + * net.minecraft.network.INetHandler, + * net.minecraft.network.login.server.S02PacketLoginSuccess) + */ + @Override + protected void handlePacket(PacketEventInfo e, INetHandler netHandler, S02PacketLoginSuccess packet) + { + if (netHandler instanceof INetHandlerLoginClient) + { + INetHandlerLoginClient netHandlerLoginClient = (INetHandlerLoginClient)netHandler; + + ClientPluginChannels clientPluginChannels = LiteLoader.getClientPluginChannels(); + if (clientPluginChannels instanceof ClientPluginChannelsClient) + { + ((ClientPluginChannelsClient)clientPluginChannels).onPostLogin(netHandlerLoginClient, packet); + } + + this.postLoginListeners.all().onPostLogin(netHandlerLoginClient, packet); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.PacketEvents#handlePacket( + * com.mumfrey.liteloader.common.transformers.PacketEventInfo, + * net.minecraft.network.INetHandler, + * net.minecraft.network.play.server.S02PacketChat) + */ + @Override + protected void handlePacket(PacketEventInfo e, INetHandler netHandler, S02PacketChat packet) + { + if (packet.getChatComponent() == null) + { + return; + } + + IChatComponent originalChat = packet.getChatComponent(); + IChatComponent chat = originalChat; + String message = chat.getFormattedText(); + + // Chat filters get a stab at the chat first, if any filter returns false the chat is discarded + for (ChatFilter chatFilter : this.chatFilters) + { + ReturnValue ret = new ReturnValue(); + + if (chatFilter.onChat(chat, message, ret)) + { + if (ret.isSet()) + { + chat = ret.get(); + if (chat == null) + { + chat = new ChatComponentText(""); + } + message = chat.getFormattedText(); + } + } + else + { + e.cancel(); + return; + } + } + + if (chat != originalChat) + { + try + { + chat = ChatUtilities.convertLegacyCodes(chat); + ((IChatPacket)packet).setChatComponent(chat); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + // Chat listeners get the chat if no filter removed it + this.chatListeners.all().onChat(chat, message); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ResourceObserver.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ResourceObserver.java new file mode 100644 index 00000000..49949b3e --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ResourceObserver.java @@ -0,0 +1,117 @@ +package com.mumfrey.liteloader.client; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.IResourcePack; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.api.ModLoadObserver; +import com.mumfrey.liteloader.common.Resources; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.resources.ModResourcePack; +import com.mumfrey.liteloader.resources.ModResourcePackDir; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Observer which handles registering mods on the client as resource packs + * + * @author Adam Mummery-Smith + */ +public class ResourceObserver implements ModLoadObserver +{ + private final Map resourcePacks = new HashMap(); + + public ResourceObserver() + { + } + + @Override + public void onModLoaded(LiteMod mod) + { + } + + @SuppressWarnings("unchecked") + @Override + public void onPostModLoaded(ModInfo> handle) + { + if (!handle.hasContainer()) return; + + LoadableMod container = handle.getContainer(); + String modName = handle.getMod().getName(); + + if (modName == null) return; + + if (container.hasResources()) + { + LiteLoaderLogger.info("Adding \"%s\" to active resource pack set", container.getLocation()); + IResourcePack resourcePack = this.initResourcePack(container, modName); + Resources resources + = (Resources)LiteLoader.getGameEngine().getResources(); + if (resources.registerResourcePack(resourcePack)) + { + LiteLoaderLogger.info("Successfully added \"%s\" to active resource pack set", container.getLocation()); + } + } + } + + public IResourcePack initResourcePack(LoadableMod container, String name) + { + IResourcePack resourcePack = this.getResourcePack(container); + + if (resourcePack == null) + { + if (container.isDirectory()) + { + LiteLoaderLogger.info("Setting up \"%s/%s\" as mod resource pack with identifier \"%s\"", + container.toFile().getParentFile().getName(), container.getName(), name); + resourcePack = new ModResourcePackDir(name, container.toFile()); + } + else + { + LiteLoaderLogger.info("Setting up \"%s\" as mod resource pack with identifier \"%s\"", container.getName(), name); + resourcePack = new ModResourcePack(name, container.toFile()); + } + + this.setResourcePack(container, resourcePack); + } + + return resourcePack; + } + + private IResourcePack getResourcePack(LoadableMod container) + { + String path = container.getLocation(); + return this.resourcePacks.get(path); + } + + private void setResourcePack(LoadableMod container, IResourcePack resourcePack) + { + String path = container.getLocation(); + this.resourcePacks.put(path, resourcePack); + } + + @Override + public void onModLoadFailed(LoadableMod container, String identifier, String reason, Throwable th) + { + } + + @Override + public void onPreInitMod(LiteMod mod) + { + } + + @Override + public void onPostInitMod(LiteMod mod) + { + } + + @Override + public void onMigrateModConfig(LiteMod mod, File newConfigPath, File oldConfigPath) + { + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ResourcesClient.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ResourcesClient.java new file mode 100644 index 00000000..67947d9c --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ResourcesClient.java @@ -0,0 +1,95 @@ +package com.mumfrey.liteloader.client; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.IResourcePack; + +import com.mumfrey.liteloader.client.overlays.IMinecraft; +import com.mumfrey.liteloader.common.LoadingProgress; +import com.mumfrey.liteloader.common.Resources; + +public class ResourcesClient implements Resources +{ + private final Minecraft engine = Minecraft.getMinecraft(); + + /** + * Registered resource packs + */ + private final Map registeredResourcePacks = new HashMap(); + + /** + * True while initialising mods if we need to do a resource manager reload + * once the process is completed. + */ + private boolean pendingResourceReload; + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#refreshResources(boolean) + */ + @Override + public void refreshResources(boolean force) + { + if (this.pendingResourceReload || force) + { + LoadingProgress.setMessage("Reloading Resources..."); + this.pendingResourceReload = false; + this.engine.refreshResources(); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#getResourceManager() + */ + @Override + public IResourceManager getResourceManager() + { + return this.engine.getResourceManager(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#registerResourcePack( + * net.minecraft.client.resources.IResourcePack) + */ + @Override + public boolean registerResourcePack(IResourcePack resourcePack) + { + if (!this.registeredResourcePacks.containsKey(resourcePack.getPackName())) + { + this.pendingResourceReload = true; + + List defaultResourcePacks = ((IMinecraft)this.engine).getDefaultResourcePacks(); + if (!defaultResourcePacks.contains(resourcePack)) + { + defaultResourcePacks.add(resourcePack); + this.registeredResourcePacks.put(resourcePack.getPackName(), resourcePack); + return true; + } + } + + return false; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.common.GameEngine#unRegisterResourcePack( + * net.minecraft.client.resources.IResourcePack) + */ + @Override + public boolean unRegisterResourcePack(IResourcePack resourcePack) + { + if (this.registeredResourcePacks.containsValue(resourcePack)) + { + this.pendingResourceReload = true; + + List defaultResourcePacks = ((IMinecraft)this.engine).getDefaultResourcePacks(); + this.registeredResourcePacks.remove(resourcePack.getPackName()); + defaultResourcePacks.remove(resourcePack); + return true; + } + + return false; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/SoundHandlerReloadInhibitor.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/SoundHandlerReloadInhibitor.java new file mode 100644 index 00000000..60b1a3ee --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/SoundHandlerReloadInhibitor.java @@ -0,0 +1,129 @@ +package com.mumfrey.liteloader.client; + +import java.util.List; + +import com.mumfrey.liteloader.client.ducks.IReloadable; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +import net.minecraft.client.audio.SoundHandler; +import net.minecraft.client.resources.IResourceManagerReloadListener; +import net.minecraft.client.resources.SimpleReloadableResourceManager; + +/** + * Manager object which handles inhibiting the sound handler's reload + * notification at startup. + * + * @author Adam Mummery-Smith + */ +public class SoundHandlerReloadInhibitor +{ + /** + * Resource Manager + */ + private SimpleReloadableResourceManager resourceManager; + + /** + * Sound manager + */ + private SoundHandler soundHandler; + + /** + * True if inhibition is currently active + */ + private boolean inhibited; + + /** + * So that we can re-insert the sound manager at the same index, we store + * the index we remove it from. + */ + private int storedIndex; + + SoundHandlerReloadInhibitor(SimpleReloadableResourceManager resourceManager, SoundHandler soundHandler) + { + this.resourceManager = resourceManager; + this.soundHandler = soundHandler; + } + + /** + * Inhibit the sound manager reload notification + * + * @return true if inhibit was applied + */ + public boolean inhibit() + { + try + { + if (!this.inhibited) + { + List reloadListeners = ((IReloadable)this.resourceManager).getReloadListeners(); + if (reloadListeners != null) + { + this.storedIndex = reloadListeners.indexOf(this.soundHandler); + if (this.storedIndex > -1) + { + LiteLoaderLogger.info("Inhibiting sound handler reload"); + reloadListeners.remove(this.soundHandler); + this.inhibited = true; + return true; + } + } + } + } + catch (Exception ex) + { + LiteLoaderLogger.warning("Error inhibiting sound handler reload"); + } + + return false; + } + + /** + * Remove the sound manager reload inhibit + * + * @param reload True to reload the sound manager now + * @return true if the sound manager was successfully restored + */ + public boolean unInhibit(boolean reload) + { + try + { + if (this.inhibited) + { + List reloadListeners = ((IReloadable)this.resourceManager).getReloadListeners(); + if (reloadListeners != null) + { + if (this.storedIndex > -1) + { + reloadListeners.add(this.storedIndex, this.soundHandler); + } + else + { + reloadListeners.add(this.soundHandler); + } + + LiteLoaderLogger.info("Sound handler reload inhibit removed"); + + if (reload) + { + LiteLoaderLogger.info("Reloading sound handler"); + this.soundHandler.onResourceManagerReload(this.resourceManager); + } + + this.inhibited = false; + return true; + } + } + } + catch (Exception ex) + { + LiteLoaderLogger.warning("Error removing sound handler reload inhibit"); + } + + return false; + } + + public boolean isInhibited() + { + return this.inhibited; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/Translator.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/Translator.java new file mode 100644 index 00000000..795628bc --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/Translator.java @@ -0,0 +1,30 @@ +package com.mumfrey.liteloader.client; + +import net.minecraft.client.resources.I18n; + +import com.mumfrey.liteloader.api.TranslationProvider; + +public class Translator implements TranslationProvider +{ + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.TranslationProvider#translate( + * java.lang.String, java.lang.Object[]) + */ + @Override + public String translate(String key, Object... args) + { + // TODO doesn't currently honour the contract of TranslationProvider::translate, should return null if translation is missing + return I18n.format(key, args); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.TranslationProvider#translate( + * java.lang.String, java.lang.String, java.lang.Object[]) + */ + @Override + public String translate(String locale, String key, Object... args) + { + // TODO doesn't currently honour the contract of TranslationProvider::translate, should return null if translation is missing + return I18n.format(key, args); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderBrandingProvider.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderBrandingProvider.java new file mode 100644 index 00000000..2d519cdf --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderBrandingProvider.java @@ -0,0 +1,141 @@ +package com.mumfrey.liteloader.client.api; + +import java.net.URI; + +import net.minecraft.client.resources.I18n; +import net.minecraft.util.ResourceLocation; + +import com.mumfrey.liteloader.api.BrandingProvider; +import com.mumfrey.liteloader.client.util.render.IconAbsolute; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.util.render.Icon; + +/** + * LiteLoader's branding provider + * + * @author Adam Mummery-Smith + */ +public class LiteLoaderBrandingProvider implements BrandingProvider +{ + public static final int BRANDING_COLOUR = 0xFF4785D1; + + public static final ResourceLocation ABOUT_TEXTURE = new ResourceLocation("liteloader", "textures/gui/about.png"); + + public static final IconAbsolute LOGO_COORDS = new IconAbsolute(LiteLoaderBrandingProvider.ABOUT_TEXTURE, + "logo", 128, 40, 0, 0, 256, 80); + public static final IconAbsolute ICON_COORDS = new IconAbsolute(LiteLoaderBrandingProvider.ABOUT_TEXTURE, + "chicken", 32, 45, 0, 80, 64, 170); + public static final IconAbsolute TWITTER_AVATAR_COORDS = new IconAbsolute(LiteLoaderBrandingProvider.ABOUT_TEXTURE, + "twitter_avatar",32, 32, 192, 80, 256, 144); + + public static final URI LITELOADER_URI = URI.create("http://www.liteloader.com/"); + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getPriority() + */ + @Override + public int getPriority() + { + return -1000; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getDisplayName() + */ + @Override + public String getDisplayName() + { + return "LiteLoader " + I18n.format("gui.about.versiontext", LiteLoader.getVersion()); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getCopyrightText() + */ + @Override + public String getCopyrightText() + { + return "Copyright (c) 2012-2016 Adam Mummery-Smith"; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getHomepage() + */ + @Override + public URI getHomepage() + { + return LiteLoaderBrandingProvider.LITELOADER_URI; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getBrandingColour() + */ + @Override + public int getBrandingColour() + { + return LiteLoaderBrandingProvider.BRANDING_COLOUR; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getLogoResource() + */ + @Override + public ResourceLocation getLogoResource() + { + return LiteLoaderBrandingProvider.ABOUT_TEXTURE; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getLogoCoords() + */ + @Override + public Icon getLogoCoords() + { + return LiteLoaderBrandingProvider.LOGO_COORDS; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getIconResource() + */ + @Override + public ResourceLocation getIconResource() + { + return LiteLoaderBrandingProvider.ABOUT_TEXTURE; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getIconCoords() + */ + @Override + public Icon getIconCoords() + { + return LiteLoaderBrandingProvider.ICON_COORDS; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getTwitterUserName() + */ + @Override + public String getTwitterUserName() + { + return "therealeq2"; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider + * #getTwitterAvatarResource() + */ + @Override + public ResourceLocation getTwitterAvatarResource() + { + return LiteLoaderBrandingProvider.ABOUT_TEXTURE; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.BrandingProvider#getTwitterAvatarCoords() + */ + @Override + public Icon getTwitterAvatarCoords() + { + return LiteLoaderBrandingProvider.TWITTER_AVATAR_COORDS; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderCoreAPIClient.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderCoreAPIClient.java new file mode 100644 index 00000000..5c19bbb1 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderCoreAPIClient.java @@ -0,0 +1,161 @@ +package com.mumfrey.liteloader.client.api; + +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.server.integrated.IntegratedServer; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ObjectArrays; +import com.mumfrey.liteloader.api.CoreProvider; +import com.mumfrey.liteloader.api.CustomisationProvider; +import com.mumfrey.liteloader.api.InterfaceProvider; +import com.mumfrey.liteloader.api.Observer; +import com.mumfrey.liteloader.client.LiteLoaderCoreProviderClient; +import com.mumfrey.liteloader.client.ResourceObserver; +import com.mumfrey.liteloader.client.Translator; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.core.api.LiteLoaderCoreAPI; +import com.mumfrey.liteloader.interfaces.ObjectFactory; +import com.mumfrey.liteloader.messaging.MessageBus; +import com.mumfrey.liteloader.transformers.event.json.ModEvents; + +/** + * Client side of the core API + * + * @author Adam Mummery-Smith + */ +public class LiteLoaderCoreAPIClient extends LiteLoaderCoreAPI +{ + private static final String PKG_LITELOADER_CLIENT = LiteLoaderCoreAPI.PKG_LITELOADER + ".client"; + + private static final String[] requiredTransformers = { + LiteLoaderCoreAPI.PKG_LITELOADER + ".transformers.event.EventProxyTransformer", + LiteLoaderCoreAPI.PKG_LITELOADER + ".launch.LiteLoaderTransformer", + LiteLoaderCoreAPIClient.PKG_LITELOADER_CLIENT + ".transformers.CrashReportTransformer" + }; + + private static final String[] requiredDownstreamTransformers = { + LiteLoaderCoreAPI.PKG_LITELOADER_COMMON + ".transformers.LiteLoaderPacketTransformer", + LiteLoaderCoreAPIClient.PKG_LITELOADER_CLIENT + ".transformers.MinecraftTransformer", + LiteLoaderCoreAPI.PKG_LITELOADER + ".transformers.event.json.ModEventInjectionTransformer" + }; + + private ObjectFactory objectFactory; + + @Override + public String[] getMixinConfigs() + { + String[] commonConfigs = super.getMixinConfigs(); + return ObjectArrays.concat(commonConfigs, new String[] { + "mixins.liteloader.client.json" + }, String.class); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getRequiredTransformers() + */ + @Override + public String[] getRequiredTransformers() + { + return LiteLoaderCoreAPIClient.requiredTransformers; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI + * #getRequiredDownstreamTransformers() + */ + @Override + public String[] getRequiredDownstreamTransformers() + { + return LiteLoaderCoreAPIClient.requiredDownstreamTransformers; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getCustomisationProviders() + */ + @Override + public List getCustomisationProviders() + { + return ImmutableList.of + ( + new LiteLoaderBrandingProvider(), + new LiteLoaderModInfoDecorator(), + new Translator() + ); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getCoreProviders() + */ + @Override + public List getCoreProviders() + { + return ImmutableList.of + ( + new LiteLoaderCoreProviderClient(this.properties), + LiteLoader.getInput() + ); + } + + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getInterfaceProviders() + */ + @Override + public List getInterfaceProviders() + { + ObjectFactory objectFactory = this.getObjectFactory(); + + return ImmutableList.of + ( + objectFactory.getEventBroker(), + objectFactory.getPacketEventBroker(), + objectFactory.getClientPluginChannels(), + objectFactory.getServerPluginChannels(), + MessageBus.getInstance() + ); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getPreInitObservers() + */ + @Override + public List getPreInitObservers() + { + return ImmutableList.of + ( + new ModEvents() + ); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getObservers() + */ + @Override + public List getObservers() + { + ObjectFactory objectFactory = this.getObjectFactory(); + + return ImmutableList.of + ( + new ResourceObserver(), + objectFactory.getPanelManager(), + objectFactory.getEventBroker() + ); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.api.LiteLoaderCoreAPI#getObjectFactory() + */ + @Override + public ObjectFactory getObjectFactory() + { + if (this.objectFactory == null) + { + this.objectFactory = new ObjectFactoryClient(this.environment, this.properties); + } + + return this.objectFactory; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderModInfoDecorator.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderModInfoDecorator.java new file mode 100644 index 00000000..92ab4a32 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/api/LiteLoaderModInfoDecorator.java @@ -0,0 +1,138 @@ +package com.mumfrey.liteloader.client.api; + +import java.util.List; + +import net.minecraft.client.resources.I18n; + +import com.mumfrey.liteloader.api.ModInfoDecorator; +import com.mumfrey.liteloader.client.gui.GuiLiteLoaderPanel; +import com.mumfrey.liteloader.client.gui.modlist.GuiModListPanel; +import com.mumfrey.liteloader.client.util.render.IconAbsolute; +import com.mumfrey.liteloader.client.util.render.IconAbsoluteClickable; +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.util.render.IconTextured; + +/** + * ModInfo decorator + * + * @author Adam Mummery-Smith + */ +public class LiteLoaderModInfoDecorator implements ModInfoDecorator +{ + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ModInfoDecorator + * #addIcons(com.mumfrey.liteloader.core.ModInfo, java.util.List) + */ + @Override + public void addIcons(final ModInfo mod, List icons) + { + if (mod.hasTweakClass()) + { + icons.add(new IconAbsoluteClickable(LiteLoaderBrandingProvider.ABOUT_TEXTURE, + I18n.format("gui.mod.providestweak"), 12, 12, 158, 80, 170, 92) + { + @Override + public void onClicked(Object source, Object container) + { + if (container instanceof GuiModListPanel) + { + ((GuiModListPanel)container).displayModHelpMessage(mod, "gui.mod.providestweak", "gui.mod.help.tweak"); + } + } + }); + } + + if (mod.hasEventTransformers()) + { + icons.add(new IconAbsoluteClickable(LiteLoaderBrandingProvider.ABOUT_TEXTURE, + I18n.format("gui.mod.providesevents"), 12, 12, 170, 92, 182, 104) + { + @Override + public void onClicked(Object source, Object container) + { + if (container instanceof GuiModListPanel) + { + ((GuiModListPanel)container).displayModHelpMessage(mod, "gui.mod.providesevents", "gui.mod.help.events"); + } + } + }); + } + + if (mod.hasClassTransformers()) + { + icons.add(new IconAbsoluteClickable(LiteLoaderBrandingProvider.ABOUT_TEXTURE, + I18n.format("gui.mod.providestransformer"), 12, 12, 170, 80, 182, 92) + { + @Override + public void onClicked(Object source, Object container) + { + if (container instanceof GuiModListPanel) + { + ((GuiModListPanel)container).displayModHelpMessage(mod, "gui.mod.providestransformer", "gui.mod.help.transformer"); + } + } + }); + } + + if (mod.hasMixins()) + { + icons.add(new IconAbsoluteClickable(LiteLoaderBrandingProvider.ABOUT_TEXTURE, + I18n.format("gui.mod.providesmixins"), 12, 12, 122, 104, 134, 116) + { + @Override + public void onClicked(Object source, Object container) + { + if (container instanceof GuiModListPanel) + { + ((GuiModListPanel)container).displayModHelpMessage(mod, "gui.mod.providesmixins", "gui.mod.help.mixins"); + } + } + }); + } + + if (mod.usesAPI()) + { + icons.add(new IconAbsolute(LiteLoaderBrandingProvider.ABOUT_TEXTURE, + I18n.format("gui.mod.usingapi"), 12, 12, 122, 92, 134, 104)); + } + + List startupErrors = mod.getStartupErrors(); + if (startupErrors != null && startupErrors.size() > 0) + { + icons.add(new IconAbsoluteClickable(LiteLoaderBrandingProvider.ABOUT_TEXTURE, + I18n.format("gui.mod.startuperror", startupErrors.size()), 12, 12, 134, 92, 146, 104) + { + @Override + public void onClicked(Object source, Object container) + { + if (source instanceof GuiLiteLoaderPanel) + { + ((GuiLiteLoaderPanel)source).showErrorPanel(mod); + } + } + }); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ModInfoDecorator + * #modifyStatusText(com.mumfrey.liteloader.core.ModInfo, + * java.lang.String) + */ + @Override + public String modifyStatusText(ModInfo mod, String statusText) + { + return null; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ModInfoDecorator + * #onDrawListEntry(int, int, float, int, int, int, int, boolean, + * com.mumfrey.liteloader.core.ModInfo, int, int, int) + */ + @Override + public void onDrawListEntry(int mouseX, int mouseY, float partialTicks, int xPosition, int yPosition, int width, int height, boolean selected, + ModInfo mod, int gradientColour, int titleColour, int statusColour) + { + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/api/ObjectFactoryClient.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/api/ObjectFactoryClient.java new file mode 100644 index 00000000..9fbb7342 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/api/ObjectFactoryClient.java @@ -0,0 +1,164 @@ +package com.mumfrey.liteloader.client.api; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.launchwrapper.Launch; +import net.minecraft.server.integrated.IntegratedServer; + +import com.mumfrey.liteloader.client.LiteLoaderEventBrokerClient; +import com.mumfrey.liteloader.client.ClientPluginChannelsClient; +import com.mumfrey.liteloader.client.GameEngineClient; +import com.mumfrey.liteloader.client.LiteLoaderPanelManager; +import com.mumfrey.liteloader.client.PacketEventsClient; +import com.mumfrey.liteloader.client.gui.startup.LoadingBar; +import com.mumfrey.liteloader.common.GameEngine; +import com.mumfrey.liteloader.core.ClientPluginChannels; +import com.mumfrey.liteloader.core.LiteLoaderEventBroker; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.core.PacketEvents; +import com.mumfrey.liteloader.core.ServerPluginChannels; +import com.mumfrey.liteloader.interfaces.PanelManager; +import com.mumfrey.liteloader.interfaces.ObjectFactory; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.permissions.PermissionsManagerClient; +import com.mumfrey.liteloader.permissions.PermissionsManagerServer; +import com.mumfrey.liteloader.util.Input; +import com.mumfrey.liteloader.util.InputManager; + +/** + * Factory for lifetime loader objects for the client side + * + * @author Adam Mummery-Smith + */ +class ObjectFactoryClient implements ObjectFactory +{ + private LoaderEnvironment environment; + + private LoaderProperties properties; + + private Input input; + + private LiteLoaderEventBrokerClient clientEvents; + + private PacketEventsClient clientPacketEvents; + + private GameEngineClient engine; + + private PanelManager modPanelManager; + + private ClientPluginChannelsClient clientPluginChannels; + + private ServerPluginChannels serverPluginChannels; + + ObjectFactoryClient(LoaderEnvironment environment, LoaderProperties properties) + { + this.environment = environment; + this.properties = properties; + } + + @Override + public Input getInput() + { + if (this.input == null) + { + this.input = new InputManager(this.environment, this.properties); + } + + return this.input; + } + + @Override + public LiteLoaderEventBroker getEventBroker() + { + if (this.clientEvents == null) + { + this.clientEvents = new LiteLoaderEventBrokerClient(LiteLoader.getInstance(), (GameEngineClient)this.getGameEngine(), this.properties); + } + + return this.clientEvents; + } + + @Override + public PacketEvents getPacketEventBroker() + { + if (this.clientPacketEvents == null) + { + this.clientPacketEvents = new PacketEventsClient(); + } + + return this.clientPacketEvents; + } + + @Override + public GameEngine getGameEngine() + { + if (this.engine == null) + { + this.engine = new GameEngineClient(); + } + + return this.engine; + } + + @Override + public PanelManager getPanelManager() + { + if (this.modPanelManager == null) + { + this.modPanelManager = new LiteLoaderPanelManager(this.getGameEngine(), this.environment, this.properties); + } + + return this.modPanelManager; + } + + @Override + public ClientPluginChannels getClientPluginChannels() + { + if (this.clientPluginChannels == null) + { + this.clientPluginChannels = new ClientPluginChannelsClient(); + } + + return this.clientPluginChannels; + } + + @Override + public ServerPluginChannels getServerPluginChannels() + { + if (this.serverPluginChannels == null) + { + this.serverPluginChannels = new ServerPluginChannels(); + } + + return this.serverPluginChannels; + } + + @Override + public PermissionsManagerClient getClientPermissionManager() + { + return PermissionsManagerClient.getInstance(); + } + + @Override + public PermissionsManagerServer getServerPermissionManager() + { + return null; + } + + @SuppressWarnings("unused") + @Override + public void preBeginGame() + { + try + { + Class progressManagerClass = Class.forName("net.minecraftforge.fml.common.ProgressManager", false, Launch.classLoader); + return; // Disable my loading bar if Forge's is present + } + catch (ClassNotFoundException ex) + { + } + + new LoadingBar(); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IClientNetLoginHandler.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IClientNetLoginHandler.java new file mode 100644 index 00000000..48ba7442 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IClientNetLoginHandler.java @@ -0,0 +1,8 @@ +package com.mumfrey.liteloader.client.ducks; + +import net.minecraft.network.NetworkManager; + +public interface IClientNetLoginHandler +{ + public abstract NetworkManager getNetMgr(); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IFramebuffer.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IFramebuffer.java new file mode 100644 index 00000000..ee7690b7 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IFramebuffer.java @@ -0,0 +1,8 @@ +package com.mumfrey.liteloader.client.ducks; + +public interface IFramebuffer +{ + public abstract IFramebuffer setDispatchRenderEvent(boolean dispatchRenderEvent); + + public abstract boolean isDispatchRenderEvent(); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/INamespacedRegistry.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/INamespacedRegistry.java new file mode 100644 index 00000000..c0b24962 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/INamespacedRegistry.java @@ -0,0 +1,6 @@ +package com.mumfrey.liteloader.client.ducks; + +public interface INamespacedRegistry +{ + public abstract IObjectIntIdentityMap getUnderlyingMap(); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IObjectIntIdentityMap.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IObjectIntIdentityMap.java new file mode 100644 index 00000000..b468384c --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IObjectIntIdentityMap.java @@ -0,0 +1,11 @@ +package com.mumfrey.liteloader.client.ducks; + +import java.util.IdentityHashMap; +import java.util.List; + +public interface IObjectIntIdentityMap +{ + public abstract IdentityHashMap getIdentityMap(); + + public abstract List getObjectList(); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IRegistrySimple.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IRegistrySimple.java new file mode 100644 index 00000000..8567969d --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IRegistrySimple.java @@ -0,0 +1,8 @@ +package com.mumfrey.liteloader.client.ducks; + +import java.util.Map; + +public interface IRegistrySimple +{ + public abstract Map getRegistryObjects(); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IReloadable.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IReloadable.java new file mode 100644 index 00000000..31b8acf0 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IReloadable.java @@ -0,0 +1,10 @@ +package com.mumfrey.liteloader.client.ducks; + +import java.util.List; + +import net.minecraft.client.resources.IResourceManagerReloadListener; + +public interface IReloadable +{ + public abstract List getReloadListeners(); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IRenderManager.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IRenderManager.java new file mode 100644 index 00000000..03138cdb --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/IRenderManager.java @@ -0,0 +1,11 @@ +package com.mumfrey.liteloader.client.ducks; + +import java.util.Map; + +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.entity.Entity; + +public interface IRenderManager +{ + public abstract Map, Render> getRenderMap(); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/ITileEntityRendererDispatcher.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/ITileEntityRendererDispatcher.java new file mode 100644 index 00000000..fcb1dc07 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/ducks/ITileEntityRendererDispatcher.java @@ -0,0 +1,11 @@ +package com.mumfrey.liteloader.client.ducks; + +import java.util.Map; + +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer; +import net.minecraft.tileentity.TileEntity; + +public interface ITileEntityRendererDispatcher +{ + public abstract Map, TileEntitySpecialRenderer> getSpecialRenderMap(); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiCheckbox.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiCheckbox.java new file mode 100644 index 00000000..5ed29f11 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiCheckbox.java @@ -0,0 +1,51 @@ +package com.mumfrey.liteloader.client.gui; + +import static com.mumfrey.liteloader.gl.GL.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; + +import com.mumfrey.liteloader.client.api.LiteLoaderBrandingProvider; + +/** + * Super-simple implementation of a checkbox control + * + * @author Adam Mummery-Smith + */ +public class GuiCheckbox extends GuiButton +{ + public boolean checked; + + public GuiCheckbox(int controlId, int xPosition, int yPosition, String displayString) + { + super(controlId, xPosition, yPosition, Minecraft.getMinecraft().fontRendererObj.getStringWidth(displayString) + 16, 12, displayString); + } + + @Override + public void drawButton(Minecraft minecraft, int mouseX, int mouseY) + { + if (this.visible) + { + minecraft.getTextureManager().bindTexture(LiteLoaderBrandingProvider.ABOUT_TEXTURE); + glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + this.hovered = mouseX >= this.xPosition + && mouseY >= this.yPosition + && mouseX < this.xPosition + this.width + && mouseY < this.yPosition + this.height; + + this.drawTexturedModalRect(this.xPosition, this.yPosition, this.checked ? 134 : 122, 80, 12, 12); + this.mouseDragged(minecraft, mouseX, mouseY); + + int colour = 0xE0E0E0; + if (!this.enabled) + { + colour = 0xA0A0A0; + } + else if (this.hovered) + { + colour = 0xFFFFA0; + } + + this.drawString(minecraft.fontRendererObj, this.displayString, this.xPosition + 16, this.yPosition + 2, colour); + } + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiHoverLabel.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiHoverLabel.java new file mode 100644 index 00000000..efc4da6e --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiHoverLabel.java @@ -0,0 +1,55 @@ +package com.mumfrey.liteloader.client.gui; + +import com.mumfrey.liteloader.client.api.LiteLoaderBrandingProvider; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; + +public class GuiHoverLabel extends GuiButton +{ + private FontRenderer fontRenderer; + private int colour; + private int hoverColour; + + public GuiHoverLabel(int id, int xPosition, int yPosition, FontRenderer fontRenderer, String displayText) + { + this(id, xPosition, yPosition, fontRenderer, displayText, LiteLoaderBrandingProvider.BRANDING_COLOUR); + } + + public GuiHoverLabel(int id, int xPosition, int yPosition, FontRenderer fontRenderer, String displayText, int colour) + { + this(id, xPosition, yPosition, fontRenderer, displayText, colour, 0xFFFFFFAA); + } + + public GuiHoverLabel(int id, int xPosition, int yPosition, FontRenderer fontRenderer, String displayText, int colour, int hoverColour) + { + super(id, xPosition, yPosition, GuiHoverLabel.getStringWidth(fontRenderer, displayText), 8, displayText); + + this.fontRenderer = fontRenderer; + this.colour = colour; + this.hoverColour = hoverColour; + } + + @Override + public void drawButton(Minecraft minecraft, int mouseX, int mouseY) + { + if (this.visible) + { + this.hovered = mouseX >= this.xPosition + && mouseY >= this.yPosition + && mouseX < this.xPosition + this.width + && mouseY < this.yPosition + this.height; + this.fontRenderer.drawString(this.displayString, this.xPosition, this.yPosition, this.hovered ? this.hoverColour : this.colour); + } + else + { + this.hovered = false; + } + } + + private static int getStringWidth(FontRenderer fontRenderer, String text) + { + return fontRenderer.getStringWidth(text); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiLiteLoaderPanel.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiLiteLoaderPanel.java new file mode 100644 index 00000000..87051adb --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiLiteLoaderPanel.java @@ -0,0 +1,815 @@ +package com.mumfrey.liteloader.client.gui; + +import static com.mumfrey.liteloader.gl.GL.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.ResourceLocation; + +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.api.BrandingProvider; +import com.mumfrey.liteloader.api.LiteAPI; +import com.mumfrey.liteloader.api.ModInfoDecorator; +import com.mumfrey.liteloader.client.api.LiteLoaderBrandingProvider; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.core.LiteLoaderMods; +import com.mumfrey.liteloader.core.LiteLoaderVersion; +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.core.api.LiteLoaderCoreAPI; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.modconfig.ConfigManager; +import com.mumfrey.liteloader.modconfig.ConfigPanel; +import com.mumfrey.liteloader.util.render.Icon; + +/** + * GUI screen which displays info about loaded mods and also allows them to be + * enabled and disabled. An instance of this class is created every time the + * main menu is displayed and is drawn as an overlay until the tab is clicked, + * at which point it becomes the active GUI screen and draws the parent main + * menu screen as its background to give the appearance of being overlaid on the + * main menu. + * + * @author Adam Mummery-Smith + */ +public class GuiLiteLoaderPanel extends GuiScreen +{ + static final int WHITE = 0xFFFFFFFF; + static final int OPAQUE = 0xFF000000; + static final int NOTIFICATION_TOOLTIP_FOREGROUND = 0xFFFFFF; + static final int NOTIFICATION_TOOLTIP_BACKGROUND = 0xB0000099; + static final int ERROR_TOOLTIP_FOREGROUND = 0xFF5555; + static final int ERROR_TOOLTIP_BACKGROUND = 0xB0330000; + static final int HEADER_HR_COLOUR = 0xFF999999; + static final int HEADER_TEXT_COLOUR = GuiLiteLoaderPanel.WHITE; + static final int HEADER_TEXT_COLOUR_SUB = 0xFFAAAAAA; + static final int TOOLTIP_FOREGROUND = 0xFFFFFF; + static final int TOOLTIP_FOREGROUND_SUB = 0xCCCCCC; + static final int TOOLTIP_BACKGROUND = 0xB0000000; + + static final int LEFT_EDGE = 80; + static final int MARGIN = 12; + static final int TAB_WIDTH = 20; + static final int TAB_HEIGHT = 40; + static final int TAB_TOP = 20; + static final int PANEL_TOP = 83; + static final int PANEL_BOTTOM = 26; + + private static final double TWEEN_RATE = 0.08; + + private static boolean displayErrorToolTip = true; + + /** + * Reference to the main menu which this screen is either overlaying or + * using as its background. + */ + private GuiScreen parentScreen; + + @SuppressWarnings("unused") + private final LoaderEnvironment environment; + + private final LoaderProperties properties; + + /** + * Tick number (update counter) used for tweening + */ + private long tickNumber; + + /** + * Last tick number, for tweening + */ + private double lastTick; + + /** + * Current tween percentage (0.0 -> 1.0) + */ + private double tweenAmount = 0.0; + + /** + * Since we don't get real mouse events we have to simulate them by tracking + * the mouse state. + */ + private boolean mouseDown, toggled, toggleable; + + /** + * Hover opacity for the tab + */ + private float tabOpacity = 0.0F; + + private boolean showTab = true; + + /** + * Text to display under the header + */ + private String activeModText, versionText; + + /** + * Configuration panel + */ + private GuiPanel currentPanel; + + private final GuiPanelMods modsPanel; + private final GuiPanelSettings settingsPanel; + + private int brandColour = LiteLoaderBrandingProvider.BRANDING_COLOUR; + + private ResourceLocation logoResource = LiteLoaderBrandingProvider.ABOUT_TEXTURE; + private Icon logoCoords = LiteLoaderBrandingProvider.LOGO_COORDS; + + private ResourceLocation iconResource = LiteLoaderBrandingProvider.ABOUT_TEXTURE; + private Icon iconCoords = LiteLoaderBrandingProvider.ICON_COORDS; + + private List modInfoDecorators = new ArrayList(); + + private boolean mouseOverLogo = false; + + private int startupErrorCount = 0, criticalErrorCount = 0; + + private String notification; + + private boolean isSnapshot; + + /** + * @param minecraft + * @param parentScreen + * @param mods + */ + public GuiLiteLoaderPanel(Minecraft minecraft, GuiScreen parentScreen, LiteLoaderMods mods, LoaderEnvironment environment, + LoaderProperties properties, ConfigManager configManager, boolean showTab) + { + this.mc = minecraft; + this.fontRendererObj = minecraft.fontRendererObj; + this.parentScreen = parentScreen; + this.showTab = showTab; + + this.environment = environment; + this.properties = properties; + + this.toggleable = true; + + this.versionText = I18n.format("gui.about.versiontext", LiteLoader.getVersion()); + this.activeModText = I18n.format("gui.about.modsloaded", mods.getLoadedMods().size()); + + this.initBranding(); + + this.currentPanel = this.modsPanel = new GuiPanelMods(this, minecraft, mods, environment, configManager, + this.brandColour, this.modInfoDecorators); + this.settingsPanel = new GuiPanelSettings(this, minecraft); + + this.startupErrorCount = mods.getStartupErrorCount(); + this.criticalErrorCount = mods.getCriticalErrorCount(); + + String branding = LiteLoader.getBranding(); + if (branding != null && branding.contains("SNAPSHOT")) + { + this.isSnapshot = true; + this.versionText = "\247c" + branding; + } + } + + /** + * + */ + private void initBranding() + { + LiteAPI logoProvider = null; + + int brandingColourProviderPriority = Integer.MIN_VALUE; + int logoProviderPriority = Integer.MIN_VALUE; + int iconProviderPriority = Integer.MIN_VALUE; + + for (LiteAPI api : LiteLoader.getAPIs()) + { + BrandingProvider brandingProvider = LiteLoader.getCustomisationProvider(api, BrandingProvider.class); + if (brandingProvider != null) + { + if (brandingProvider.getBrandingColour() != 0 && brandingProvider.getPriority() > brandingColourProviderPriority) + { + brandingColourProviderPriority = brandingProvider.getPriority(); + this.brandColour = GuiLiteLoaderPanel.OPAQUE | brandingProvider.getBrandingColour(); + } + + ResourceLocation logoResource = brandingProvider.getLogoResource(); + Icon logoCoords = brandingProvider.getLogoCoords(); + if (logoResource != null && logoCoords != null && brandingProvider.getPriority() > logoProviderPriority) + { + logoProvider = api; + logoProviderPriority = brandingProvider.getPriority(); + this.logoResource = logoResource; + this.logoCoords = logoCoords; + } + + ResourceLocation iconResource = brandingProvider.getIconResource(); + Icon iconCoords = brandingProvider.getIconCoords(); + if (iconResource != null && iconCoords != null && brandingProvider.getPriority() > iconProviderPriority) + { + iconProviderPriority = brandingProvider.getPriority(); + this.iconResource = iconResource; + this.iconCoords = iconCoords; + } + } + + ModInfoDecorator modInfoDecorator = LiteLoader.getCustomisationProvider(api, ModInfoDecorator.class); + if (modInfoDecorator != null) + { + this.modInfoDecorators.add(modInfoDecorator); + } + } + + if (logoProvider != null && !LiteLoaderCoreAPI.class.isAssignableFrom(logoProvider.getClass())) + { + this.versionText = I18n.format("gui.about.poweredbyversion", logoProvider.getVersion(), LiteLoader.getVersion()); + } + } + + /** + * Get the computed branding colour + */ + public int getBrandColour() + { + return this.brandColour; + } + + /** + * Release references prior to being disposed + */ + public void release() + { + this.parentScreen = null; + } + + /** + * Get the parent menu + */ + public GuiScreen getScreen() + { + return this.parentScreen; + } + + /** + * Return true if the panel is not fully closed (tweening or open) + */ + public boolean isOpen() + { + return this.tweenAmount > 0.0; + } + + public void setToggleable(boolean toggleable) + { + this.toggleable = toggleable; + } + + public void setNotification(String notification) + { + this.notification = notification; + } + + /* (non-Javadoc) + * @see net.minecraft.client.gui.GuiScreen#initGui() + */ + @SuppressWarnings("unchecked") + @Override + public void initGui() + { + // Hide the tooltip once the user opens the panel + GuiLiteLoaderPanel.displayErrorToolTip = false; + + this.currentPanel.setSize(this.width - LEFT_EDGE, this.height); + + this.buttonList.add(new GuiHoverLabel(2, LEFT_EDGE + MARGIN, this.height - PANEL_BOTTOM + 9, this.fontRendererObj, + I18n.format("gui.about.taboptions"), this.brandColour)); + + if (LiteLoaderVersion.getUpdateSite().canCheckForUpdate() && this.mc.theWorld == null && !this.isSnapshot) + { + this.buttonList.add(new GuiHoverLabel(3, LEFT_EDGE + MARGIN + 38 + this.fontRendererObj.getStringWidth(this.versionText) + 6, 50, + this.fontRendererObj, I18n.format("gui.about.checkupdates"), this.brandColour)); + } + + Keyboard.enableRepeatEvents(true); + } + + @Override + public void onGuiClosed() + { + Keyboard.enableRepeatEvents(false); + } + + /* (non-Javadoc) + * @see net.minecraft.client.gui.GuiScreen + * #setWorldAndResolution(net.minecraft.client.Minecraft, int, int) + */ + @Override + public void setWorldAndResolution(Minecraft minecraft, int width, int height) + { + if (this.mc.currentScreen == this) + { + // Set res in parent screen if we are the active GUI + this.parentScreen.setWorldAndResolution(minecraft, width, height); + } + + super.setWorldAndResolution(minecraft, width, height); + } + + /* (non-Javadoc) + * @see net.minecraft.client.gui.GuiScreen#updateScreen() + */ + @Override + public void updateScreen() + { + this.currentPanel.onTick(); + + this.tickNumber++; + + if (this.mc.currentScreen == this) + { + this.mc.currentScreen = this.parentScreen; + this.parentScreen.updateScreen(); + this.mc.currentScreen = this; + } + + if (this.toggled && this.toggleable) + { + this.onToggled(); + } + } + + /* (non-Javadoc) + * @see net.minecraft.client.gui.GuiScreen#drawScreen(int, int, float) + */ + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) + { + this.drawScreen(mouseX, mouseY, partialTicks, false); + } + + /** + * @param mouseX + * @param mouseY + * @param partialTicks + * @param alwaysExpandTab + */ + public void drawScreen(int mouseX, int mouseY, float partialTicks, boolean alwaysExpandTab) + { + boolean active = this.mc.currentScreen == this; + + if (active) + { + // Draw the parent screen as our background if we are the active screen + glClear(GL_DEPTH_BUFFER_BIT); + this.parentScreen.drawScreen(-10, -10, partialTicks); + glClear(GL_DEPTH_BUFFER_BIT); + } + else + { + // If this is not the active screen, copy the width and height from the parent GUI + this.width = this.parentScreen.width; + this.height = this.parentScreen.height; + } + + // Calculate the current tween position + float xOffset = (this.width - LEFT_EDGE) * this.calcTween(partialTicks, active) + 16.0F + (this.tabOpacity * -32.0F); + int offsetMouseX = mouseX - (int)xOffset; + + // Handle mouse stuff here since we won't get mouse events when not the active GUI + boolean mouseOverTab = this.showTab && (offsetMouseX > LEFT_EDGE - TAB_WIDTH + && offsetMouseX < LEFT_EDGE + && mouseY > TAB_TOP + && mouseY < TAB_TOP + TAB_HEIGHT); + this.handleMouseClick(offsetMouseX, mouseY, partialTicks, active, mouseOverTab); + + // Calculate the tab opacity, not framerate adjusted because we don't really care + this.tabOpacity = mouseOverTab || alwaysExpandTab || this.startupErrorCount > 0 || this.notification != null + || this.isOpen() ? 0.5F : Math.max(0.0F, this.tabOpacity - partialTicks * 0.1F); + + // Draw the panel contents + this.drawPanel(offsetMouseX, mouseY, partialTicks, active, xOffset); + this.drawTooltips(mouseX, mouseY, partialTicks, active, xOffset, mouseOverTab); + } + + /** + * @param mouseX + * @param mouseY + * @param partialTicks + * @param active + * @param xOffset + */ + private void drawPanel(int mouseX, int mouseY, float partialTicks, boolean active, float xOffset) + { + this.mouseOverLogo = false; + + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); + + glPushMatrix(); + glTranslatef(xOffset, 0.0F, 0.0F); + + // Draw the background and left edge + drawRect(LEFT_EDGE, 0, this.width, this.height, GuiLiteLoaderPanel.TOOLTIP_BACKGROUND); + + if (this.showTab) + { + drawRect(LEFT_EDGE, 0, LEFT_EDGE + 1, TAB_TOP, GuiLiteLoaderPanel.WHITE); + drawRect(LEFT_EDGE, TAB_TOP + TAB_HEIGHT, LEFT_EDGE + 1, this.height, GuiLiteLoaderPanel.WHITE); + + this.mc.getTextureManager().bindTexture(LiteLoaderBrandingProvider.ABOUT_TEXTURE); + glDrawTexturedRect(LEFT_EDGE - TAB_WIDTH, TAB_TOP, TAB_WIDTH + 1, TAB_HEIGHT, 80, 80, 122, 160, 0.5F + this.tabOpacity); + if (this.startupErrorCount > 0) + { + glDrawTexturedRect(LEFT_EDGE - TAB_WIDTH + 7, TAB_TOP + 2, 12, 12, 134, 92, 134 + 12, 92 + 12, 0.5F + this.tabOpacity); + } + else if (this.notification != null) + { + glDrawTexturedRect(LEFT_EDGE - TAB_WIDTH + 7, TAB_TOP + 2, 12, 12, 134 + 12, 92, 134 + 24, 92 + 12, 0.5F + this.tabOpacity); + } + } + else + { + drawRect(LEFT_EDGE, 0, LEFT_EDGE + 1, this.height, GuiLiteLoaderPanel.WHITE); + } + + // Only draw the panel contents if we are actually open + if (this.isOpen()) + { + if (this.currentPanel.isCloseRequested()) + { + this.closeCurrentPanel(); + } + + this.drawCurrentPanel(mouseX, mouseY, partialTicks); + + if (!this.currentPanel.stealFocus()) + { + // Draw other controls inside the transform so that they slide properly + super.drawScreen(mouseX, mouseY, partialTicks); + } + } + else + { + this.closeCurrentPanel(); + } + + glPopMatrix(); + } + + /** + * @param mouseX + * @param mouseY + * @param partialTicks + */ + private void drawCurrentPanel(int mouseX, int mouseY, float partialTicks) + { + glPushMatrix(); + glTranslatef(LEFT_EDGE, 0, 0); + + this.currentPanel.draw(mouseX - LEFT_EDGE, mouseY, partialTicks); + + glPopMatrix(); + } + + /** + * @param mouseX + * @param mouseY + * @param partialTicks + */ + protected boolean drawInfoPanel(int mouseX, int mouseY, float partialTicks, int left, int bottom) + { + int right = this.width - MARGIN - LEFT_EDGE + left; + left += MARGIN; + + // Draw the header pieces + this.mc.getTextureManager().bindTexture(this.logoResource); + glDrawTexturedRect(left, MARGIN, this.logoCoords, 1.0F); + this.mc.getTextureManager().bindTexture(this.iconResource); + glDrawTexturedRect(right - this.iconCoords.getIconWidth(), MARGIN, this.iconCoords, 1.0F); + + // Draw header text + this.fontRendererObj.drawString(this.versionText, left + 38, 50, GuiLiteLoaderPanel.HEADER_TEXT_COLOUR); + this.fontRendererObj.drawString(this.activeModText, left + 38, 60, GuiLiteLoaderPanel.HEADER_TEXT_COLOUR_SUB); + + // Draw top and bottom horizontal rules + drawRect(left, 80, right, 81, GuiLiteLoaderPanel.HEADER_HR_COLOUR); + drawRect(left, this.height - bottom + 2, right, this.height - bottom + 3, GuiLiteLoaderPanel.HEADER_HR_COLOUR); + + this.mouseOverLogo = (mouseY > MARGIN && mouseY < MARGIN + this.logoCoords.getIconHeight() + && mouseX > left && mouseX < left + this.logoCoords.getIconWidth()); + return this.mouseOverLogo; + } + + private void drawTooltips(int mouseX, int mouseY, float partialTicks, boolean active, float xOffset, boolean mouseOverTab) + { + boolean annoyingTip = this.startupErrorCount > 0 || this.notification != null; + + if (mouseOverTab && this.tweenAmount < 0.01) + { + GuiLiteLoaderPanel.drawTooltip(this.fontRendererObj, LiteLoader.getVersionDisplayString(), mouseX, mouseY, this.width, this.height, + GuiLiteLoaderPanel.TOOLTIP_FOREGROUND, GuiLiteLoaderPanel.TOOLTIP_BACKGROUND); + GuiLiteLoaderPanel.drawTooltip(this.fontRendererObj, this.activeModText, mouseX, mouseY + 13, this.width, this.height, + GuiLiteLoaderPanel.TOOLTIP_FOREGROUND_SUB, GuiLiteLoaderPanel.TOOLTIP_BACKGROUND); + + if (annoyingTip) + { + this.drawNotificationTooltip(mouseX, mouseY - 13); + } + } + else if (GuiLiteLoaderPanel.displayErrorToolTip && annoyingTip && !active && this.parentScreen instanceof GuiMainMenu) + { + this.drawNotificationTooltip((int)xOffset + LEFT_EDGE - 12, TAB_TOP + 2); + } + } + + private void drawNotificationTooltip(int left, int top) + { + if (this.startupErrorCount > 0) + { + GuiLiteLoaderPanel.drawTooltip(this.fontRendererObj, I18n.format("gui.error.tooltip", this.startupErrorCount, this.criticalErrorCount), + left, top, this.width, this.height, GuiLiteLoaderPanel.ERROR_TOOLTIP_FOREGROUND, GuiLiteLoaderPanel.ERROR_TOOLTIP_BACKGROUND); + } + else if (this.notification != null) + { + GuiLiteLoaderPanel.drawTooltip(this.fontRendererObj, this.notification, left, top, this.width, this.height, + GuiLiteLoaderPanel.NOTIFICATION_TOOLTIP_FOREGROUND, GuiLiteLoaderPanel.NOTIFICATION_TOOLTIP_BACKGROUND); + } + } + + /* (non-Javadoc) + * @see net.minecraft.client.gui.GuiScreen + * #actionPerformed(net.minecraft.client.gui.GuiButton) + */ + @Override + protected void actionPerformed(GuiButton button) + { + if (button.id == 2) + { + this.setCurrentPanel(this.settingsPanel); + } + + if (button.id == 3) + { + this.setCurrentPanel(new GuiPanelUpdateCheck(this, this.mc, LiteLoaderVersion.getUpdateSite(), "LiteLoader", this.properties)); + } + } + + /* (non-Javadoc) + * @see net.minecraft.client.gui.GuiScreen#keyTyped(char, int) + */ + @Override + protected void keyTyped(char keyChar, int keyCode) + { + this.currentPanel.keyPressed(keyChar, keyCode); + } + + /** + * + */ + void showLogPanel() + { + this.setCurrentPanel(new GuiPanelLiteLoaderLog(this.mc, this)); + } + + /** + * + */ + void showAboutPanel() + { + this.setCurrentPanel(new GuiPanelAbout(this.mc, this)); + } + + public void showErrorPanel(ModInfo mod) + { + this.setCurrentPanel(new GuiPanelError(this.mc, this, mod)); + } + + /* (non-Javadoc) + * @see net.minecraft.client.gui.GuiScreen#mouseClicked(int, int, int) + */ + @Override + protected void mouseClicked(int mouseX, int mouseY, int button) throws IOException + { + this.currentPanel.mousePressed(mouseX - LEFT_EDGE, mouseY, button); + + if (button == 0 && this.mouseOverLogo && !this.currentPanel.stealFocus()) + { + this.showAboutPanel(); + } + + if (!this.currentPanel.stealFocus()) + { + super.mouseClicked(mouseX, mouseY, button); + } + } + + /* (non-Javadoc) + * @see net.minecraft.client.gui.GuiScreen#mouseReleased(int, int, int) + */ + @Override + protected void mouseReleased(int mouseX, int mouseY, int button) + { + if (button == -1) + { + this.currentPanel.mouseMoved(mouseX - LEFT_EDGE, mouseY); + } + else + { + this.currentPanel.mouseReleased(mouseX - LEFT_EDGE, mouseY, button); + } + + if (!this.currentPanel.stealFocus()) + { + super.mouseReleased(mouseX, mouseY, button); + } + } + + /* (non-Javadoc) + * @see net.minecraft.client.gui.GuiScreen#handleMouseInput() + */ + @Override + public void handleMouseInput() throws IOException + { + int mouseWheelDelta = Mouse.getEventDWheel(); + if (mouseWheelDelta != 0) + { + this.currentPanel.mouseWheelScrolled(mouseWheelDelta); + } + + super.handleMouseInput(); + } + + /** + * @param mouseX + * @param active + * @param mouseOverTab + */ + public void handleMouseClick(int mouseX, int mouseY, float partialTicks, boolean active, boolean mouseOverTab) + { + boolean mouseDown = Mouse.isButtonDown(0); + if (((active && mouseX < LEFT_EDGE && this.tweenAmount > 0.75) || mouseOverTab) && !this.mouseDown && mouseDown) + { + this.mouseDown = true; + this.toggled = true; + } + else if (this.mouseDown && !mouseDown) + { + this.mouseDown = false; + } + } + + /** + * @param partialTicks + * @param active + */ + private float calcTween(float partialTicks, boolean active) + { + double tickValue = this.tickNumber + partialTicks; + + if (active && this.tweenAmount < 1.0) + { + this.tweenAmount = Math.min(1.0, this.tweenAmount + ((tickValue - this.lastTick) * TWEEN_RATE)); + } + else if (!active && this.isOpen()) + { + this.tweenAmount = Math.max(0.0, this.tweenAmount - ((tickValue - this.lastTick) * TWEEN_RATE)); + } + + this.lastTick = tickValue; + return 1.0F - (float)Math.sin(this.tweenAmount * 0.5 * Math.PI); + } + + /** + * Called when the tab is clicked + */ + void onToggled() + { + this.toggled = false; + this.mc.displayGuiScreen(this.mc.currentScreen == this ? this.parentScreen : this); + } + + /** + * Callback for the "config" button, display the config panel for the + * currently selected mod. + */ + void openConfigPanel(ConfigPanel panel, LiteMod mod) + { + if (panel != null) + { + this.setCurrentPanel(new GuiPanelConfigContainer(this.mc, panel, mod)); + } + } + + /** + * @param newPanel + */ + private void setCurrentPanel(GuiPanel newPanel) + { + this.closeCurrentPanel(); + + this.currentPanel = newPanel; + this.currentPanel.setSize(this.width - LEFT_EDGE, this.height); + this.currentPanel.onShown(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.modconfig.ConfigPanelHost#close() + */ + private void closeCurrentPanel() + { + this.currentPanel.onHidden(); + this.currentPanel = this.modsPanel; + this.modsPanel.setSize(this.width - LEFT_EDGE, this.height); + } + + /** + * Draw a tooltip at the specified location and clip to screenWidth and + * screenHeight + * + * @param fontRenderer + * @param tooltipText + * @param mouseX + * @param mouseY + * @param screenWidth + * @param screenHeight + * @param colour + * @param backgroundColour + */ + public static void drawTooltip(FontRenderer fontRenderer, String tooltipText, int mouseX, int mouseY, int screenWidth, int screenHeight, + int colour, int backgroundColour) + { + int textSize = fontRenderer.getStringWidth(tooltipText); + mouseX = Math.max(0, Math.min(screenWidth - 4, mouseX - 4)); + mouseY = Math.max(0, Math.min(screenHeight - 16, mouseY)); + drawRect(mouseX - textSize - 2, mouseY, mouseX + 2, mouseY + 12, backgroundColour); + fontRenderer.drawStringWithShadow(tooltipText, mouseX - textSize, mouseY + 2, colour); + } + + + /** + * @param x + * @param y + * @param width + * @param height + * @param u + * @param v + * @param u2 + * @param v2 + * @param alpha + */ + static void glDrawTexturedRect(int x, int y, int width, int height, int u, int v, int u2, int v2, float alpha) + { + float texMapScale = 0.00390625F; // 256px + glDrawTexturedRect(x, y, width, height, u * texMapScale, v * texMapScale, u2 * texMapScale, v2 * texMapScale, alpha); + } + + /** + * @param x + * @param y + * @param width + * @param height + * @param u + * @param v + * @param u2 + * @param v2 + * @param alpha + */ + static void glDrawTexturedRect(int x, int y, int width, int height, float u, float v, float u2, float v2, float alpha) + { + glDisableLighting(); + glEnableBlend(); + glAlphaFunc(GL_GREATER, 0.0F); + glEnableTexture2D(); + glColor4f(1.0F, 1.0F, 1.0F, alpha); + + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer worldRenderer = tessellator.getWorldRenderer(); + worldRenderer.startDrawingQuads(); + worldRenderer.addVertexWithUV(x + 0, y + height, 0, u , v2); + worldRenderer.addVertexWithUV(x + width, y + height, 0, u2, v2); + worldRenderer.addVertexWithUV(x + width, y + 0, 0, u2, v ); + worldRenderer.addVertexWithUV(x + 0, y + 0, 0, u , v ); + tessellator.draw(); + + glDisableBlend(); + glAlphaFunc(GL_GREATER, 0.01F); + } + + /** + * @param x + * @param y + * @param icon + * @param alpha + */ + static void glDrawTexturedRect(int x, int y, Icon icon, float alpha) + { + glDrawTexturedRect(x, y, icon.getIconWidth(), icon.getIconHeight(), icon.getMinU(), icon.getMinV(), icon.getMaxU(), icon.getMaxV(), alpha); + } +} \ No newline at end of file diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanel.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanel.java new file mode 100644 index 00000000..219fcd13 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanel.java @@ -0,0 +1,209 @@ +package com.mumfrey.liteloader.client.gui; + +import static com.mumfrey.liteloader.gl.GL.*; + +import java.util.LinkedList; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiButton; + +import com.mumfrey.liteloader.client.api.LiteLoaderBrandingProvider; + +/** + * Base class for panels + * + * @author Adam Mummery-Smith + */ +public abstract class GuiPanel extends Gui +{ + protected static final int TOP = 26; + protected static final int BOTTOM = 40; + protected static final int MARGIN = 12; + + /** + * Minecraft + */ + protected Minecraft mc; + + /** + * Buttons + */ + protected List controls = new LinkedList(); + + /** + * Current available width + */ + protected int width = 0; + + /** + * Current available height + */ + protected int height = 0; + + /** + * Current inner pane width (width - margins) + */ + protected int innerWidth = 0; + + /** + * Current inner pane visible height (height - chrome) + */ + protected int innerHeight = 0; + + /** + * Panel Y position (for scroll) + */ + protected int innerTop = TOP; + + /** + * True if the client wants to close the panel + */ + private boolean closeRequested; + + /** + * @param minecraft + */ + public GuiPanel(Minecraft minecraft) + { + this.mc = minecraft; + } + + boolean stealFocus() + { + return true; + } + + /** + * Called by the containing screen to set the panel size + * + * @param width + * @param height + */ + void setSize(int width, int height) + { + this.controls.clear(); + + this.width = width; + this.height = height; + + this.innerHeight = this.height - TOP - BOTTOM; + this.innerWidth = this.width - (MARGIN * 2) - 6; + } + + /** + * @param mouseX + * @param mouseY + * @param partialTicks + */ + void draw(int mouseX, int mouseY, float partialTicks) + { + for (GuiButton control : this.controls) + control.drawButton(this.mc, mouseX, mouseY); + } + + /** + * + */ + public void close() + { + this.closeRequested = true; + } + + /** + * Get whether the client wants to close the panel + */ + boolean isCloseRequested() + { + return this.closeRequested; + } + + /** + * @param mouseX + * @param mouseY + * @param mouseButton + */ + void mousePressed(int mouseX, int mouseY, int mouseButton) + { + if (mouseButton == 0) + { + for (GuiButton control : this.controls) + { + if (control.mousePressed(this.mc, mouseX, mouseY)) + { + control.playPressSound(this.mc.getSoundHandler()); + this.actionPerformed(control); + } + } + } + } + + /** + * @param mouseX + * @param mouseY + */ + boolean mouseOverPanel(int mouseX, int mouseY) + { + return mouseX > MARGIN && mouseX <= this.width - MARGIN && mouseY > TOP && mouseY <= this.height - BOTTOM; + } + + /** + * Called every tick + */ + abstract void onTick(); + + /** + * Called after the screen is hidden + */ + abstract void onHidden(); + + /** + * Called when the panel is shown + */ + abstract void onShown(); + + /** + * @param keyChar + * @param keyCode + */ + abstract void keyPressed(char keyChar, int keyCode); + + /** + * @param mouseX + * @param mouseY + */ + abstract void mouseMoved(int mouseX, int mouseY); + + /** + * @param mouseX + * @param mouseY + * @param mouseButton + */ + abstract void mouseReleased(int mouseX, int mouseY, int mouseButton); + + /** + * @param mouseWheelDelta + */ + abstract void mouseWheelScrolled(int mouseWheelDelta); + + /** + * @param control + */ + abstract void actionPerformed(GuiButton control); + + /** + * @param x + * @param y + * @param frame + */ + protected void drawThrobber(int x, int y, int frame) + { + glEnableBlend(); + glAlphaFunc(GL_GREATER, 0.0F); + this.mc.getTextureManager().bindTexture(LiteLoaderBrandingProvider.ABOUT_TEXTURE); + this.drawTexturedModalRect(x, y, (frame % 4) * 16, 171 + (((frame / 4) % 3) * 16), 16, 16); + glAlphaFunc(GL_GREATER, 0.1F); + glDisableBlend(); + } +} \ No newline at end of file diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelAbout.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelAbout.java new file mode 100644 index 00000000..1be79089 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelAbout.java @@ -0,0 +1,251 @@ +package com.mumfrey.liteloader.client.gui; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.ResourceLocation; + +import org.lwjgl.input.Keyboard; + +import com.mumfrey.liteloader.api.BrandingProvider; +import com.mumfrey.liteloader.api.LiteAPI; +import com.mumfrey.liteloader.client.api.LiteLoaderBrandingProvider; +import com.mumfrey.liteloader.client.util.render.IconAbsolute; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.util.SortableValue; +import com.mumfrey.liteloader.util.render.Icon; + +/** + * "About LiteLoader" panel which docks in the mod info screen and lists + * information about the installed APIs. + * + * @author Adam Mummery-Smith + */ +class GuiPanelAbout extends GuiPanel implements ScrollPanelContent +{ + public static final IconAbsolute apiIconCoords = new IconAbsolute(LiteLoaderBrandingProvider.ABOUT_TEXTURE, "api_icon", + 32, 32, 192, 144, 256, 208); + + private static final int ROW_HEIGHT = 40; + + private static final URI MCP_URI = URI.create("http://mcp.ocean-labs.de/"); + + private GuiLiteLoaderPanel parent; + + private GuiScrollPanel scrollPane; + + private List brandings = new ArrayList(); + + private boolean mouseOverLogo; + + public GuiPanelAbout(Minecraft minecraft, GuiLiteLoaderPanel parent) + { + super(minecraft); + this.parent = parent; + this.scrollPane = new GuiScrollPanel(minecraft, this, MARGIN, 90, 100, 100); + + this.sortBrandingProviders(); + + this.scrollPane.addControl(new GuiHoverLabel(-2, 38, 22 + this.brandings.size() * GuiPanelAbout.ROW_HEIGHT, this.mc.fontRendererObj, + "\247n" + MCP_URI.toString(), this.parent.getBrandColour())); + } + + /** + * + */ + private void sortBrandingProviders() + { + Set> sortedBrandingProviders = new TreeSet>(); + + for (LiteAPI api : LiteLoader.getAPIs()) + { + BrandingProvider brandingProvider = LiteLoader.getCustomisationProvider(api, BrandingProvider.class); + if (brandingProvider != null) + { + sortedBrandingProviders.add(new SortableValue(Integer.MAX_VALUE - brandingProvider.getPriority(), 0, + brandingProvider)); + } + } + + int brandingIndex = 0; + + for (SortableValue sortedBrandingProvider : sortedBrandingProviders) + { + BrandingProvider brandingProvider = sortedBrandingProvider.getValue(); + + this.brandings.add(brandingProvider); + URI homepage = brandingProvider.getHomepage(); + if (homepage != null) + { + this.scrollPane.addControl(new GuiHoverLabel(brandingIndex, 38, 22 + brandingIndex * GuiPanelAbout.ROW_HEIGHT, + this.mc.fontRendererObj, "\247n" + homepage, this.parent.getBrandColour())); + } + + brandingIndex++; + } + } + + @Override + void setSize(int width, int height) + { + super.setSize(width, height); + + this.scrollPane.setSizeAndPosition(MARGIN, 86, this.width - MARGIN * 2, this.height - 126); + this.controls.add(new GuiButton(-1, this.width - 99 - MARGIN, this.height - BOTTOM + 9, 100, 20, I18n.format("gui.done"))); + this.controls.add(new GuiButton(-3, MARGIN, this.height - BOTTOM + 9, 100, 20, I18n.format("gui.log.button"))); + } + + @Override + void draw(int mouseX, int mouseY, float partialTicks) + { + this.mouseOverLogo = this.parent.drawInfoPanel(mouseX, mouseY, partialTicks, 0, 38); + + this.scrollPane.draw(mouseX, mouseY, partialTicks); + + super.draw(mouseX, mouseY, partialTicks); + } + + @Override + public int getScrollPanelContentHeight(GuiScrollPanel source) + { + return 64 + this.brandings.size() * GuiPanelAbout.ROW_HEIGHT; + } + + @Override + public void drawScrollPanelContent(GuiScrollPanel source, int mouseX, int mouseY, float partialTicks, int scrollAmount, int visibleHeight) + { + FontRenderer fontRenderer = this.mc.fontRendererObj; + int textColour = 0xFFAAAAAA; + + int yPos = 0; + + for (BrandingProvider branding : this.brandings) + { + ResourceLocation twitterAvatarResource = branding.getTwitterAvatarResource(); + Icon twitterAvatarCoords = branding.getTwitterAvatarCoords(); + + this.mc.getTextureManager().bindTexture(twitterAvatarResource != null ? twitterAvatarResource : LiteLoaderBrandingProvider.ABOUT_TEXTURE); + GuiLiteLoaderPanel.glDrawTexturedRect(0, yPos, twitterAvatarCoords != null ? twitterAvatarCoords : GuiPanelAbout.apiIconCoords, 1.0F); + + fontRenderer.drawString(branding.getDisplayName(), 38, yPos, 0xFFFFFFFF); + fontRenderer.drawString(branding.getCopyrightText(), 38, yPos + 11, textColour); + + yPos += GuiPanelAbout.ROW_HEIGHT; + } + + fontRenderer.drawString("Created using Mod Coder Pack", 38, yPos, 0xFFFFFFFF); + fontRenderer.drawString("MCP is (c) Copyright by the MCP Team", 38, yPos + 11, textColour); + + yPos += GuiPanelAbout.ROW_HEIGHT; + + fontRenderer.drawString("Minecraft is Copyright (c) Mojang AB", 38, yPos, textColour); + fontRenderer.drawString("All rights reserved.", 38, yPos + 11, textColour); + } + + @Override + public void scrollPanelMousePressed(GuiScrollPanel source, int mouseX, int mouseY, int mouseButton) + { + int index = mouseY / GuiPanelAbout.ROW_HEIGHT; + int yOffset = mouseY - (GuiPanelAbout.ROW_HEIGHT * index); + + if (mouseButton == 0 && mouseX < 33 && index >= 0 && index < this.brandings.size() && yOffset < 33) + { + String twitterUserName = this.brandings.get(index).getTwitterUserName(); + if (twitterUserName != null) + { + URI twitterURI = URI.create("https://www.twitter.com/" + twitterUserName); + this.openURI(twitterURI); + } + } + } + + /** + * @param control + */ + @Override + void actionPerformed(GuiButton control) + { + if (control.id == -1) this.close(); + if (control.id == -2) this.openURI(MCP_URI); + if (control.id == -3) this.parent.showLogPanel(); + } + + @Override + public void scrollPanelActionPerformed(GuiScrollPanel source, GuiButton control) + { + if (control.id >= 0 && control.id < this.brandings.size()) + { + URI homepage = this.brandings.get(control.id).getHomepage(); + if (homepage != null) this.openURI(homepage); + } + } + + private void openURI(URI uri) + { + try + { + Class desktop = Class.forName("java.awt.Desktop"); + Object instance = desktop.getMethod("getDesktop").invoke(null); + desktop.getMethod("browse", URI.class).invoke(instance, uri); + } + catch (Throwable th) {} + } + + @Override + void onTick() + { + } + + @Override + void onHidden() + { + } + + @Override + void onShown() + { + } + + @Override + void keyPressed(char keyChar, int keyCode) + { + if (keyCode == Keyboard.KEY_ESCAPE) this.close(); + } + + @Override + void mousePressed(int mouseX, int mouseY, int mouseButton) + { + this.scrollPane.mousePressed(mouseX, mouseY, mouseButton); + + if (mouseButton == 0 && this.mouseOverLogo) + { + this.close(); + } + + super.mousePressed(mouseX, mouseY, mouseButton); + } + + @Override + void mouseMoved(int mouseX, int mouseY) + { + } + + @Override + void mouseReleased(int mouseX, int mouseY, int mouseButton) + { + this.scrollPane.mouseReleased(mouseX, mouseY, mouseButton); + } + + @Override + void mouseWheelScrolled(int mouseWheelDelta) + { + this.scrollPane.mouseWheelScrolled(mouseWheelDelta); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelConfigContainer.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelConfigContainer.java new file mode 100644 index 00000000..dbb3c6ab --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelConfigContainer.java @@ -0,0 +1,274 @@ +package com.mumfrey.liteloader.client.gui; + +import static com.mumfrey.liteloader.gl.GL.*; +import static com.mumfrey.liteloader.gl.GLClippingPlanes.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.resources.I18n; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.modconfig.ConfigPanel; +import com.mumfrey.liteloader.modconfig.ConfigPanelHost; + +/** + * Config panel container, this handles drawing the configuration panel chrome + * and also hosts the configuration panels themselves to support scrolling and + * stuff. + * + * @author Adam Mummery-Smith + */ +class GuiPanelConfigContainer extends GuiPanel implements ConfigPanelHost +{ + /** + * Panel we are hosting + */ + private ConfigPanel panel; + + /** + * Mod being configured, the panel may want a reference to it + */ + private LiteMod mod; + + /** + * Scroll bar for the panel + */ + GuiSimpleScrollBar scrollBar = new GuiSimpleScrollBar(); + + /** + * Panel's internal height (for scrolling) + */ + private int totalHeight = -1; + + /** + * @param minecraft + * @param panel + * @param mod + */ + GuiPanelConfigContainer(Minecraft minecraft, ConfigPanel panel, LiteMod mod) + { + super(minecraft); + + this.panel = panel; + this.mod = mod; + } + + /** + * + */ + String getPanelTitle() + { + String panelTitle = this.panel.getPanelTitle(); + return panelTitle != null ? panelTitle : I18n.format("gui.settings.title", this.mod.getName()); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.modconfig.ConfigPanelHost#getMod() + */ + @SuppressWarnings("unchecked") + @Override + public TModClass getMod() + { + return (TModClass)this.mod; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.modconfig.ConfigPanelHost#getWidth() + */ + @Override + public int getWidth() + { + return this.innerWidth; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.modconfig.ConfigPanelHost#getHeight() + */ + @Override + public int getHeight() + { + return this.innerHeight; + } + + /** + * Callback from parent screen when window is resized + * + * @param width + * @param height + */ + @Override + void setSize(int width, int height) + { + super.setSize(width, height); + + this.panel.onPanelResize(this); + this.controls.add(new GuiButton(0, this.width - 99 - MARGIN, this.height - BOTTOM + 9, 100, 20, I18n.format("gui.saveandclose"))); + } + + /** + * Callback from parent screen when panel is displayed + */ + @Override + void onShown() + { + try + { + this.panel.onPanelShown(this); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + /** + * Callback from parent screen when panel is hidden + */ + @Override + void onHidden() + { + try + { + this.panel.onPanelHidden(); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + /** + * Callback from parent screen every tick + */ + @Override + void onTick() + { + this.panel.onTick(this); + } + + /** + * Draw the panel and chrome + * + * @param mouseX + * @param mouseY + * @param partialTicks + */ + @Override + void draw(int mouseX, int mouseY, float partialTicks) + { + // Scroll position + this.innerTop = TOP - this.scrollBar.getValue(); + + // Draw panel title + this.mc.fontRendererObj.drawString(this.getPanelTitle(), MARGIN, TOP - 14, 0xFFFFFFFF); + + // Draw top and bottom horizontal bars + drawRect(MARGIN, TOP - 4, this.width - MARGIN, TOP - 3, 0xFF999999); + drawRect(MARGIN, this.height - BOTTOM + 2, this.width - MARGIN, this.height - BOTTOM + 3, 0xFF999999); + + // Clip rect + glEnableClipping(MARGIN, this.width - MARGIN - 6, TOP, this.height - BOTTOM); + + // Offset by scroll + glPushMatrix(); + glTranslatef(MARGIN, this.innerTop, 0.0F); + + // Draw panel contents + this.panel.drawPanel(this, mouseX - MARGIN - (this.mouseOverPanel(mouseX, mouseY) ? 0 : 99999), mouseY - this.innerTop, partialTicks); + glClear(GL_DEPTH_BUFFER_BIT); + + // Disable clip rect + glDisableClipping(); + + // Restore transform + glPopMatrix(); + + // Get total scroll height from panel + this.totalHeight = Math.max(-1, this.panel.getContentHeight()); + + // Update and draw scroll bar + this.scrollBar.setMaxValue(this.totalHeight - this.innerHeight); + this.scrollBar.drawScrollBar(mouseX, mouseY, partialTicks, this.width - MARGIN - 5, TOP, 5, this.innerHeight, + Math.max(this.innerHeight, this.totalHeight)); + + // Draw other buttons + super.draw(mouseX, mouseY, partialTicks); + } + + /** + * @param control + */ + @Override + void actionPerformed(GuiButton control) + { + if (control.id == 0) this.close(); + } + + /** + * @param mouseWheelDelta + */ + @Override + void mouseWheelScrolled(int mouseWheelDelta) + { + this.scrollBar.offsetValue(-mouseWheelDelta / 8); + } + + /** + * @param mouseX + * @param mouseY + * @param mouseButton + */ + @Override + void mousePressed(int mouseX, int mouseY, int mouseButton) + { + if (mouseButton == 0) + { + if (this.scrollBar.wasMouseOver()) + { + this.scrollBar.setDragging(true); + } + } + + super.mousePressed(mouseX, mouseY, mouseButton); + + if (this.mouseOverPanel(mouseX, mouseY)) + { + this.panel.mousePressed(this, mouseX - MARGIN, mouseY - this.innerTop, mouseButton); + } + } + + /** + * @param mouseX + * @param mouseY + * @param mouseButton + */ + @Override + void mouseReleased(int mouseX, int mouseY, int mouseButton) + { + if (mouseButton == 0) + { + this.scrollBar.setDragging(false); + } + + this.panel.mouseReleased(this, mouseX - MARGIN, mouseY - this.innerTop, mouseButton); + } + + /** + * @param mouseX + * @param mouseY + */ + @Override + void mouseMoved(int mouseX, int mouseY) + { + this.panel.mouseMoved(this, mouseX - MARGIN, mouseY - this.innerTop); + } + + /** + * @param keyChar + * @param keyCode + */ + @Override + void keyPressed(char keyChar, int keyCode) + { + this.panel.keyPressed(this, keyChar, keyCode); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelError.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelError.java new file mode 100644 index 00000000..d465e556 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelError.java @@ -0,0 +1,161 @@ +package com.mumfrey.liteloader.client.gui; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import org.lwjgl.input.Keyboard; + +import com.mumfrey.liteloader.core.ModInfo; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.resources.I18n; + +public class GuiPanelError extends GuiPanel implements ScrollPanelContent +{ + private final ModInfo mod; + + private GuiScrollPanel scrollPane; + + private List scrollPaneContent = new ArrayList(); + + public GuiPanelError(Minecraft minecraft, GuiLiteLoaderPanel parent, ModInfo mod) + { + super(minecraft); + + this.mod = mod; + this.scrollPane = new GuiScrollPanel(minecraft, this, MARGIN, TOP, this.width - (MARGIN * 2), this.height - TOP - BOTTOM); + + this.populateScrollPaneContent(); + } + + private void populateScrollPaneContent() + { + for (Throwable th : this.mod.getStartupErrors()) + { + StringWriter sw = new StringWriter(); + th.printStackTrace(new PrintWriter(sw, true)); + for (String line : sw.toString().split("\\r?\\n")) + { + this.scrollPaneContent.add(line.replace("\t", " ")); + } + + this.scrollPaneContent.add("!"); + } + + this.scrollPaneContent.remove(this.scrollPaneContent.size() - 1); + } + + @Override + public int getScrollPanelContentHeight(GuiScrollPanel source) + { + return this.scrollPaneContent.size() * 10; + } + + @Override + public void drawScrollPanelContent(GuiScrollPanel source, int mouseX, int mouseY, float partialTicks, int scrollAmount, int visibleHeight) + { + int yPos = -10; + + for (String line : this.scrollPaneContent) + { + if ("!".equals(line)) + { + yPos += 10; + drawRect(0, yPos + 4, this.width, yPos + 5, 0xFF555555); + } + else + { + boolean indented = line.startsWith(" "); + line = line.replaceAll("\\((.+?\\.java:[0-9]+)\\)", "(\247f$1\247r)"); + line = line.replaceAll("at ([^\\(]+)\\(", "at \2476$1\247r("); + this.mc.fontRendererObj.drawString(line, 2, yPos += 10, indented ? 0xFF999999 : 0xFFFF5555); + } + } + } + + @Override + public void scrollPanelActionPerformed(GuiScrollPanel source, GuiButton control) + { + } + + @Override + public void scrollPanelMousePressed(GuiScrollPanel source, int mouseX, int mouseY, int mouseButton) + { + } + + @Override + void setSize(int width, int height) + { + super.setSize(width, height); + + this.scrollPane.setSizeAndPosition(MARGIN, TOP, this.width - (MARGIN * 2), this.height - TOP - BOTTOM); + this.controls.add(new GuiButton(0, this.width - 59 - MARGIN, this.height - BOTTOM + 9, 60, 20, I18n.format("gui.done"))); + } + + @Override + void draw(int mouseX, int mouseY, float partialTicks) + { + this.mc.fontRendererObj.drawString(I18n.format("gui.error.title", this.mod.getDisplayName()), MARGIN, TOP - 14, 0xFFFFFFFF); + + drawRect(MARGIN, TOP - 4, this.width - MARGIN, TOP - 3, 0xFF999999); + drawRect(MARGIN, this.height - BOTTOM + 2, this.width - MARGIN, this.height - BOTTOM + 3, 0xFF999999); + + this.scrollPane.draw(mouseX, mouseY, partialTicks); + + super.draw(mouseX, mouseY, partialTicks); + } + + @Override + void onTick() + { + } + + @Override + void onHidden() + { + } + + @Override + void onShown() + { + } + + @Override + void keyPressed(char keyChar, int keyCode) + { + if (keyCode == Keyboard.KEY_ESCAPE) this.close(); + } + + @Override + void mousePressed(int mouseX, int mouseY, int mouseButton) + { + this.scrollPane.mousePressed(mouseX, mouseY, mouseButton); + super.mousePressed(mouseX, mouseY, mouseButton); + } + + @Override + void mouseMoved(int mouseX, int mouseY) + { + } + + @Override + void mouseReleased(int mouseX, int mouseY, int mouseButton) + { + this.scrollPane.mouseReleased(mouseX, mouseY, mouseButton); + } + + @Override + void mouseWheelScrolled(int mouseWheelDelta) + { + this.scrollPane.mouseWheelScrolled(mouseWheelDelta); + } + + @Override + void actionPerformed(GuiButton control) + { + if (control.id == 0) this.close(); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelLiteLoaderLog.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelLiteLoaderLog.java new file mode 100644 index 00000000..0204ca96 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelLiteLoaderLog.java @@ -0,0 +1,406 @@ +package com.mumfrey.liteloader.client.gui; + +import static com.mumfrey.liteloader.gl.GL.*; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.Session; + +import org.lwjgl.input.Keyboard; + +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.net.LiteLoaderLogUpload; + +/** + * + * @author Adam Mummery-Smith + */ +class GuiPanelLiteLoaderLog extends GuiPanel implements ScrollPanelContent +{ + private static boolean useNativeRes = true; + + /** + * Scroll pane + */ + private GuiScrollPanel scrollPane; + + private List logEntries = new ArrayList(); + + private long logIndex = -1; + + private GuiCheckbox chkScale; + + private float guiScale; + + private GuiButton btnUpload; + + private LiteLoaderLogUpload logUpload; + + private String logURL; + + private int throb; + + private boolean closeDialog; + + private GuiLiteLoaderPanel parent; + + private int debugInfoTimer = 0; + + /** + * @param minecraft + * @param parent + */ + GuiPanelLiteLoaderLog(Minecraft minecraft, GuiLiteLoaderPanel parent) + { + super(minecraft); + this.parent = parent; + this.scrollPane = new GuiScrollPanel(minecraft, this, MARGIN, TOP, this.width - (MARGIN * 2), this.height - TOP - BOTTOM); + } + + private void updateLog() + { + this.logEntries = LiteLoaderLogger.getLogTail(); + this.logIndex = LiteLoaderLogger.getLogIndex(); + this.scrollPane.updateHeight(); + this.scrollPane.scrollToBottom(); + } + + @Override + public int getScrollPanelContentHeight(GuiScrollPanel source) + { + return (int)(this.logEntries.size() * 10 / (this.chkScale.checked ? this.guiScale : 1.0F)); + } + + /** + * Callback from parent screen when window is resized + * + * @param width + * @param height + */ + @Override + void setSize(int width, int height) + { + super.setSize(width, height); + + this.controls.add(new GuiButton(0, this.width - 59 - MARGIN, this.height - BOTTOM + 9, 60, 20, + I18n.format("gui.done"))); + this.controls.add(this.btnUpload = new GuiButton(1, this.width - 145 - MARGIN, this.height - BOTTOM + 9, 80, 20, + I18n.format("gui.log.postlog"))); + this.controls.add(this.chkScale = new GuiCheckbox(2, MARGIN, this.height - BOTTOM + 15, + I18n.format("gui.log.scalecheckbox"))); + + this.chkScale.checked = GuiPanelLiteLoaderLog.useNativeRes; + + ScaledResolution res = new ScaledResolution(this.mc, this.mc.displayWidth, this.mc.displayHeight); + this.guiScale = res.getScaleFactor(); + + this.scrollPane.setSizeAndPosition(MARGIN, TOP, this.width - (MARGIN * 2), this.height - TOP - BOTTOM); + + this.updateLog(); + } + + /** + * Callback from parent screen when panel is displayed + */ + @Override + void onShown() + { + } + + /** + * Callback from parent screen when panel is hidden + */ + @Override + void onHidden() + { + } + + /** + * Callback from parent screen every tick + */ + @Override + void onTick() + { + this.throb++; + + if (LiteLoaderLogger.getLogIndex() > this.logIndex) + { + this.updateLog(); + } + + if (this.logUpload != null && this.logUpload.isCompleted()) + { + this.logURL = this.logUpload.getLogUrl().trim(); + this.logUpload = null; + + int xMid = this.width / 2; + if (this.logURL.startsWith("http:")) + { + LiteLoaderLogger.info("Log file upload succeeded, url is %s", this.logURL); + int urlWidth = this.mc.fontRendererObj.getStringWidth(this.logURL); + this.controls.add(new GuiHoverLabel(3, xMid - (urlWidth / 2), this.height / 2, this.mc.fontRendererObj, "\247n" + this.logURL, + this.parent.getBrandColour())); + } + else + { + LiteLoaderLogger.info("Log file upload failed, reason is %s", this.logURL); + } + + this.controls.add(new GuiButton(4, xMid - 40, this.height - BOTTOM - MARGIN - 24, 80, 20, I18n.format("gui.log.closedialog"))); + } + + if (this.closeDialog) + { + this.closeDialog = false; + this.logURL = null; + this.setSize(this.width, this.height); + } + + if (Keyboard.isKeyDown(Keyboard.KEY_F3)) + { + this.debugInfoTimer++; + if (this.debugInfoTimer == 60) + { + LiteLoader.dumpDebugInfo(); + } + } + else + { + this.debugInfoTimer = 0; + } + } + + /** + * Draw the panel and chrome + * + * @param mouseX + * @param mouseY + * @param partialTicks + */ + @Override + void draw(int mouseX, int mouseY, float partialTicks) + { + // Draw panel title + this.mc.fontRendererObj.drawString(I18n.format("gui.log.title"), MARGIN, TOP - 14, 0xFFFFFFFF); + + // Draw top and bottom horizontal bars + drawRect(MARGIN, TOP - 4, this.width - MARGIN, TOP - 3, 0xFF999999); + drawRect(MARGIN, this.height - BOTTOM + 2, this.width - MARGIN, this.height - BOTTOM + 3, 0xFF999999); + + this.scrollPane.draw(mouseX, mouseY, partialTicks); + + int xMid = this.width / 2; + int yMid = this.height / 2; + + if (this.logUpload != null || this.logURL != null) + { + drawRect(MARGIN + MARGIN, TOP + MARGIN, this.width - MARGIN - MARGIN, this.height - BOTTOM - MARGIN, 0xC0000000); + + if (this.logUpload != null) + { + this.drawCenteredString(this.mc.fontRendererObj, I18n.format("gui.log.uploading"), xMid, yMid - 10, 0xFFFFFFFF); + this.drawThrobber(xMid - 90, yMid - 14, this.throb); + } + else + { + if (this.logURL.startsWith("http:")) + { + this.drawCenteredString(this.mc.fontRendererObj, I18n.format("gui.log.uploadsuccess"), xMid, yMid - 14, 0xFF55FF55); + } + else + { + this.drawCenteredString(this.mc.fontRendererObj, I18n.format("gui.log.uploadfailed"), xMid, yMid - 10, 0xFFFF5555); + } + } + } + + // Draw other buttons + super.draw(mouseX, mouseY, partialTicks); + } + + @Override + public void drawScrollPanelContent(GuiScrollPanel source, int mouseX, int mouseY, float partialTicks, int scrollAmount, int visibleHeight) + { + int yPos = 0; + int height = this.innerHeight; + + if (this.chkScale.checked) + { + float scale = 1.0F / this.guiScale; + glScalef(scale, scale, scale); + + height = (int)(height * this.guiScale); + scrollAmount = (int)(scrollAmount * this.guiScale); + } + + for (String logLine : this.logEntries) + { + if (yPos > scrollAmount - 10 && yPos <= scrollAmount + height) + { + this.mc.fontRendererObj.drawString(logLine, 0, yPos, this.getMessageColour(logLine.toLowerCase().substring(11))); + } + yPos += 10; + } + } + + @Override + public void scrollPanelMousePressed(GuiScrollPanel source, int mouseX, int mouseY, int mouseButton) + { + } + + private int getMessageColour(String logLine) + { + if (logLine.startsWith("liteloader")) return 0xFFFFFF; + if (logLine.startsWith("active pack:")) return 0xFFFF55; + if (logLine.startsWith("success")) return 0x55FF55; + if (logLine.startsWith("discovering")) return 0xFFFF55; + if (logLine.startsWith("searching")) return 0x00AA00; + if (logLine.startsWith("considering")) return 0xFFAA00; + if (logLine.startsWith("not adding")) return 0xFF5555; + if (logLine.startsWith("mod in")) return 0xAA0000; + if (logLine.startsWith("error")) return 0xAA0000; + if (logLine.startsWith("adding newest")) return 0x5555FF; + if (logLine.startsWith("found")) return 0xFFFF55; + if (logLine.startsWith("discovered")) return 0xFFFF55; + if (logLine.startsWith("setting up")) return 0xAA00AA; + if (logLine.startsWith("adding \"")) return 0xAA00AA; + if (logLine.startsWith("injecting")) return 0xFF55FF; + if (logLine.startsWith("loading")) return 0x5555FF; + if (logLine.startsWith("initialising")) return 0x55FFFF; + if (logLine.startsWith("calling late")) return 0x00AAAA; + if (logLine.startsWith("dependency check")) return 0xFFAA00; + if (logLine.startsWith("dependency")) return 0xFF5500; + if (logLine.startsWith("mod name collision")) return 0xAA0000; + if (logLine.startsWith("registering discovery module")) return 0x55FF55; + if (logLine.startsWith("registering interface provider")) return 0xFFAA00; + if (logLine.startsWith("mod file '")) return 0xFFAA00; + if (logLine.startsWith("classtransformer '")) return 0x5555FF; + if (logLine.startsWith("tweakClass '")) return 0x5555FF; + if (logLine.startsWith("baking listener list")) return 0x00AAAA; + if (logLine.startsWith("generating new event handler")) return 0xFFFF55; + + return 0xCCCCCC; + } + + /** + * @param control + */ + @Override + public void actionPerformed(GuiButton control) + { + if (control.id == 0) this.close(); + if (control.id == 1) this.postLog(); + + if (control.id == 2 && this.chkScale != null) + { + this.chkScale.checked = !this.chkScale.checked; + GuiPanelLiteLoaderLog.useNativeRes = this.chkScale.checked; + this.updateLog(); + } + + if (control.id == 3 && this.logURL != null) + { + this.openURI(URI.create(this.logURL)); + } + + if (control.id == 4) + { + this.closeDialog = true; + } + } + + @Override + public void scrollPanelActionPerformed(GuiScrollPanel source, GuiButton control) + { + } + + /** + * @param mouseWheelDelta + */ + @Override + void mouseWheelScrolled(int mouseWheelDelta) + { + this.scrollPane.mouseWheelScrolled(mouseWheelDelta); + } + + /** + * @param mouseX + * @param mouseY + * @param mouseButton + */ + @Override + void mousePressed(int mouseX, int mouseY, int mouseButton) + { + this.scrollPane.mousePressed(mouseX, mouseY, mouseButton); + + super.mousePressed(mouseX, mouseY, mouseButton); + } + + /** + * @param mouseX + * @param mouseY + * @param mouseButton + */ + @Override + void mouseReleased(int mouseX, int mouseY, int mouseButton) + { + this.scrollPane.mouseReleased(mouseX, mouseY, mouseButton); + } + + /** + * @param mouseX + * @param mouseY + */ + @Override + void mouseMoved(int mouseX, int mouseY) + { + } + + /** + * @param keyChar + * @param keyCode + */ + @Override + void keyPressed(char keyChar, int keyCode) + { + if (keyCode == Keyboard.KEY_ESCAPE) this.close(); + if (keyCode == Keyboard.KEY_SPACE) this.actionPerformed(this.chkScale); + + this.scrollPane.keyPressed(keyChar, keyCode); + } + + private void postLog() + { + this.btnUpload.enabled = false; + + StringBuilder completeLog = new StringBuilder(); + + for (String logLine : this.logEntries) + { + completeLog.append(logLine).append("\r\n"); + } + + LiteLoaderLogger.info("Uploading log file to liteloader..."); + Session session = this.mc.getSession(); + this.logUpload = new LiteLoaderLogUpload(session.getUsername(), session.getPlayerID(), completeLog.toString()); + this.logUpload.start(); + } + + private void openURI(URI uri) + { + try + { + Class desktop = Class.forName("java.awt.Desktop"); + Object instance = desktop.getMethod("getDesktop").invoke(null); + desktop.getMethod("browse", URI.class).invoke(instance, uri); + } + catch (Throwable th) {} + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelMods.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelMods.java new file mode 100644 index 00000000..29b6de81 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelMods.java @@ -0,0 +1,295 @@ +package com.mumfrey.liteloader.client.gui; + +import static com.mumfrey.liteloader.gl.GL.*; +import static com.mumfrey.liteloader.gl.GLClippingPlanes.*; + +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.resources.I18n; + +import org.lwjgl.input.Keyboard; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.api.ModInfoDecorator; +import com.mumfrey.liteloader.client.gui.modlist.ModList; +import com.mumfrey.liteloader.client.gui.modlist.ModListContainer; +import com.mumfrey.liteloader.core.LiteLoaderMods; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.modconfig.ConfigManager; +import com.mumfrey.liteloader.modconfig.ConfigPanel; + +/** + * Mods panel + * + * @author Adam Mummery-Smith + */ +public class GuiPanelMods extends GuiPanel implements ModListContainer +{ + private static final int SCROLLBAR_WIDTH = 5; + + private final GuiLiteLoaderPanel parentScreen; + + private final ConfigManager configManager; + + /** + * List of enumerated mods + */ + private ModList modList; + + /** + * Enable / disable button + */ + private GuiButton btnToggle; + + /** + * Config button + */ + private GuiButton btnConfig; + + /** + * Height of all the items in the list + */ + private int listHeight = 100; + + /** + * Scroll bar control for the mods list + */ + private GuiSimpleScrollBar scrollBar = new GuiSimpleScrollBar(); + + public GuiPanelMods(GuiLiteLoaderPanel parentScreen, Minecraft minecraft, LiteLoaderMods mods, LoaderEnvironment environment, + ConfigManager configManager, int brandColour, List decorators) + { + super(minecraft); + + this.parentScreen = parentScreen; + this.configManager = configManager; + + this.modList = new ModList(this, minecraft, mods, environment, configManager, brandColour, decorators); + } + + @Override + public GuiLiteLoaderPanel getParentScreen() + { + return this.parentScreen; + } + + @Override + public void setConfigButtonVisible(boolean visible) + { + this.btnConfig.visible = visible; + } + + @Override + public void setEnableButtonVisible(boolean visible) + { + this.btnToggle.visible = visible; + } + + @Override + public void setEnableButtonText(String displayString) + { + this.btnToggle.displayString = displayString; + } + + @Override + boolean stealFocus() + { + return false; + } + + @Override + void setSize(int width, int height) + { + super.setSize(width, height); + + int rightPanelLeftEdge = MARGIN + 4 + (this.width - MARGIN - MARGIN - 4) / 2; + + this.controls.clear(); + this.controls.add(this.btnToggle = new GuiButton(0, rightPanelLeftEdge, this.height - GuiLiteLoaderPanel.PANEL_BOTTOM - 24, 90, 20, + I18n.format("gui.enablemod"))); + this.controls.add(this.btnConfig = new GuiButton(1, rightPanelLeftEdge + 92, this.height - GuiLiteLoaderPanel.PANEL_BOTTOM - 24, 69, 20, + I18n.format("gui.modsettings"))); + + this.modList.setSize(width, height); + } + + @Override + void onTick() + { + this.modList.onTick(); + } + + @Override + void onHidden() + { + } + + @Override + void onShown() + { + } + + @Override + void mousePressed(int mouseX, int mouseY, int mouseButton) + { + if (mouseButton == 0) + { + if (this.scrollBar.wasMouseOver()) + { + this.scrollBar.setDragging(true); + } + + if (mouseY > GuiLiteLoaderPanel.PANEL_TOP && mouseY < this.height - GuiLiteLoaderPanel.PANEL_BOTTOM) + { + this.modList.mousePressed(mouseX, mouseY, mouseButton); + } + } + + super.mousePressed(mouseX, mouseY, mouseButton); + } + + @Override + void keyPressed(char keyChar, int keyCode) + { + if (keyCode == Keyboard.KEY_ESCAPE) + { + this.parentScreen.onToggled(); + return; + } + else if (this.modList.keyPressed(keyChar, keyCode)) + { + // Suppress further handling + } + else if (keyCode == Keyboard.KEY_F3) + { + this.parentScreen.showLogPanel(); + } + else if (keyCode == Keyboard.KEY_F1) + { + this.parentScreen.showAboutPanel(); + } + } + + @Override + void mouseMoved(int mouseX, int mouseY) + { + } + + @Override + void mouseReleased(int mouseX, int mouseY, int mouseButton) + { + if (mouseButton == 0) + { + this.scrollBar.setDragging(false); + this.modList.mouseReleased(mouseX, mouseY, mouseButton); + } + } + + @Override + void mouseWheelScrolled(int mouseWheelDelta) + { + if (!this.modList.mouseWheelScrolled(mouseWheelDelta)) + { + this.scrollBar.offsetValue(-mouseWheelDelta / 8); + } + } + + @Override + public void showConfig() + { + this.actionPerformed(this.btnConfig); + } + + @Override + void actionPerformed(GuiButton control) + { + if (control.id == 0) + { + this.modList.toggleSelectedMod(); + } + + if (control.id == 1) + { + Class modClass = this.modList.getSelectedModClass(); + + if (modClass != null) + { + ConfigPanel panel = this.configManager.getPanel(modClass); + LiteMod mod = this.modList.getSelectedModInstance(); + this.parentScreen.openConfigPanel(panel, mod); + } + } + } + + @Override + void draw(int mouseX, int mouseY, float partialTicks) + { + this.parentScreen.drawInfoPanel(mouseX, mouseY, partialTicks, 0, GuiLiteLoaderPanel.PANEL_BOTTOM); + + int innerWidth = this.width - MARGIN - MARGIN - 4; + int panelWidth = innerWidth / 2; + int panelHeight = this.height - GuiLiteLoaderPanel.PANEL_BOTTOM - GuiLiteLoaderPanel.PANEL_TOP; + + this.drawModsList(mouseX, mouseY, partialTicks, panelWidth, panelHeight); + + int left = MARGIN + panelWidth; + int top = GuiLiteLoaderPanel.PANEL_TOP; + int spaceForButtons = (this.btnConfig.visible || this.btnToggle.visible ? 28 : 0); + int bottom = this.height - GuiLiteLoaderPanel.PANEL_BOTTOM - spaceForButtons; + + glEnableClipping(left, this.width - MARGIN, top, bottom); + this.modList.drawModPanel(mouseX, mouseY, partialTicks, left, top, this.width - MARGIN - left, panelHeight - spaceForButtons); + glDisableClipping(); + + super.draw(mouseX, mouseY, partialTicks); + } + + /** + * @param mouseX + * @param mouseY + * @param partialTicks + * @param width + * @param height + */ + private void drawModsList(int mouseX, int mouseY, float partialTicks, int width, int height) + { + this.scrollBar.drawScrollBar(mouseX, mouseY, partialTicks, MARGIN + width - SCROLLBAR_WIDTH, GuiLiteLoaderPanel.PANEL_TOP, SCROLLBAR_WIDTH, + height, this.listHeight); + + // clip outside of scroll area + glEnableClipping(MARGIN, MARGIN + width - SCROLLBAR_WIDTH - 1, GuiLiteLoaderPanel.PANEL_TOP, this.height - GuiLiteLoaderPanel.PANEL_BOTTOM); + + // handle scrolling + glPushMatrix(); + glTranslatef(0.0F, GuiLiteLoaderPanel.PANEL_TOP - this.scrollBar.getValue(), 0.0F); + + mouseY -= (GuiLiteLoaderPanel.PANEL_TOP - this.scrollBar.getValue()); + + this.listHeight = this.modList.drawModList(mouseX, mouseY, partialTicks, MARGIN, 0, width - SCROLLBAR_WIDTH - 1, height); + this.scrollBar.setMaxValue(this.listHeight - height); + + glPopMatrix(); + glDisableClipping(); + } + + @Override + public void scrollTo(int yPosTop, int yPosBottom) + { + // Mod is above the top of the visible window + if (yPosTop < this.scrollBar.getValue()) + { + this.scrollBar.setValue(yPosTop); + return; + } + + int panelHeight = this.height - GuiLiteLoaderPanel.PANEL_BOTTOM - GuiLiteLoaderPanel.PANEL_TOP; + + // Mod is below the bottom of the visible window + if (yPosBottom - this.scrollBar.getValue() > panelHeight) + { + this.scrollBar.setValue(yPosBottom - panelHeight); + } + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelSettings.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelSettings.java new file mode 100644 index 00000000..05c68b75 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelSettings.java @@ -0,0 +1,152 @@ +package com.mumfrey.liteloader.client.gui; + +import org.lwjgl.input.Keyboard; + +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.interfaces.PanelManager; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.resources.I18n; + +class GuiPanelSettings extends GuiPanel +{ + private GuiLiteLoaderPanel parentScreen; + + private GuiCheckbox chkShowTab, chkNoHide, chkForceUpdate; + + private boolean hide; + + private String[] helpText = new String[5]; + + GuiPanelSettings(GuiLiteLoaderPanel parentScreen, Minecraft minecraft) + { + super(minecraft); + + this.parentScreen = parentScreen; + + this.helpText[0] = I18n.format("gui.settings.showtab.help1"); + this.helpText[1] = I18n.format("gui.settings.showtab.help2"); + this.helpText[2] = I18n.format("gui.settings.notabhide.help1"); + this.helpText[3] = I18n.format("gui.settings.forceupdate.help1"); + this.helpText[4] = I18n.format("gui.settings.forceupdate.help2"); + } + + @Override + public void close() + { + this.hide = true; + } + + @Override + boolean isCloseRequested() + { + boolean hide = this.hide; + this.hide = false; + return hide; + } + + @Override + void setSize(int width, int height) + { + super.setSize(width, height); + + this.controls.add(new GuiButton(-1, this.width - 99 - MARGIN, this.height - BOTTOM + 9, 100, 20, I18n.format("gui.done"))); + this.controls.add(this.chkShowTab = new GuiCheckbox(0, 34, 90, I18n.format("gui.settings.showtab.label"))); + this.controls.add(this.chkNoHide = new GuiCheckbox(1, 34, 128, I18n.format("gui.settings.notabhide.label"))); + this.controls.add(this.chkForceUpdate = new GuiCheckbox(2, 34, 158, I18n.format("gui.settings.forceupdate.label"))); + + this.updateCheckBoxes(); + } + + private void updateCheckBoxes() + { + PanelManager panelManager = LiteLoader.getModPanelManager(); + + this.chkShowTab.checked = panelManager.isTabVisible(); + this.chkNoHide.checked = panelManager.isTabAlwaysExpanded(); + this.chkForceUpdate.checked = panelManager.isForceUpdateEnabled(); + } + + private void updateSettings() + { + PanelManager panelManager = LiteLoader.getModPanelManager(); + + panelManager.setTabVisible(this.chkShowTab.checked); + panelManager.setTabAlwaysExpanded(this.chkNoHide.checked); + panelManager.setForceUpdateEnabled(this.chkForceUpdate.checked); + } + + @Override + void draw(int mouseX, int mouseY, float partialTicks) + { + this.parentScreen.drawInfoPanel(mouseX, mouseY, partialTicks, 0, 38); + + FontRenderer fontRenderer = this.mc.fontRendererObj; + int brandColour = this.parentScreen.getBrandColour(); + + fontRenderer.drawString(this.helpText[0], 50, 104, brandColour); + fontRenderer.drawString(this.helpText[1], 50, 114, brandColour); + fontRenderer.drawString(this.helpText[2], 50, 142, brandColour); + fontRenderer.drawString(this.helpText[3], 50, 172, brandColour); + fontRenderer.drawString(this.helpText[4], 50, 182, brandColour); + + super.draw(mouseX, mouseY, partialTicks); + } + + @Override + void actionPerformed(GuiButton control) + { + if (control.id == -1) + { + this.close(); + return; + } + + if (control instanceof GuiCheckbox) + { + ((GuiCheckbox)control).checked = !((GuiCheckbox)control).checked; + this.updateSettings(); + } + } + + @Override + void keyPressed(char keyChar, int keyCode) + { + if (keyCode == Keyboard.KEY_ESCAPE) + { + this.close(); + } + } + + @Override + void onTick() + { + } + + @Override + void onHidden() + { + } + + @Override + void onShown() + { + } + + @Override + void mouseMoved(int mouseX, int mouseY) + { + } + + @Override + void mouseReleased(int mouseX, int mouseY, int mouseButton) + { + } + + @Override + void mouseWheelScrolled(int mouseWheelDelta) + { + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelUpdateCheck.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelUpdateCheck.java new file mode 100644 index 00000000..908abf6a --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiPanelUpdateCheck.java @@ -0,0 +1,232 @@ +package com.mumfrey.liteloader.client.gui; + +import java.net.URI; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.resources.I18n; + +import org.lwjgl.input.Keyboard; + +import com.mumfrey.liteloader.core.LiteLoaderUpdateSite; +import com.mumfrey.liteloader.launch.ClassPathUtilities; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.update.UpdateSite; + +/** + * "Check for updates" panel which docks in the mod info screen + * + * @author Adam Mummery-Smith + */ +class GuiPanelUpdateCheck extends GuiPanel +{ + private static final int WHITE = 0xFFFFFFFF; + + /** + * URI to open if a new version is available + */ + private static final URI DOWNLOAD_URI = URI.create("http://dl.liteloader.com"); + + private final GuiLiteLoaderPanel parentScreen; + + /** + * Update site to contact + */ + private final UpdateSite updateSite; + + /** + * Panel title + */ + private final String panelTitle; + + /** + * Buttons + */ + private GuiButton btnCheck, btnDownload; + + /** + * Throbber frame + */ + private int throb; + + private boolean canForceUpdate, updateForced; + + public GuiPanelUpdateCheck(GuiLiteLoaderPanel parentScreen, Minecraft minecraft, UpdateSite updateSite, String updateName, + LoaderProperties properties) + { + super(minecraft); + + this.parentScreen = parentScreen; + this.updateSite = updateSite; + this.panelTitle = I18n.format("gui.updates.title", updateName); + + this.canForceUpdate = (updateSite instanceof LiteLoaderUpdateSite && ((LiteLoaderUpdateSite)updateSite).canForceUpdate(properties)); + } + + @Override + void setSize(int width, int height) + { + super.setSize(width, height); + + this.controls.add(new GuiButton(0, this.width - 99 - MARGIN, this.height - BOTTOM + 9, 100, 20, + this.updateForced ? I18n.format("gui.exitgame") : I18n.format("gui.done"))); + this.controls.add(this.btnCheck = new GuiButton(1, MARGIN + 16, TOP + 16, 100, 20, + I18n.format("gui.checknow"))); + this.controls.add(this.btnDownload = new GuiButton(2, MARGIN + 16, TOP + 118, 100, 20, + this.canForceUpdate ? I18n.format("gui.forceupdate") : I18n.format("gui.downloadupdate"))); + } + + @Override + void draw(int mouseX, int mouseY, float partialTicks) + { + FontRenderer fontRenderer = this.mc.fontRendererObj; + + // Draw panel title + fontRenderer.drawString(this.panelTitle, MARGIN, TOP - 14, GuiPanelUpdateCheck.WHITE); + + // Draw top and bottom horizontal bars + drawRect(MARGIN, TOP - 4, this.width - MARGIN, TOP - 3, 0xFF999999); + drawRect(MARGIN, this.height - BOTTOM + 2, this.width - MARGIN, this.height - BOTTOM + 3, 0xFF999999); + + this.btnCheck.enabled = !this.updateForced && !this.updateSite.isCheckInProgress(); + this.btnDownload.visible = false; + + if (this.updateSite.isCheckInProgress()) + { + this.drawThrobber(MARGIN, TOP + 40, this.throb); + fontRenderer.drawString(I18n.format("gui.updates.status.checking", ""), MARGIN + 18, TOP + 44, GuiPanelUpdateCheck.WHITE); + } + else if (this.updateSite.isCheckComplete()) + { + boolean success = this.updateSite.isCheckSucceess(); + String status = success ? I18n.format("gui.updates.status.success") : I18n.format("gui.updates.status.failed"); + fontRenderer.drawString(I18n.format("gui.updates.status.checking", status), MARGIN + 18, TOP + 44, GuiPanelUpdateCheck.WHITE); + + if (success) + { + fontRenderer.drawString(I18n.format("gui.updates.available.title"), MARGIN + 18, TOP + 70, GuiPanelUpdateCheck.WHITE); + if (this.updateSite.isUpdateAvailable()) + { + this.btnDownload.visible = !this.updateForced; + fontRenderer.drawString(I18n.format("gui.updates.available.newversion"), MARGIN + 18, TOP + 84, GuiPanelUpdateCheck.WHITE); + fontRenderer.drawString(I18n.format("gui.updates.available.version", this.updateSite.getAvailableVersion()), + MARGIN + 18, TOP + 94, GuiPanelUpdateCheck.WHITE); + fontRenderer.drawString(I18n.format("gui.updates.available.date", this.updateSite.getAvailableVersionDate()), + MARGIN + 18, TOP + 104, GuiPanelUpdateCheck.WHITE); + + if (this.updateForced) + { + fontRenderer.drawString(I18n.format("gui.updates.forced"), MARGIN + 18, TOP + 144, 0xFFFFAA00); + } + } + else + { + fontRenderer.drawString(I18n.format("gui.updates.available.nonewversion"), MARGIN + 18, TOP + 84, GuiPanelUpdateCheck.WHITE); + } + } + } + else + { + fontRenderer.drawString(I18n.format("gui.updates.status.idle"), MARGIN + 18, TOP + 44, GuiPanelUpdateCheck.WHITE); + } + + super.draw(mouseX, mouseY, partialTicks); + } + + @Override + public void close() + { + if (this.updateForced) + { + return; + } + + super.close(); + } + + /** + * @param control + */ + @Override + void actionPerformed(GuiButton control) + { + if (control.id == 0) + { + if (this.updateForced) + { + ClassPathUtilities.terminateRuntime(0); + return; + } + + this.close(); + } + if (control.id == 1) this.updateSite.beginUpdateCheck(); + if (control.id == 2) + { + if (this.canForceUpdate && ((LiteLoaderUpdateSite)this.updateSite).forceUpdate()) + { + this.updateForced = true; + this.parentScreen.setToggleable(false); + ScaledResolution sr = new ScaledResolution(this.mc, this.mc.displayWidth, this.mc.displayHeight); + this.parentScreen.setWorldAndResolution(this.mc, sr.getScaledWidth(), sr.getScaledHeight()); + } + else + { + this.openURI(GuiPanelUpdateCheck.DOWNLOAD_URI); + } + + this.btnDownload.enabled = false; + } + } + + private void openURI(URI uri) + { + try + { + Class desktop = Class.forName("java.awt.Desktop"); + Object instance = desktop.getMethod("getDesktop").invoke(null); + desktop.getMethod("browse", URI.class).invoke(instance, uri); + } + catch (Throwable th) {} + } + + @Override + void onTick() + { + this.throb++; + } + + @Override + void onHidden() + { + } + + @Override + void onShown() + { + } + + @Override + void keyPressed(char keyChar, int keyCode) + { + if (keyCode == Keyboard.KEY_ESCAPE) this.close(); + } + + @Override + void mouseMoved(int mouseX, int mouseY) + { + } + + @Override + void mouseReleased(int mouseX, int mouseY, int mouseButton) + { + } + + @Override + void mouseWheelScrolled(int mouseWheelDelta) + { + } + +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiScrollPanel.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiScrollPanel.java new file mode 100644 index 00000000..8853e9c1 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiScrollPanel.java @@ -0,0 +1,208 @@ +package com.mumfrey.liteloader.client.gui; + +import static com.mumfrey.liteloader.gl.GL.*; +import static com.mumfrey.liteloader.gl.GLClippingPlanes.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; + +import org.lwjgl.input.Keyboard; + +/** + * Basic non-interactive scrollable panel using OpenGL clipping planes + * + * TODO handle interaction + * + * @author Adam Mummery-Smith + */ +class GuiScrollPanel extends GuiPanel +{ + private ScrollPanelContent content; + + /** + * Scroll bar for the panel + */ + private GuiSimpleScrollBar scrollBar = new GuiSimpleScrollBar(); + + /** + * Left edge coord - specified + */ + private int left; + + /** + * Top edge coord - specified + */ + private int top; + + /** + * + */ + private int contentHeight; + + public GuiScrollPanel(Minecraft minecraft, ScrollPanelContent content, int left, int top, int width, int height) + { + super(minecraft); + + this.setContent(content); + } + + public void setContent(ScrollPanelContent content) + { + if (content == null) + { + throw new IllegalArgumentException("Scroll pane content can not be null"); + } + + this.content = content; + } + + @Override + void setSize(int width, int height) + { + this.width = width; + this.height = height; + + this.updateHeight(); + } + + public void setSizeAndPosition(int left, int top, int width, int height) + { + this.left = left; + this.top = top; + + this.setSize(width, height); + } + + public void updateHeight() + { + this.contentHeight = this.content.getScrollPanelContentHeight(this); + this.scrollBar.setMaxValue(this.contentHeight - this.height); + } + + public void scrollToBottom() + { + this.scrollBar.setValue(this.contentHeight); + } + + public void scrollToTop() + { + this.scrollBar.setValue(0); + } + + public void scrollBy(int amount) + { + this.scrollBar.offsetValue(amount); + } + + public GuiButton addControl(GuiButton control) + { + this.controls.add(control); + return control; + } + + /** + * Draw the panel and chrome + * + * @param mouseX + * @param mouseY + * @param partialTicks + */ + @Override + public void draw(int mouseX, int mouseY, float partialTicks) + { + int scrollPosition = this.scrollBar.getValue(); + + // Clip rect + glEnableClipping(this.left, this.left + this.width - 6, this.top, this.top + this.height); + + // Offset by scroll + glPushMatrix(); + glTranslatef(this.left, this.top - scrollPosition, 0.0F); + + this.content.drawScrollPanelContent(this, mouseX, mouseY, partialTicks, scrollPosition, this.height); + + super.draw(mouseX - this.left, mouseY + scrollPosition - this.top, partialTicks); + + // Disable clip rect + glDisableClipping(); + + // Restore transform + glPopMatrix(); + + // Update and draw scroll bar + this.scrollBar.drawScrollBar(mouseX, mouseY, partialTicks, this.left + this.width - 5, this.top, 5, this.height, + Math.max(this.height, this.contentHeight)); + } + + @Override + public void mouseWheelScrolled(int mouseWheelDelta) + { + this.scrollBy(-mouseWheelDelta / 8); + } + + @Override + public void mousePressed(int mouseX, int mouseY, int mouseButton) + { + mouseY += this.scrollBar.getValue() - this.top; + mouseX -= this.left; + super.mousePressed(mouseX, mouseY, mouseButton); + + if (mouseX > 0 && mouseX < this.width && mouseY > 0 && mouseY < this.contentHeight) + { + this.content.scrollPanelMousePressed(this, mouseX, mouseY, mouseButton); + } + + if (mouseButton == 0) + { + if (this.scrollBar.wasMouseOver()) + { + this.scrollBar.setDragging(true); + } + } + } + + @Override + public void mouseReleased(int mouseX, int mouseY, int mouseButton) + { + if (mouseButton == 0) + { + this.scrollBar.setDragging(false); + } + } + + @Override + public void keyPressed(char keyChar, int keyCode) + { + if (keyCode == Keyboard.KEY_UP) this.scrollBar.offsetValue(-10); + if (keyCode == Keyboard.KEY_DOWN) this.scrollBar.offsetValue(10); + if (keyCode == Keyboard.KEY_PRIOR) this.scrollBar.offsetValue(-this.height + 10); + if (keyCode == Keyboard.KEY_NEXT) this.scrollBar.offsetValue(this.height - 10); + if (keyCode == Keyboard.KEY_HOME) this.scrollBar.setValue(0); + if (keyCode == Keyboard.KEY_END) this.scrollBar.setValue(this.contentHeight); + } + + @Override + void onTick() + { + } + + @Override + void onHidden() + { + } + + @Override + void onShown() + { + } + + @Override + void mouseMoved(int mouseX, int mouseY) + { + } + + @Override + void actionPerformed(GuiButton control) + { + this.content.scrollPanelActionPerformed(this, control); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiSimpleScrollBar.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiSimpleScrollBar.java new file mode 100644 index 00000000..51cc7f37 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/GuiSimpleScrollBar.java @@ -0,0 +1,153 @@ +package com.mumfrey.liteloader.client.gui; + +import net.minecraft.client.gui.Gui; + +/** + * Extremely simple scrollbar implementation + * + * @author Adam Mummery-Smith + */ +public class GuiSimpleScrollBar extends Gui +{ + /** + * Current value + */ + private int value = 0; + + /** + * Current maximum value + */ + private int maxValue = 100; + + private int backColour = 0x44FFFFFF; + private int foreColour = 0xFFFFFFFF; + + /** + * True if mouse was over the drag bar when last drawn + */ + private boolean mouseOver = false; + + /** + * True if currently dragging the scroll bar + */ + private boolean dragging = false; + + /** + * Value prior to starting to drag + */ + private int mouseDownValue = 0; + + /** + * mouse Y coordinate prior to starting to drag + */ + private int mouseDownY = 0; + + /** + * Get the current scroll value + */ + public int getValue() + { + return this.value; + } + + /** + * Set the scroll value, the value is clamped between 0 and the current max + * value. + */ + public void setValue(int value) + { + this.value = Math.min(Math.max(value, 0), this.maxValue); + } + + /** + * Offset the scroll value by the specified amount, the value is clamped + * between 0 and the current max value. + */ + public void offsetValue(int offset) + { + this.setValue(this.value + offset); + } + + /** + * Get the current max value + */ + public int getMaxValue() + { + return this.maxValue; + } + + /** + * Sets the current max value + */ + public void setMaxValue(int maxValue) + { + this.maxValue = Math.max(0, maxValue); + this.value = Math.min(this.value, this.maxValue); + } + + /** + * Returns true if the mouse was over the drag bar on the last render + */ + public boolean wasMouseOver() + { + return this.mouseOver; + } + + /** + * Set the current dragging state + */ + public void setDragging(boolean dragging) + { + this.dragging = dragging; + } + + /** + * Draw the scroll bar + * + * @param mouseX + * @param mouseY + * @param partialTicks + * @param xPosition + * @param yPosition + * @param width + * @param height + * @param totalHeight + */ + public void drawScrollBar(int mouseX, int mouseY, float partialTicks, int xPosition, int yPosition, int width, int height, int totalHeight) + { + drawRect(xPosition, yPosition, xPosition + width, yPosition + height, this.backColour); + + if (totalHeight > 0) + { + int slideHeight = height - 2; + float pct = Math.min(1.0F, (float)slideHeight / (float)totalHeight); + int barHeight = (int)(pct * slideHeight); + int barTravel = slideHeight - barHeight; + int barPosition = yPosition + 1 + (this.maxValue > 0 ? (int)((this.value / (float)this.maxValue) * barTravel) : 0); + + drawRect(xPosition + 1, barPosition, xPosition + width - 1, barPosition + barHeight, this.foreColour); + + this.mouseOver = mouseX > xPosition && mouseX < xPosition + width && mouseY > barPosition && mouseY < barPosition + barHeight; + this.handleDrag(mouseY, barTravel); + } + } + + /** + * @param mouseY + * @param barTravel + */ + public void handleDrag(int mouseY, int barTravel) + { + if (this.dragging) + { + // Convert pixel delta to value delta + float valuePerPixel = (float)this.maxValue / barTravel; + this.setValue((int)(this.mouseDownValue + ((mouseY - this.mouseDownY) * valuePerPixel))); + } + else + { + this.mouseDownY = mouseY; + this.mouseDownValue = this.value; + } + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/ScrollPanelContent.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/ScrollPanelContent.java new file mode 100644 index 00000000..a2436103 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/ScrollPanelContent.java @@ -0,0 +1,14 @@ +package com.mumfrey.liteloader.client.gui; + +import net.minecraft.client.gui.GuiButton; + +public interface ScrollPanelContent +{ + public abstract int getScrollPanelContentHeight(GuiScrollPanel source); + + public abstract void drawScrollPanelContent(GuiScrollPanel source, int mouseX, int mouseY, float partialTicks, int scrollAmt, int visibleHeight); + + public abstract void scrollPanelActionPerformed(GuiScrollPanel source, GuiButton control); + + public abstract void scrollPanelMousePressed(GuiScrollPanel source, int mouseX, int mouseY, int mouseButton); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModInfoPanel.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModInfoPanel.java new file mode 100644 index 00000000..87b1d83a --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModInfoPanel.java @@ -0,0 +1,155 @@ +package com.mumfrey.liteloader.client.gui.modlist; + +import static com.mumfrey.liteloader.gl.GL.*; +import static com.mumfrey.liteloader.gl.GLClippingPlanes.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.resources.I18n; + +import com.google.common.base.Strings; +import com.mumfrey.liteloader.client.api.LiteLoaderBrandingProvider; +import com.mumfrey.liteloader.client.gui.GuiSimpleScrollBar; +import com.mumfrey.liteloader.client.util.render.IconAbsolute; +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.util.render.IconTextured; + +public class GuiModInfoPanel extends Gui +{ + private static final int TITLE_COLOUR = GuiModListPanel.WHITE; + private static final int AUTHORS_COLOUR = GuiModListPanel.WHITE; + private static final int DIVIDER_COLOUR = GuiModListPanel.GREY; + private static final int DESCRIPTION_COLOUR = GuiModListPanel.WHITE; + + private static final IconAbsolute infoIcon = new IconAbsolute(LiteLoaderBrandingProvider.ABOUT_TEXTURE, "Info", 12, 12, 146, 92, 158, 104); + + private final ModListEntry owner; + + private final FontRenderer fontRenderer; + + private final int brandColour; + + private final ModInfo modInfo; + + private GuiSimpleScrollBar scrollBar = new GuiSimpleScrollBar(); + + private boolean mouseOverPanel, mouseOverScrollBar; + + private boolean showHelp; + + private String helpTitle, helpText; + + public GuiModInfoPanel(ModListEntry owner, FontRenderer fontRenderer, int brandColour, ModInfo modInfo) + { + this.owner = owner; + this.fontRenderer = fontRenderer; + this.brandColour = brandColour; + this.modInfo = modInfo; + } + + public void draw(int mouseX, int mouseY, float partialTicks, int xPosition, int yPosition, int width, int height) + { + int bottom = height + yPosition; + int yPos = yPosition + 2; + + this.mouseOverPanel = this.isMouseOver(mouseX, mouseY, xPosition, yPos, width, height); + + this.fontRenderer.drawString(this.owner.getTitleText(), xPosition + 5, yPos, GuiModInfoPanel.TITLE_COLOUR); yPos += 10; + this.fontRenderer.drawString(this.owner.getVersionText(), xPosition + 5, yPos, GuiModListPanel.VERSION_TEXT_COLOUR); yPos += 10; + + drawRect(xPosition + 5, yPos, xPosition + width, yPos + 1, GuiModInfoPanel.DIVIDER_COLOUR); yPos += 4; // divider + + this.fontRenderer.drawString(I18n.format("gui.about.authors") + ": \2477" + this.modInfo.getAuthor(), xPosition + 5, yPos, + GuiModInfoPanel.AUTHORS_COLOUR); yPos += 10; + if (!Strings.isNullOrEmpty(this.modInfo.getURL())) + { + this.fontRenderer.drawString(this.modInfo.getURL(), xPosition + 5, yPos, GuiModListPanel.BLEND_2THRDS & this.brandColour); yPos += 10; + } + + drawRect(xPosition + 5, yPos, xPosition + width, yPos + 1, GuiModInfoPanel.DIVIDER_COLOUR); yPos += 4; // divider + drawRect(xPosition + 5, bottom - 1, xPosition + width, bottom, GuiModInfoPanel.DIVIDER_COLOUR); // divider + + glEnableClipping(-1, -1, yPos, bottom - 3); + + int scrollHeight = bottom - yPos - 3; + int contentHeight = this.drawContent(xPosition, width, yPos); + + this.scrollBar.setMaxValue(contentHeight - scrollHeight); + this.scrollBar.drawScrollBar(mouseX, mouseY, partialTicks, xPosition + width - 5, yPos, 5, scrollHeight, contentHeight); + + this.mouseOverScrollBar = this.isMouseOver(mouseX, mouseY, xPosition + width - 5, yPos, 5, scrollHeight); + } + + private int drawContent(int xPosition, int width, int yPos) + { + yPos -= this.scrollBar.getValue(); + + if (this.showHelp) + { + this.drawIcon(xPosition + 3, yPos, GuiModInfoPanel.infoIcon); yPos += 2; + this.fontRenderer.drawString(this.helpTitle, xPosition + 17, yPos, this.brandColour); yPos += 12; + return this.drawText(xPosition + 17, width - 24, yPos, this.helpText, GuiModInfoPanel.DESCRIPTION_COLOUR) + 15; + } + + return this.drawText(xPosition + 5, width - 11, yPos, this.modInfo.getDescription(), GuiModInfoPanel.DESCRIPTION_COLOUR); + } + + protected void drawIcon(int xPosition, int yPosition, IconTextured icon) + { + glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + Minecraft.getMinecraft().getTextureManager().bindTexture(icon.getTextureResource()); + + glEnableBlend(); + this.drawTexturedModalRect(xPosition, yPosition, icon.getUPos(), icon.getVPos(), icon.getIconWidth(), icon.getIconHeight()); + glDisableBlend(); + } + + private int drawText(int xPosition, int width, int yPos, String text, int colour) + { + int totalHeight = this.fontRenderer.splitStringWidth(text, width); + this.fontRenderer.drawSplitString(text, xPosition, yPos, width, colour); + return totalHeight; + } + + private boolean isMouseOver(int mouseX, int mouseY, int x, int y, int width, int height) + { + return mouseX > x && mouseX < x + width && mouseY > y && mouseY < y + height; + } + + public void mousePressed() + { + if (this.mouseOverScrollBar) + { + this.scrollBar.setDragging(true); + } + } + + public void mouseReleased() + { + this.scrollBar.setDragging(false); + } + + public boolean mouseWheelScrolled(int mouseWheelDelta) + { + if (this.mouseOverPanel) + { + this.scrollBar.offsetValue(-mouseWheelDelta / 8); + return true; + } + + return false; + } + + public void displayHelpMessage(String title, String text) + { + this.showHelp = true; + this.helpTitle = I18n.format(title); + this.helpText = I18n.format(text); + this.scrollBar.setValue(0); + } + + public void clearHelpMessage() + { + this.showHelp = false; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModListPanel.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModListPanel.java new file mode 100644 index 00000000..cced9b66 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModListPanel.java @@ -0,0 +1,271 @@ +package com.mumfrey.liteloader.client.gui.modlist; + +import static com.mumfrey.liteloader.gl.GL.*; +import static com.mumfrey.liteloader.gl.GLClippingPlanes.*; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; + +import com.mumfrey.liteloader.api.ModInfoDecorator; +import com.mumfrey.liteloader.client.gui.GuiLiteLoaderPanel; +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.util.render.IconClickable; +import com.mumfrey.liteloader.util.render.IconTextured; + +public class GuiModListPanel extends Gui +{ + static final int BLACK = 0xFF000000; + static final int DARK_GREY = 0xB0333333; + static final int GREY = 0xFF999999; + static final int WHITE = 0xFFFFFFFF; + + static final int BLEND_2THRDS = 0xB0FFFFFF; + static final int BLEND_HALF = 0x80FFFFFF; + + static final int API_COLOUR = 0xFFAA00AA; + static final int EXTERNAL_ENTRY_COLOUR = 0xFF47D1AA; + static final int MISSING_DEPENDENCY_COLOUR = 0xFFFFAA00; + static final int ERROR_COLOUR = 0xFFFF5555; + static final int ERROR_GRADIENT_COLOUR = 0xFFAA0000; + static final int ERROR_GRADIENT_COLOUR2 = 0xFF550000; + + static final int VERSION_TEXT_COLOUR = GuiModListPanel.GREY; + static final int GRADIENT_COLOUR2 = GuiModListPanel.BLEND_2THRDS & GuiModListPanel.DARK_GREY; + static final int HANGER_COLOUR = GuiModListPanel.GREY; + static final int HANGER_COLOUR_MOUSEOVER = GuiModListPanel.WHITE; + + static final int PANEL_HEIGHT = 32; + static final int PANEL_SPACING = 3; + + protected ModListEntry owner; + + /** + * For text display + */ + protected final FontRenderer fontRenderer; + + protected final int brandColour; + + protected final List decorators; + + protected final ModInfo modInfo; + + /** + * True if the mouse was over this mod on the last render + */ + private boolean mouseOver; + + private IconClickable mouseOverIcon = null; + + private List modIcons = new ArrayList(); + + public GuiModListPanel(ModListEntry owner, FontRenderer fontRenderer, int brandColour, ModInfo modInfo, List decorators) + { + this.owner = owner; + this.fontRenderer = fontRenderer; + this.brandColour = brandColour; + this.modInfo = modInfo; + this.decorators = decorators; + + for (ModInfoDecorator decorator : this.decorators) + { + decorator.addIcons(modInfo, this.modIcons); + } + } + + public void draw(int mouseX, int mouseY, float partialTicks, int xPosition, int yPosition, int width, boolean selected, int pass) + { + if (pass == 0) + { + this.render(mouseX, mouseY, partialTicks, xPosition, yPosition, width, selected); + } + else if (pass == 1) + { + this.postRender(mouseX, mouseY, partialTicks, xPosition, yPosition, width, selected); + } + } + + /** + * Draw this list entry as a list item + * + * @param mouseX + * @param mouseY + * @param partialTicks + * @param xPosition + * @param yPosition + * @param width + * @param selected + */ + protected void render(int mouseX, int mouseY, float partialTicks, int xPosition, int yPosition, int width, boolean selected) + { + int gradientColour = this.getGradientColour(selected); + int titleColour = this.getTitleColour(selected); + int statusColour = this.getStatusColour(selected); + + this.drawGradientRect(xPosition, yPosition, xPosition + width, yPosition + GuiModListPanel.PANEL_HEIGHT, gradientColour, + GuiModListPanel.GRADIENT_COLOUR2); + + String titleText = this.owner.getTitleText(); + String versionText = this.owner.getVersionText(); + String statusText = this.owner.getStatusText(); + + for (ModInfoDecorator decorator : this.decorators) + { + String newStatusText = decorator.modifyStatusText(this.modInfo, statusText); + if (newStatusText != null) statusText = newStatusText; + } + + this.fontRenderer.drawString(titleText, xPosition + 5, yPosition + 2, titleColour); + this.fontRenderer.drawString(versionText, xPosition + 5, yPosition + 12, GuiModListPanel.VERSION_TEXT_COLOUR); + this.fontRenderer.drawString(statusText, xPosition + 5, yPosition + 22, statusColour); + + this.updateMouseOver(mouseX, mouseY, xPosition, yPosition, width); + int hangerColour = this.mouseOver ? GuiModListPanel.HANGER_COLOUR_MOUSEOVER : GuiModListPanel.HANGER_COLOUR; + drawRect(xPosition, yPosition, xPosition + 1, yPosition + PANEL_HEIGHT, hangerColour); + + for (ModInfoDecorator decorator : this.decorators) + { + decorator.onDrawListEntry(mouseX, mouseY, partialTicks, xPosition, yPosition, width, GuiModListPanel.PANEL_HEIGHT, selected, + this.modInfo, gradientColour, titleColour, statusColour); + } + } + + /** + * @param mouseX + * @param mouseY + * @param xPosition + * @param yPosition + * @param width + */ + protected void updateMouseOver(int mouseX, int mouseY, int xPosition, int yPosition, int width) + { + this.mouseOver = this.isMouseOver(mouseX, mouseY, xPosition, yPosition, width, PANEL_HEIGHT); + } + + protected void postRender(int mouseX, int mouseY, float partialTicks, int xPosition, int yPosition, int width, boolean selected) + { + xPosition += (width - 14); + yPosition += (GuiModListPanel.PANEL_HEIGHT - 14); + + this.mouseOverIcon = null; + + for (IconTextured icon : this.modIcons) + { + xPosition = this.drawPropertyIcon(xPosition, yPosition, icon, mouseX, mouseY); + } + } + + protected int drawPropertyIcon(int xPosition, int yPosition, IconTextured icon, int mouseX, int mouseY) + { + glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + Minecraft.getMinecraft().getTextureManager().bindTexture(icon.getTextureResource()); + + glEnableBlend(); + this.drawTexturedModalRect(xPosition, yPosition, icon.getUPos(), icon.getVPos(), icon.getIconWidth(), icon.getIconHeight()); + glDisableBlend(); + + if (mouseX >= xPosition && mouseX <= xPosition + 12 && mouseY >= yPosition && mouseY <= yPosition + 12) + { + String tooltipText = icon.getDisplayText(); + if (tooltipText != null) + { + glDisableClipping(); + GuiLiteLoaderPanel.drawTooltip(this.fontRenderer, tooltipText, mouseX, mouseY, 4096, 4096, GuiModListPanel.WHITE, + GuiModListPanel.BLEND_HALF & GuiModListPanel.BLACK); + glEnableClipping(); + } + + if (icon instanceof IconClickable) this.mouseOverIcon = (IconClickable)icon; + } + + return xPosition - 14; + } + + /** + * @param selected + */ + protected int getGradientColour(boolean selected) + { + return GuiModListPanel.BLEND_2THRDS + & (this.owner.isErrored() + ? (selected ? GuiModListPanel.ERROR_GRADIENT_COLOUR : GuiModListPanel.ERROR_GRADIENT_COLOUR2) + : (selected ? (this.owner.isExternal() ? GuiModListPanel.EXTERNAL_ENTRY_COLOUR : this.brandColour) : GuiModListPanel.BLACK)); + } + + /** + * @param selected + */ + protected int getTitleColour(boolean selected) + { + if (this.owner.isMissingDependencies()) return GuiModListPanel.MISSING_DEPENDENCY_COLOUR; + if (this.owner.isMissingAPIs()) return GuiModListPanel.API_COLOUR; + if (this.owner.isErrored()) return GuiModListPanel.ERROR_COLOUR; + if (!this.owner.isActive()) return GuiModListPanel.GREY; + return this.owner.isExternal() ? GuiModListPanel.EXTERNAL_ENTRY_COLOUR : GuiModListPanel.WHITE; + } + + /** + * @param selected + */ + protected int getStatusColour(boolean selected) + { + return this.owner.isExternal() ? GuiModListPanel.EXTERNAL_ENTRY_COLOUR : this.brandColour; + } + + public boolean isVisible() + { + return true; + } + + public int getSpacing() + { + return GuiModListPanel.PANEL_SPACING; + } + + public int getHeight() + { + return GuiModListPanel.PANEL_HEIGHT; + } + + public int getTotalHeight() + { + return GuiModListPanel.PANEL_HEIGHT + GuiModListPanel.PANEL_SPACING; + } + + protected boolean isMouseOver(int mouseX, int mouseY, int x, int y, int width, int height) + { + return mouseX > x && mouseX < x + width && mouseY > y && mouseY < y + height; + } + + public boolean isMouseOverIcon() + { + return this.mouseOver && this.mouseOverIcon != null; + } + + public boolean isMouseOver() + { + return this.mouseOver; + } + + public void iconClick(Object source) + { + if (this.mouseOverIcon != null) + { + this.mouseOverIcon.onClicked(source, this); + } + } + + public void mousePressed(int mouseX, int mouseY, int mouseButton) + { + this.owner.clearHelpMessage(); + } + + public void displayModHelpMessage(ModInfo mod, String title, String text) + { + this.owner.displayHelpMessage(title, text); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModListPanelInvalid.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModListPanelInvalid.java new file mode 100644 index 00000000..82bee8a6 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/GuiModListPanelInvalid.java @@ -0,0 +1,53 @@ +package com.mumfrey.liteloader.client.gui.modlist; + +import java.util.List; + +import net.minecraft.client.gui.FontRenderer; + +import com.mumfrey.liteloader.api.ModInfoDecorator; +import com.mumfrey.liteloader.core.ModInfo; + +public class GuiModListPanelInvalid extends GuiModListPanel +{ + private static final int BAD_PANEL_HEIGHT = 22; + + public GuiModListPanelInvalid(ModListEntry owner, FontRenderer fontRenderer, int brandColour, ModInfo modInfo, + List decorators) + { + super(owner, fontRenderer, brandColour, modInfo, decorators); + } + + @Override + protected void render(int mouseX, int mouseY, float partialTicks, int xPosition, int yPosition, int width, boolean selected) + { + int gradientColour = selected ? ERROR_GRADIENT_COLOUR : ERROR_GRADIENT_COLOUR2; + + this.drawGradientRect(xPosition, yPosition, xPosition + width, yPosition + 22, gradientColour, GuiModListPanel.GRADIENT_COLOUR2); + + String titleText = this.owner.getTitleText(); + String reasonText = this.modInfo.getDescription(); + + this.fontRenderer.drawString(titleText, xPosition + 5, yPosition + 2, 0xFF8888); + this.fontRenderer.drawString(reasonText, xPosition + 5, yPosition + 12, GuiModListPanel.ERROR_GRADIENT_COLOUR); + + this.updateMouseOver(mouseX, mouseY, xPosition, yPosition, width); + drawRect(xPosition, yPosition, xPosition + 1, yPosition + 22, ERROR_COLOUR); + } + + @Override + protected void postRender(int mouseX, int mouseY, float partialTicks, int xPosition, int yPosition, int width, boolean selected) + { + } + + @Override + public int getHeight() + { + return GuiModListPanelInvalid.BAD_PANEL_HEIGHT; + } + + @Override + public int getTotalHeight() + { + return GuiModListPanelInvalid.BAD_PANEL_HEIGHT + GuiModListPanel.PANEL_SPACING; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModList.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModList.java new file mode 100644 index 00000000..073ae4a8 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModList.java @@ -0,0 +1,273 @@ +package com.mumfrey.liteloader.client.gui.modlist; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.I18n; + +import org.lwjgl.input.Keyboard; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.api.ModInfoDecorator; +import com.mumfrey.liteloader.client.gui.GuiLiteLoaderPanel; +import com.mumfrey.liteloader.core.LiteLoaderMods; +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.interfaces.Loadable; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.modconfig.ConfigManager; + +public class ModList +{ + private final ModListContainer container; + + private final ConfigManager configManager; + + /** + * List of enumerated mods + */ + private final List mods = new ArrayList(); + + /** + * Currently selected mod + */ + private ModListEntry selectedMod = null; + + private boolean hasConfig = false; + + public ModList(ModListContainer container, Minecraft minecraft, LiteLoaderMods mods, LoaderEnvironment environment, ConfigManager configManager, + int brandColour, List decorators) + { + this.container = container; + this.configManager = configManager; + + this.populate(minecraft, mods, environment, brandColour, decorators); + } + + /** + * @param minecraft + * @param mods + * @param environment + * @param brandColour + * @param decorators + */ + protected void populate(Minecraft minecraft, LiteLoaderMods mods, LoaderEnvironment environment, int brandColour, + List decorators) + { + // Add mods to this treeset first, in order to sort them + Map sortedMods = new TreeMap(); + + // Active mods + for (ModInfo> mod : mods.getLoadedMods()) + { + ModListEntry modListEntry = new ModListEntry(this, mods, environment, minecraft.fontRendererObj, brandColour, decorators, mod); + sortedMods.put(modListEntry.getKey(), modListEntry); + } + + // Disabled mods + for (ModInfo disabledMod : mods.getDisabledMods()) + { + ModListEntry modListEntry = new ModListEntry(this, mods, environment, minecraft.fontRendererObj, brandColour, decorators, disabledMod); + sortedMods.put(modListEntry.getKey(), modListEntry); + } + + // Show bad containers if no other containers are found, should help users realise they have the wrong mod version! + if (sortedMods.size() == 0) + { + for (ModInfo badMod : mods.getBadContainers()) + { + ModListEntry modListEntry = new ModListEntry(this, mods, environment, minecraft.fontRendererObj, brandColour, decorators, badMod); + sortedMods.put(modListEntry.getKey(), modListEntry); + } + } + + // Injected tweaks + for (ModInfo> injectedTweak : mods.getInjectedTweaks()) + { + ModListEntry modListEntry = new ModListEntry(this, mods, environment, minecraft.fontRendererObj, brandColour, decorators, injectedTweak); + sortedMods.put(modListEntry.getKey(), modListEntry); + } + + // Add the sorted mods to the mods list + this.mods.addAll(sortedMods.values()); + + // Select the first mod in the list + if (this.mods.size() > 0) + { + this.selectedMod = this.mods.get(0); + } + } + + public GuiLiteLoaderPanel getParentScreen() + { + return this.container.getParentScreen(); + } + + public LiteMod getSelectedModInstance() + { + return this.selectedMod != null ? this.selectedMod.getModInstance() : null; + } + + public Class getSelectedModClass() + { + return this.selectedMod != null ? this.selectedMod.getModClass() : null; + } + + public void setSize(int width, int height) + { + this.selectMod(this.selectedMod); + } + + public void onTick() + { + for (ModListEntry mod : this.mods) + { + mod.onTick(); + } + } + + public void mousePressed(int mouseX, int mouseY, int mouseButton) + { + ModListEntry lastSelectedMod = this.selectedMod; + + for (ModListEntry mod : this.mods) + { + mod.mousePressed(mouseX, mouseY, mouseButton); + } + + if (this.selectedMod != null && this.selectedMod == lastSelectedMod) + { + this.selectedMod.getInfoPanel().mousePressed(); + } + } + + public boolean keyPressed(char keyChar, int keyCode) + { + if (keyCode == Keyboard.KEY_UP) + { + int selectedIndex = this.mods.indexOf(this.selectedMod) - 1; + if (selectedIndex > -1) this.selectMod(this.mods.get(selectedIndex)); + this.scrollSelectedModIntoView(); + return true; + } + else if (keyCode == Keyboard.KEY_DOWN) + { + int selectedIndex = this.mods.indexOf(this.selectedMod); + if (selectedIndex > -1 && selectedIndex < this.mods.size() - 1) this.selectMod(this.mods.get(selectedIndex + 1)); + this.scrollSelectedModIntoView(); + return true; + } + else if (keyCode == Keyboard.KEY_SPACE + || keyCode == Keyboard.KEY_RETURN + || keyCode == Keyboard.KEY_NUMPADENTER + || keyCode == Keyboard.KEY_RIGHT) + { + this.toggleSelectedMod(); + return true; + } + + return false; + } + + public void mouseReleased(int mouseX, int mouseY, int mouseButton) + { + if (this.selectedMod != null) + { + this.selectedMod.getInfoPanel().mouseReleased(); + } + } + + public boolean mouseWheelScrolled(int mouseWheelDelta) + { + return this.selectedMod != null && this.selectedMod.getInfoPanel().mouseWheelScrolled(mouseWheelDelta); + } + + public int drawModList(int mouseX, int mouseY, float partialTicks, int left, int top, int width, int height) + { + this.drawModListPass(mouseX, mouseY, partialTicks, left, top, width, 0); + return this.drawModListPass(mouseX, mouseY, partialTicks, left, top, width, 1); + } + + protected int drawModListPass(int mouseX, int mouseY, float partialTicks, int left, int top, int width, int pass) + { + int yPos = top; + for (ModListEntry mod : this.mods) + { + GuiModListPanel panel = mod.getListPanel(); + if (panel.isVisible()) + { + if (yPos > 0) yPos += panel.getSpacing(); + panel.draw(mouseX, mouseY, partialTicks, left, yPos, width, mod == this.selectedMod, pass); + yPos += panel.getHeight(); + } + } + return yPos; + } + + public void drawModPanel(int mouseX, int mouseY, float partialTicks, int left, int top, int width, int height) + { + if (this.selectedMod != null) + { + this.selectedMod.getInfoPanel().draw(mouseX, mouseY, partialTicks, left, top, width, height); + } + } + + /** + * @param mod Mod list entry to select + */ + void selectMod(ModListEntry mod) + { + if (this.selectedMod != null) + { + this.selectedMod.getInfoPanel().mouseReleased(); + } + + this.selectedMod = mod; + this.hasConfig = false; + this.container.setEnableButtonVisible(false); + this.container.setConfigButtonVisible(false); + + if (this.selectedMod != null && this.selectedMod.canBeToggled()) + { + this.container.setEnableButtonVisible(true); + this.container.setEnableButtonText(this.selectedMod.willBeEnabled() ? I18n.format("gui.disablemod") : I18n.format("gui.enablemod")); + this.hasConfig = this.configManager.hasPanel(this.selectedMod.getModClass()); + this.container.setConfigButtonVisible(this.hasConfig); + } + } + + /** + * Toggle the selected mod's enabled status + */ + public void toggleSelectedMod() + { + if (this.selectedMod != null) + { + this.selectedMod.toggleEnabled(); + this.selectMod(this.selectedMod); + } + } + + private void scrollSelectedModIntoView() + { + if (this.selectedMod == null) return; + + int yPos = 0; + for (ModListEntry mod : this.mods) + { + if (mod == this.selectedMod) break; + yPos += mod.getListPanel().getTotalHeight(); + } + + int modHeight = this.selectedMod.getListPanel().getTotalHeight(); + this.container.scrollTo(yPos, yPos + modHeight); + } + + public void showConfig(ModListEntry modListEntry) + { + this.container.showConfig(); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModListContainer.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModListContainer.java new file mode 100644 index 00000000..4e0011b5 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModListContainer.java @@ -0,0 +1,18 @@ +package com.mumfrey.liteloader.client.gui.modlist; + +import com.mumfrey.liteloader.client.gui.GuiLiteLoaderPanel; + +public interface ModListContainer +{ + public abstract GuiLiteLoaderPanel getParentScreen(); + + public abstract void setEnableButtonVisible(boolean visible); + + public abstract void setConfigButtonVisible(boolean visible); + + public abstract void setEnableButtonText(String displayString); + + public abstract void showConfig(); + + public abstract void scrollTo(int yPos, int modHeight); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModListEntry.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModListEntry.java new file mode 100644 index 00000000..13a28170 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/modlist/ModListEntry.java @@ -0,0 +1,334 @@ +package com.mumfrey.liteloader.client.gui.modlist; + +import java.util.List; +import java.util.Set; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.resources.I18n; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.api.ModInfoDecorator; +import com.mumfrey.liteloader.core.LiteLoaderMods; +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.interfaces.Loadable; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.launch.LoaderEnvironment; + +/** + * Represents a mod in the mod info screen, keeps track of mod information and + * provides methods for displaying the mod in the mod list and drawing the + * selected mod info. + * + * @author Adam Mummery-Smith + */ +public class ModListEntry +{ + private final ModList modList; + + private final LiteLoaderMods mods; + + private final ModInfo modInfo; + + private GuiModListPanel listPanel; + + private GuiModInfoPanel infoPanel; + + /** + * Whether the mod is currently active + */ + private boolean isActive; + + private boolean isValid; + + private boolean isMissingDependencies; + + private boolean isMissingAPIs; + + private boolean isErrored; + + /** + * True if the mod is missing a dependency which has caused it not to load + */ + private Set missingDependencies; + + /** + * True if the mod is missing an API which has caused it not to load + */ + private Set missingAPIs; + + /** + * Whether the mod can be toggled, not all mods support this, eg. internal + * mods + */ + private boolean canBeToggled; + + /** + * Whether the mod WILL be enabled on the next startup, if the mod is active + * and has been disabled this will be false, and if it's currently disabled + * by has been toggled then it will be true. + */ + private boolean willBeEnabled; + + /** + * True if this is not a mod but an external jar + */ + private boolean isExternal; + + /** + * Timer used to handle double-clicking on a mod + */ + private int doubleClickTime = 0; + + /** + * @param modList + * @param mods + * @param environment + * @param fontRenderer + * @param brandColour + * @param decorators + * @param modInfo + */ + ModListEntry(ModList modList, LiteLoaderMods mods, LoaderEnvironment environment, FontRenderer fontRenderer, int brandColour, + List decorators, ModInfo modInfo) + { + this.modList = modList; + this.mods = mods; + this.modInfo = modInfo; + + this.isActive = modInfo.isActive(); + this.isValid = modInfo.isValid(); + this.canBeToggled = modInfo.isToggleable() && mods.getEnabledModsList().saveAllowed(); + this.willBeEnabled = mods.isModEnabled(this.modInfo.getIdentifier()); + this.isExternal = modInfo.getContainer().isExternalJar(); + this.isErrored = modInfo.getStartupErrors() != null && modInfo.getStartupErrors().size() > 0; + + if (!modInfo.isActive() && this.isValid) + { + this.isActive = modInfo.getContainer().isEnabled(environment); + + Loadable modContainer = modInfo.getContainer(); + if (modContainer instanceof LoadableMod) + { + LoadableMod loadableMod = (LoadableMod)modContainer; + + this.missingDependencies = loadableMod.getMissingDependencies(); + this.missingAPIs = loadableMod.getMissingAPIs(); + this.isMissingDependencies = this.missingDependencies.size() > 0; + this.isMissingAPIs = this.missingAPIs.size() > 0; + } + } + + this.initPanels(fontRenderer, brandColour, decorators, modInfo); + } + + /** + * @param fontRenderer + * @param brandColour + * @param decorators + * @param modInfo + */ + protected void initPanels(FontRenderer fontRenderer, int brandColour, List decorators, ModInfo modInfo) + { + this.infoPanel = new GuiModInfoPanel(this, fontRenderer, brandColour, modInfo); + + if (this.isValid) + { + this.listPanel = new GuiModListPanel(this, fontRenderer, brandColour, modInfo, decorators); + } + else + { + this.listPanel = new GuiModListPanelInvalid(this, fontRenderer, brandColour, modInfo, decorators); + } + } + + public void onTick() + { + if (this.doubleClickTime > 0) + { + this.doubleClickTime--; + } + } + + public void mousePressed(int mouseX, int mouseY, int mouseButton) + { + if (this.getListPanel().isMouseOver()) + { + this.modList.selectMod(this); + + if (this.getListPanel().isMouseOver()) + { + this.getListPanel().mousePressed(mouseX, mouseY, mouseButton); + } + + if (this.getListPanel().isMouseOverIcon()) + { + this.getListPanel().iconClick(this.modList.getParentScreen()); + } + else + { + // handle double-click + if (this.doubleClickTime > 0) + { + this.onDoubleClicked(); + } + } + + this.doubleClickTime = 5; + } + } + + protected void onDoubleClicked() + { + this.modList.showConfig(this); + } + + protected String getTitleText() + { + return this.modInfo.getDisplayName(); + } + + protected String getVersionText() + { + return I18n.format("gui.about.versiontext", this.modInfo.getVersion()); + } + + protected String getStatusText() + { + String statusText = this.isExternal ? I18n.format("gui.status.loaded") : I18n.format("gui.status.active"); + + if (this.isMissingAPIs) + { + statusText = "\2475" + I18n.format("gui.status.missingapis"); + if (this.canBeToggled && !this.willBeEnabled) statusText = "\247c" + I18n.format("gui.status.pending.disabled"); + } + else if (this.isMissingDependencies) + { + statusText = "\247e" + I18n.format("gui.status.missingdeps"); + if (this.canBeToggled && !this.willBeEnabled) statusText = "\247c" + I18n.format("gui.status.pending.disabled"); + } + else if (this.isErrored) + { + statusText = "\247c" + I18n.format("gui.status.startuperror"); + } + else if (this.canBeToggled) + { + if (!this.isActive && !this.willBeEnabled) statusText = "\2477" + I18n.format("gui.status.disabled"); + if (!this.isActive && this.willBeEnabled) statusText = "\247a" + I18n.format("gui.status.pending.enabled"); + if ( this.isActive && !this.willBeEnabled) statusText = "\247c" + I18n.format("gui.status.pending.disabled"); + } + + return statusText; + } + + /** + * Toggle the enablement status of this mod, if supported + */ + public void toggleEnabled() + { + if (this.canBeToggled) + { + this.willBeEnabled = !this.willBeEnabled; + this.mods.setModEnabled(this.modInfo.getIdentifier(), this.willBeEnabled); + } + } + + protected void displayHelpMessage(String title, String text) + { + this.infoPanel.displayHelpMessage(title, text); + } + + public void clearHelpMessage() + { + this.infoPanel.clearHelpMessage(); + } + + public String getKey() + { + return (this.isErrored ? "0000" : "") + this.modInfo.getIdentifier() + Integer.toHexString(this.hashCode()); + } + + public ModInfo getModInfo() + { + return this.modInfo; + } + + public LiteMod getModInstance() + { + return this.modInfo.getMod(); + } + + public Class getModClass() + { + return this.modInfo.getModClass(); + } + + public String getName() + { + return this.modInfo.getDisplayName(); + } + + public String getVersion() + { + return this.modInfo.getVersion(); + } + + public String getAuthor() + { + return this.modInfo.getAuthor(); + } + + public String getDescription() + { + return this.modInfo.getDescription(); + } + + public boolean isEnabled() + { + return this.isActive; + } + + public boolean canBeToggled() + { + return this.canBeToggled; + } + + public boolean willBeEnabled() + { + return this.willBeEnabled; + } + + public boolean isActive() + { + return this.isActive; + } + + public boolean isErrored() + { + return this.isErrored; + } + + public boolean isExternal() + { + return this.isExternal; + } + + public boolean isMissingAPIs() + { + return this.isMissingAPIs; + } + + public boolean isMissingDependencies() + { + return this.isMissingDependencies; + } + + public GuiModListPanel getListPanel() + { + return this.listPanel; + } + + public GuiModInfoPanel getInfoPanel() + { + return this.infoPanel; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/startup/LoadingBar.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/startup/LoadingBar.java new file mode 100644 index 00000000..155414f1 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/gui/startup/LoadingBar.java @@ -0,0 +1,440 @@ +package com.mumfrey.liteloader.client.gui.startup; + +import static com.mumfrey.liteloader.gl.GL.*; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.imageio.ImageIO; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.ITextureObject; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.client.resources.IResource; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.shader.Framebuffer; +import net.minecraft.util.ResourceLocation; + +import org.lwjgl.opengl.Display; + +import com.mumfrey.liteloader.common.LoadingProgress; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Crappy implementation of a "Mojang Screen" loading bar + * + * @author Adam Mummery-Smith + */ +public class LoadingBar extends LoadingProgress +{ + private static LoadingBar instance; + + private static final String LOADING_MESSAGE_1 = "Starting Game..."; + private static final String LOADING_MESSAGE_2 = "Initialising..."; + + private int minecraftProgress = 0; + private int totalMinecraftProgress = 606; + + private int liteLoaderProgressScale = 3; + + private int liteLoaderProgress = 0; + private int totalLiteLoaderProgress = 0; + + private ResourceLocation textureLocation = new ResourceLocation("textures/gui/title/mojang.png"); + + private String minecraftMessage = LoadingBar.LOADING_MESSAGE_1; + private String message = ""; + + private Minecraft minecraft; + private TextureManager textureManager; + private FontRenderer fontRenderer; + + private Framebuffer fbo; + + private boolean enabled = true; + private boolean errored; + + private boolean calculatedColour = false; + private int barLuma = 0, r2 = 246, g2 = 136, b2 = 62; + + private int logIndex = 0; + private List logTail = new ArrayList(); + + public LoadingBar() + { + LoadingBar.instance = this; + } + + @Override + protected void _setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + @Override + protected void _dispose() + { + this.minecraft = null; + this.textureManager = null; + this.fontRenderer = null; + + this.disposeFbo(); + } + + private void disposeFbo() + { + if (this.fbo != null) + { + this.fbo.deleteFramebuffer(); + this.fbo = null; + } + } + + public static void incrementProgress() + { + if (LoadingBar.instance != null) LoadingBar.instance._incrementProgress(); + } + + protected void _incrementProgress() + { + this.message = this.minecraftMessage; + + this.minecraftProgress++; + this.render(); + } + + public static void initTextures() + { + if (LoadingBar.instance != null) LoadingBar.instance._initTextures(); + } + + protected void _initTextures() + { + this.minecraftMessage = LoadingBar.LOADING_MESSAGE_2; + } + + @Override + protected void _incLiteLoaderProgress() + { + this.liteLoaderProgress += this.liteLoaderProgressScale; + this.render(); + } + + @Override + protected void _setMessage(String message) + { + this.message = message; + this.render(); + } + + @Override + protected void _incLiteLoaderProgress(String message) + { + this.message = message; + this.liteLoaderProgress += this.liteLoaderProgressScale ; + this.render(); + } + + @Override + protected void _incTotalLiteLoaderProgress(int by) + { + this.totalLiteLoaderProgress += (by * this.liteLoaderProgressScale); + this.render(); + } + + /** + * + */ + private void render() + { + if (!this.enabled || this.errored) return; + + try + { + if (this.minecraft == null) this.minecraft = Minecraft.getMinecraft(); + if (this.textureManager == null) this.textureManager = this.minecraft.getTextureManager(); + + if (Display.isCreated() && this.textureManager != null) + { + if (this.fontRenderer == null) + { + this.fontRenderer = new FontRenderer(this.minecraft.gameSettings, new ResourceLocation("textures/font/ascii.png"), + this.textureManager, false); + this.fontRenderer.onResourceManagerReload(this.minecraft.getResourceManager()); + } + + double totalProgress = this.totalMinecraftProgress + this.totalLiteLoaderProgress; + double progress = (this.minecraftProgress + this.liteLoaderProgress) / totalProgress; + +// if (progress >= 1.0) LoadingBar.message = "Preparing..."; + + this.render(progress); + } + } + catch (Exception ex) + { + // Disable the loading bar if ANY errors occur + this.errored = true; + } + } + + /** + * @param progress + */ + private void render(double progress) + { + if (this.totalMinecraftProgress == -1) + { + this.totalMinecraftProgress = 606 - this.minecraftProgress; + this.minecraftProgress = 0; + } + + // Calculate the bar colour if we haven't already done that + if (!this.calculatedColour) + { + this.calculatedColour = true; + ITextureObject texture = this.textureManager.getTexture(this.textureLocation); + if (texture == null) + { + try + { + DynamicTexture textureData = this.loadTexture(this.minecraft.getResourceManager(), this.textureLocation); + this.textureLocation = this.minecraft.getTextureManager().getDynamicTextureLocation("loadingScreen", textureData); + this.findMostCommonColour(textureData.getTextureData()); + textureData.updateDynamicTexture(); + } + catch (IOException ex) + { + ex.printStackTrace(); + } + } + } + + ScaledResolution scaledResolution = new ScaledResolution(this.minecraft, this.minecraft.displayWidth, this.minecraft.displayHeight); + int scaleFactor = scaledResolution.getScaleFactor(); + int scaledWidth = scaledResolution.getScaledWidth(); + int scaledHeight = scaledResolution.getScaledHeight(); + + int fboWidth = scaledWidth * scaleFactor; + int fboHeight = scaledHeight * scaleFactor; + + if (this.fbo == null) + { + this.fbo = new Framebuffer(fboWidth, fboHeight, true); + } + else if (this.fbo.framebufferWidth != fboWidth || this.fbo.framebufferHeight != fboHeight) + { + this.fbo.createBindFramebuffer(fboWidth, fboHeight); + } + + this.fbo.bindFramebuffer(false); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0.0D, scaledWidth, scaledHeight, 0.0D, 1000.0D, 3000.0D); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glTranslatef(0.0F, 0.0F, -2000.0F); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glDisableLighting(); + glDisableFog(); + glDisableDepthTest(); + glEnableTexture2D(); + + this.textureManager.bindTexture(this.textureLocation); + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer worldRenderer = tessellator.getWorldRenderer(); + worldRenderer.startDrawingQuads(); + worldRenderer.setColorOpaque_I(0xFFFFFFFF); // TODO OBF MCPTEST func_178991_c - setColorOpaque_I + worldRenderer.addVertexWithUV(0.0D, scaledHeight, 0.0D, 0.0D, 0.0D); + worldRenderer.addVertexWithUV(scaledWidth, scaledHeight, 0.0D, 0.0D, 0.0D); + worldRenderer.addVertexWithUV(scaledWidth, 0.0D, 0.0D, 0.0D, 0.0D); + worldRenderer.addVertexWithUV(0.0D, 0.0D, 0.0D, 0.0D, 0.0D); + tessellator.draw(); + + glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + + int left = (scaledWidth - 256) / 2; + int top = (scaledHeight - 256) / 2; + int u1 = 0; + int v1 = 0; + int u2 = 256; + int v2 = 256; + + float texMapScale = 0.00390625F; + worldRenderer.startDrawingQuads(); + worldRenderer.setColorOpaque_I(0xFFFFFFFF); // TODO OBF MCPTEST func_178991_c - setColorOpaque_I + worldRenderer.addVertexWithUV(left + 0, top + v2, 0.0D, (u1 + 0) * texMapScale, (v1 + v2) * texMapScale); + worldRenderer.addVertexWithUV(left + u2, top + v2, 0.0D, (u1 + u2) * texMapScale, (v1 + v2) * texMapScale); + worldRenderer.addVertexWithUV(left + u2, top + 0, 0.0D, (u1 + u2) * texMapScale, (v1 + 0) * texMapScale); + worldRenderer.addVertexWithUV(left + 0, top + 0, 0.0D, (u1 + 0) * texMapScale, (v1 + 0) * texMapScale); + tessellator.draw(); + + glEnableTexture2D(); + glEnableColorLogic(); + glLogicOp(GL_OR_REVERSE); + this.fontRenderer.drawString(this.message, 1, scaledHeight - 19, 0xFF000000); + + if (LiteLoaderLogger.DEBUG) + { + int logBottom = this.minecraft.displayHeight - (20 * scaleFactor) - 2; + + glPushMatrix(); + glScalef(1.0F / scaleFactor, 1.0F / scaleFactor, 1.0F); + this.renderLogTail(logBottom); + glPopMatrix(); + } + + glDisableColorLogic(); + glEnableTexture2D(); + + double barHeight = 10.0D; + + double barWidth = scaledResolution.getScaledWidth_double() - 2.0D; + + glDisableTexture2D(); + glEnableBlend(); + glEnableAlphaTest(); + glAlphaFunc(GL_GREATER, 0.0F); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + +// tessellator.startDrawingQuads(); +// tessellator.setColorRGBA(0, 0, 0, 32); +// tessellator.addVertex(0.0D, scaledHeight, 0.0D); +// tessellator.setColorRGBA(0, 0, 0, 180); +// tessellator.addVertex(0.0D + scaledWidth, scaledHeight, 0.0D); +// tessellator.setColorRGBA(0, 0, 0, 0); +// tessellator.addVertex(0.0D + scaledWidth, (scaledHeight / 10), 0.0D); +// tessellator.addVertex(0.0D, scaledHeight - (scaledHeight / 3), 0.0D); +// tessellator.draw(); + + worldRenderer.startDrawingQuads(); + worldRenderer.setColorRGBA(this.barLuma, this.barLuma, this.barLuma, 128); // TODO OBF MCPTEST func_178961_b - setColorRGBA + worldRenderer.addVertex(0.0D, scaledHeight, 0.0D); + worldRenderer.addVertex(0.0D + scaledWidth, scaledHeight, 0.0D); + worldRenderer.addVertex(0.0D + scaledWidth, scaledHeight - barHeight, 0.0D); + worldRenderer.addVertex(0.0D, scaledHeight - barHeight, 0.0D); + tessellator.draw(); + + barHeight -= 1; + + worldRenderer.startDrawingQuads(); + worldRenderer.setColorRGBA(this.r2, this.g2, this.b2, 255); // TODO OBF MCPTEST func_178961_b - setColorRGBA + worldRenderer.addVertex(1.0D + barWidth * progress, scaledHeight - 1, 1.0D); + worldRenderer.addVertex(1.0D + barWidth * progress, scaledHeight - barHeight, 1.0D); + worldRenderer.setColorRGBA(0, 0, 0, 255); // TODO OBF MCPTEST func_178961_b - setColorRGBA + worldRenderer.addVertex(1.0D, scaledHeight - barHeight, 1.0D); + worldRenderer.addVertex(1.0D, scaledHeight - 1, 1.0D); + tessellator.draw(); + + glAlphaFunc(GL_GREATER, 0.1F); + glDisableLighting(); + glDisableFog(); + this.fbo.unbindFramebuffer(); + + this.fbo.framebufferRender(fboWidth, fboHeight); + + glEnableAlphaTest(); + glAlphaFunc(GL_GREATER, 0.1F); +// glFlush(); + + this.minecraft.updateDisplay(); // TODO OBF MCPTEST updateDisplay - func_175601_h + } + + private void renderLogTail(int yPos) + { + if (this.logIndex != LiteLoaderLogger.getLogIndex()) + { + this.logTail = LiteLoaderLogger.getLogTail(); + } + + for (int logIndex = this.logTail.size() - 1; yPos > 10 && logIndex >= 0; logIndex--) + { + this.fontRenderer.drawString(this.logTail.get(logIndex), 10, yPos -= 10, 0xFF000000); + } + } + + /** + * Find the most common (approx) colour in the image and assign it to the + * bar, reduces the palette to 9-bit by stripping the the 5 LSB from each + * byte to create a 9-bit palette index in the form RRRGGGBBB + * + * @param textureData + */ + private void findMostCommonColour(int[] textureData) + { + // Array of frequency values, indexed by palette index + int[] freq = new int[512]; + + for (int pos = 0; pos < textureData.length; pos++) + { + int paletteIndex = ((textureData[pos] >> 21 & 0x7) << 6) + ((textureData[pos] >> 13 & 0x7) << 3) + (textureData[pos] >> 5 & 0x7); + freq[paletteIndex]++; + } + + int peak = 0; + + // Black, white and 0x200000 excluded on purpose + for (int paletteIndex = 2; paletteIndex < 511; paletteIndex++) + { + if (freq[paletteIndex] > peak) + { + peak = freq[paletteIndex]; + this.setBarColour(paletteIndex); + } + } + } + + /** + * @param paletteIndex + */ + private void setBarColour(int paletteIndex) + { + this.r2 = this.padComponent((paletteIndex & 0x1C0) >> 1); + this.g2 = this.padComponent((paletteIndex & 0x38) << 2); + this.b2 = this.padComponent((paletteIndex & 0x7) << 5); + + this.barLuma = (Math.max(this.r2, Math.max(this.g2, this.b2)) < 64) ? 255 : 0; + } + + /** + * Pad LSB with 1's if any MSB are 1 (effectively a bitwise ceil() function) + * + * @param component + */ + private int padComponent(int component) + { + return (component > 0x1F) ? component | 0x1F : component; + } + + private DynamicTexture loadTexture(IResourceManager resourceManager, ResourceLocation textureLocation) throws IOException + { + InputStream inputStream = null; + + try + { + IResource resource = resourceManager.getResource(textureLocation); + inputStream = resource.getInputStream(); + BufferedImage image = ImageIO.read(inputStream); + return new DynamicTexture(image); + } + finally + { + if (inputStream != null) + { + inputStream.close(); + } + } + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinEntityPlayerSP.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinEntityPlayerSP.java new file mode 100644 index 00000000..5522cf0c --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinEntityPlayerSP.java @@ -0,0 +1,26 @@ +package com.mumfrey.liteloader.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mumfrey.liteloader.client.ClientProxy; + +import net.minecraft.client.entity.AbstractClientPlayer; +import net.minecraft.client.entity.EntityPlayerSP; + +@Mixin(EntityPlayerSP.class) +public abstract class MixinEntityPlayerSP extends AbstractClientPlayer +{ + public MixinEntityPlayerSP() + { + super(null, null); + } + + @Inject(method = "sendChatMessage(Ljava/lang/String;)V", at = { @At("HEAD") }, cancellable = true) + public void onSendChatMessage(String message, CallbackInfo ci) + { + ClientProxy.onOutboundChat(ci, message); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinEntityRenderer.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinEntityRenderer.java new file mode 100644 index 00000000..94fea707 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinEntityRenderer.java @@ -0,0 +1,115 @@ +package com.mumfrey.liteloader.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.At.Shift; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mumfrey.liteloader.client.ClientProxy; + +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.client.renderer.RenderGlobal; + +@Mixin(EntityRenderer.class) +public abstract class MixinEntityRenderer +{ + @Inject(method = "updateCameraAndRender(F)V", at = @At( + value = "INVOKE", + shift = Shift.AFTER, + target = "Lnet/minecraft/client/renderer/GlStateManager;clear(I)V" + )) + private void onPreRenderGUI(float partialTicks, CallbackInfo ci) + { + ClientProxy.preRenderGUI(partialTicks); + } + + @Inject(method = "updateCameraAndRender(F)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/GuiIngame;renderGameOverlay(F)V" + )) + private void onRenderHUD(float partialTicks, CallbackInfo ci) + { + ClientProxy.onRenderHUD(partialTicks); + } + + @Inject(method = "updateCameraAndRender(F)V", at = @At( + value = "INVOKE", + shift = Shift.AFTER, + target = "Lnet/minecraft/client/gui/GuiIngame;renderGameOverlay(F)V" + )) + private void onPostRenderHUD(float partialTicks, CallbackInfo ci) + { + ClientProxy.postRenderHUD(partialTicks); + } + + @Inject(method = "renderWorld(FJ)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/profiler/Profiler;startSection(Ljava/lang/String;)V", + ordinal = 0 + )) + private void onRenderWorld(float partialTicks, long timeSlice, CallbackInfo ci) + { + ClientProxy.onRenderWorld(partialTicks, timeSlice); + } + + @Inject(method = "renderWorld(FJ)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/profiler/Profiler;endSection()V", + ordinal = 0 + )) + private void onPostRender(float partialTicks, long timeSlice, CallbackInfo ci) + { + ClientProxy.postRender(partialTicks, timeSlice); + } + + @Inject(method = "renderWorldPass(IFJ)V", at = @At( + value = "INVOKE_STRING", + target = "Lnet/minecraft/profiler/Profiler;endStartSection(Ljava/lang/String;)V", + args = "ldc=frustum" + )) + private void onSetupCameraTransform(int pass, float partialTicks, long timeSlice, CallbackInfo ci) + { + ClientProxy.onSetupCameraTransform(pass, partialTicks, timeSlice); + } + + @Inject(method = "renderWorldPass(IFJ)V", at = @At( + value = "INVOKE_STRING", + target = "Lnet/minecraft/profiler/Profiler;endStartSection(Ljava/lang/String;)V", + args = "ldc=sky" + )) + private void onRenderSky(int pass, float partialTicks, long timeSlice, CallbackInfo ci) + { + ClientProxy.onRenderSky(pass, partialTicks, timeSlice); + } + + @Inject(method = "renderWorldPass(IFJ)V", at = @At( + value = "INVOKE_STRING", + target = "Lnet/minecraft/profiler/Profiler;endStartSection(Ljava/lang/String;)V", + args = "ldc=terrain" + )) + private void onRenderTerrain(int pass, float partialTicks, long timeSlice, CallbackInfo ci) + { + ClientProxy.onRenderTerrain(pass, partialTicks, timeSlice); + + } + + @Inject(method = "renderWorldPass(IFJ)V", at = @At( + value = "INVOKE_STRING", + target = "Lnet/minecraft/profiler/Profiler;endStartSection(Ljava/lang/String;)V", + args = "ldc=litParticles" + )) + private void onPostRenderEntities(int pass, float partialTicks, long timeSlice, CallbackInfo ci) + { + ClientProxy.postRenderEntities(pass, partialTicks, timeSlice); + } + + @Inject(method = "renderCloudsCheck(Lnet/minecraft/client/renderer/RenderGlobal;FI)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/profiler/Profiler;endStartSection(Ljava/lang/String;)V" + )) + private void onRenderClouds(RenderGlobal renderGlobalIn, float partialTicks, int pass, CallbackInfo ci) + { + ClientProxy.onRenderClouds(renderGlobalIn, partialTicks, pass); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinFramebuffer.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinFramebuffer.java new file mode 100644 index 00000000..96fd89b6 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinFramebuffer.java @@ -0,0 +1,43 @@ +package com.mumfrey.liteloader.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mumfrey.liteloader.client.ClientProxy; +import com.mumfrey.liteloader.client.ducks.IFramebuffer; + +import net.minecraft.client.shader.Framebuffer; + +@Mixin(Framebuffer.class) +public abstract class MixinFramebuffer implements IFramebuffer +{ + private boolean dispatchRenderEvent; + + @Override + public IFramebuffer setDispatchRenderEvent(boolean dispatchRenderEvent) + { + this.dispatchRenderEvent = dispatchRenderEvent; + return this; + } + + @Override + public boolean isDispatchRenderEvent() + { + return this.dispatchRenderEvent; + } + + @Inject(method = "framebufferRenderExt(IIZ)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/shader/Framebuffer;bindFramebufferTexture()V" + )) + private void onRenderFBO(int width, int height, boolean flag, CallbackInfo ci) + { + if (this.dispatchRenderEvent) + { + ClientProxy.renderFBO((Framebuffer)(Object)this, width, height, flag); + this.dispatchRenderEvent = false; + } + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinGuiIngame.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinGuiIngame.java new file mode 100644 index 00000000..0f2cedb4 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinGuiIngame.java @@ -0,0 +1,39 @@ +package com.mumfrey.liteloader.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.At.Shift; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mumfrey.liteloader.client.ClientProxy; + +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiIngame; +import net.minecraft.client.gui.GuiNewChat; + +@Mixin(GuiIngame.class) +public abstract class MixinGuiIngame extends Gui +{ + @Shadow private GuiNewChat persistantChatGUI; + + @Inject(method = "renderGameOverlay(F)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/GuiNewChat;drawChat(I)V" + )) + private void onRenderChat(float partialTicks, CallbackInfo ci) + { + ClientProxy.onRenderChat(this.persistantChatGUI, partialTicks); + } + + @Inject(method = "renderGameOverlay(F)V", at = @At( + value = "INVOKE", + shift = Shift.AFTER, + target = "Lnet/minecraft/client/gui/GuiNewChat;drawChat(I)V" + )) + private void postRenderChat(float partialTicks, CallbackInfo ci) + { + ClientProxy.postRenderChat(this.persistantChatGUI, partialTicks); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinIntegratedServer.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinIntegratedServer.java new file mode 100644 index 00000000..486719fb --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinIntegratedServer.java @@ -0,0 +1,39 @@ +package com.mumfrey.liteloader.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Surrogate; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mumfrey.liteloader.client.ClientProxy; + +import net.minecraft.client.Minecraft; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.integrated.IntegratedServer; +import net.minecraft.world.WorldSettings; + +@Mixin(IntegratedServer.class) +public abstract class MixinIntegratedServer extends MinecraftServer +{ + public MixinIntegratedServer() + { + super(null, null); + } + + @Inject( + method = "*", //(Lnet/minecraft/client/Minecraft;Ljava/lang/String;Ljava/lang/String;Lnet/minecraft/world/WorldSettings;)V", + at = @At("RETURN"), + remap = false + ) + private void onConstructed(Minecraft mcIn, String folderName, String worldName, WorldSettings settings, CallbackInfo ci) + { + ClientProxy.onCreateIntegratedServer((IntegratedServer)(Object)this, folderName, worldName, settings); + } + + @Surrogate + private void onConstructed(Minecraft mcIn, CallbackInfo ci) + { +// ClientProxy.onCreateIntegratedServer((IntegratedServer)(Object)this, folderName, worldName, settings); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinMinecraft.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinMinecraft.java new file mode 100644 index 00000000..f7ab0d86 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinMinecraft.java @@ -0,0 +1,85 @@ +package com.mumfrey.liteloader.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.At.Shift; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mumfrey.liteloader.client.ClientProxy; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.shader.Framebuffer; + +@Mixin(Minecraft.class) +public abstract class MixinMinecraft +{ + @Inject(method = "startGame()V", at = @At("RETURN")) + private void onStartupComplete(CallbackInfo ci) + { + ClientProxy.onStartupComplete(); + } + + @Inject(method = "updateFramebufferSize()V", at = @At("HEAD")) + private void onResize(CallbackInfo ci) + { + ClientProxy.onResize((Minecraft)(Object)this); + } + + @Inject(method = "runTick()V", at = @At("HEAD")) + private void newTick(CallbackInfo ci) + { + ClientProxy.newTick(); + } + + @Inject(method = "runGameLoop()V", at = @At( + value = "INVOKE", + shift = Shift.AFTER, + target = "Lnet/minecraft/client/renderer/EntityRenderer;updateCameraAndRender(F)V" + )) + private void onTick(CallbackInfo ci) + { + ClientProxy.onTick(); + } + + @Redirect(method = "runGameLoop()V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/shader/Framebuffer;framebufferRender(II)V" + )) + private void renderFBO(Framebuffer framebufferMc, int width, int height) + { + boolean fboEnabled = OpenGlHelper.isFramebufferEnabled(); + if (fboEnabled) + { + ClientProxy.preRenderFBO(framebufferMc); + framebufferMc.framebufferRender(width, height); + ClientProxy.preRenderFBO(framebufferMc); + } + else + { + framebufferMc.framebufferRender(width, height); + } + } + + @Inject(method = "runGameLoop()V", at = @At( + value = "INVOKE_STRING", + target = "Lnet/minecraft/profiler/Profiler;startSection(Ljava/lang/String;)V", + args = "ldc=tick" + )) + private void onTimerUpdate(CallbackInfo ci) + { + ClientProxy.onTimerUpdate(); + } + + @Inject (method = "runGameLoop()V", at = @At( + value = "INVOKE_STRING", + target = "Lnet/minecraft/profiler/Profiler;endStartSection(Ljava/lang/String;)V", + args = "ldc=gameRenderer" + )) + private void onRender(CallbackInfo ci) + { + ClientProxy.onRender(); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinNetHandlerLoginClient.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinNetHandlerLoginClient.java new file mode 100644 index 00000000..4de0048f --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinNetHandlerLoginClient.java @@ -0,0 +1,21 @@ +package com.mumfrey.liteloader.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.mumfrey.liteloader.client.ducks.IClientNetLoginHandler; + +import net.minecraft.client.network.NetHandlerLoginClient; +import net.minecraft.network.NetworkManager; + +@Mixin(NetHandlerLoginClient.class) +public abstract class MixinNetHandlerLoginClient implements IClientNetLoginHandler +{ + @Shadow private NetworkManager networkManager; + + @Override + public NetworkManager getNetMgr() + { + return this.networkManager; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinObjectIntIdentityMap.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinObjectIntIdentityMap.java new file mode 100644 index 00000000..53c20bc4 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinObjectIntIdentityMap.java @@ -0,0 +1,32 @@ +package com.mumfrey.liteloader.client.mixin; + +import java.util.IdentityHashMap; +import java.util.List; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.mumfrey.liteloader.client.ducks.IObjectIntIdentityMap; + +import net.minecraft.util.ObjectIntIdentityMap; + +@Mixin(ObjectIntIdentityMap.class) +public abstract class MixinObjectIntIdentityMap implements IObjectIntIdentityMap +{ + @Shadow private IdentityHashMap identityMap; + @Shadow private List objectList; + + @SuppressWarnings("unchecked") + @Override + public IdentityHashMap getIdentityMap() + { + return (IdentityHashMap)this.identityMap; + } + + @SuppressWarnings("unchecked") + @Override + public List getObjectList() + { + return (List)this.objectList; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRealmsMainScreen.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRealmsMainScreen.java new file mode 100644 index 00000000..8c8ce8e4 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRealmsMainScreen.java @@ -0,0 +1,26 @@ +package com.mumfrey.liteloader.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import com.mojang.realmsclient.RealmsMainScreen; +import com.mojang.realmsclient.dto.RealmsServer; +import com.mumfrey.liteloader.client.PacketEventsClient; + +import net.minecraft.realms.RealmsScreen; + +@Mixin(value = RealmsMainScreen.class, remap = false) +public abstract class MixinRealmsMainScreen extends RealmsScreen +{ + @Inject(method = "play(J)V", locals = LocalCapture.CAPTURE_FAILSOFT, at = @At( + value = "INVOKE", + target = "Lcom/mojang/realmsclient/RealmsMainScreen;stopRealmsFetcherAndPinger()V" + )) + private void onJoinRealm(long serverId, CallbackInfo ci, RealmsServer server) + { + PacketEventsClient.onJoinRealm(serverId, server); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRegistryNamespaced.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRegistryNamespaced.java new file mode 100644 index 00000000..71f41b53 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRegistryNamespaced.java @@ -0,0 +1,23 @@ +package com.mumfrey.liteloader.client.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.mumfrey.liteloader.client.ducks.INamespacedRegistry; +import com.mumfrey.liteloader.client.ducks.IObjectIntIdentityMap; + +import net.minecraft.util.ObjectIntIdentityMap; +import net.minecraft.util.RegistryNamespaced; +import net.minecraft.util.RegistrySimple; + +@Mixin(RegistryNamespaced.class) +public abstract class MixinRegistryNamespaced extends RegistrySimple implements INamespacedRegistry +{ + @Shadow protected ObjectIntIdentityMap underlyingIntegerMap; + + @Override + public IObjectIntIdentityMap getUnderlyingMap() + { + return (IObjectIntIdentityMap)this.underlyingIntegerMap; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRegistrySimple.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRegistrySimple.java new file mode 100644 index 00000000..313d909d --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRegistrySimple.java @@ -0,0 +1,23 @@ +package com.mumfrey.liteloader.client.mixin; + +import java.util.Map; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.mumfrey.liteloader.client.ducks.IRegistrySimple; + +import net.minecraft.util.RegistrySimple; + +@Mixin(RegistrySimple.class) +public abstract class MixinRegistrySimple implements IRegistrySimple +{ + @Shadow protected Map registryObjects; + + @SuppressWarnings("unchecked") + @Override + public Map getRegistryObjects() + { + return (Map)this.registryObjects; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRenderManager.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRenderManager.java new file mode 100644 index 00000000..6a1fe6de --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinRenderManager.java @@ -0,0 +1,39 @@ +package com.mumfrey.liteloader.client.mixin; + +import java.util.Map; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import com.mumfrey.liteloader.client.ClientProxy; +import com.mumfrey.liteloader.client.ducks.IRenderManager; + +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.Entity; + +@Mixin(RenderManager.class) +public abstract class MixinRenderManager implements IRenderManager +{ + @Shadow private Map, Render> entityRenderMap; + + @Override + public Map, Render> getRenderMap() + { + return this.entityRenderMap; + } + + @Redirect(method = "doRenderEntity(Lnet/minecraft/entity/Entity;DDDFFZ)Z", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/entity/Render;doRender(Lnet/minecraft/entity/Entity;DDDFF)V" + )) + private void onRenderEntity(Render render, Entity entity, double x, double y, double z, float entityYaw, float partialTicks) + { + RenderManager source = (RenderManager)(Object)this; + ClientProxy.onRenderEntity(source, render, entity, x, y, z, entityYaw, partialTicks); + render.doRender(entity, x, y, z, entityYaw, partialTicks); + ClientProxy.onPostRenderEntity(source, render, entity, x, y, z, entityYaw, partialTicks); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinScreenShotHelper.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinScreenShotHelper.java new file mode 100644 index 00000000..e809a497 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinScreenShotHelper.java @@ -0,0 +1,32 @@ +package com.mumfrey.liteloader.client.mixin; + +import java.io.File; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.mumfrey.liteloader.client.ClientProxy; + +import net.minecraft.client.shader.Framebuffer; +import net.minecraft.util.IChatComponent; +import net.minecraft.util.ScreenShotHelper; + +@Mixin(ScreenShotHelper.class) +public abstract class MixinScreenShotHelper +{ + @Inject( + method = "saveScreenshot(Ljava/io/File;Ljava/lang/String;IILnet/minecraft/client/shader/Framebuffer;)Lnet/minecraft/util/IChatComponent;", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/OpenGlHelper;isFramebufferEnabled()Z", + ordinal = 0 + ), + cancellable = true + ) + private static void onSaveScreenshot(File gameDir, String name, int width, int height, Framebuffer fbo, CallbackInfoReturnable ci) + { + ClientProxy.onSaveScreenshot(ci, gameDir, name, width, height, fbo); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinSession.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinSession.java new file mode 100644 index 00000000..78615edf --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinSession.java @@ -0,0 +1,31 @@ +package com.mumfrey.liteloader.client.mixin; + +import java.util.UUID; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.mojang.authlib.GameProfile; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.Session; + +@Mixin(Session.class) +public abstract class MixinSession +{ + @Shadow public abstract String getUsername(); + + @Inject(method = "getProfile()Lcom/mojang/authlib/GameProfile;", cancellable = true, at = @At( + value = "NEW", + args = "class=com/mojang/authlib/GameProfile", + ordinal = 1 + )) + private void generateGameProfile(CallbackInfoReturnable ci) + { + UUID uuid = EntityPlayer.getUUID(new GameProfile((UUID)null, this.getUsername())); + ci.setReturnValue(new GameProfile(uuid, this.getUsername())); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinSimpleReloadableResourceManager.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinSimpleReloadableResourceManager.java new file mode 100644 index 00000000..686c7884 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinSimpleReloadableResourceManager.java @@ -0,0 +1,23 @@ +package com.mumfrey.liteloader.client.mixin; + +import java.util.List; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.mumfrey.liteloader.client.ducks.IReloadable; + +import net.minecraft.client.resources.IResourceManagerReloadListener; +import net.minecraft.client.resources.SimpleReloadableResourceManager; + +@Mixin(SimpleReloadableResourceManager.class) +public abstract class MixinSimpleReloadableResourceManager implements IReloadable +{ + @Shadow private List reloadListeners; + + @Override + public List getReloadListeners() + { + return this.reloadListeners; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinTileEntityRendererDispatcher.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinTileEntityRendererDispatcher.java new file mode 100644 index 00000000..4c56eeca --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/mixin/MixinTileEntityRendererDispatcher.java @@ -0,0 +1,24 @@ +package com.mumfrey.liteloader.client.mixin; + +import java.util.Map; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.mumfrey.liteloader.client.ducks.ITileEntityRendererDispatcher; + +import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer; +import net.minecraft.tileentity.TileEntity; + +@Mixin(TileEntityRendererDispatcher.class) +public abstract class MixinTileEntityRendererDispatcher implements ITileEntityRendererDispatcher +{ + @Shadow private Map, TileEntitySpecialRenderer> mapSpecialRenderers; + + @Override + public Map, TileEntitySpecialRenderer> getSpecialRenderMap() + { + return this.mapSpecialRenderers; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IEntityRenderer.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IEntityRenderer.java new file mode 100644 index 00000000..1729548f --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IEntityRenderer.java @@ -0,0 +1,29 @@ +package com.mumfrey.liteloader.client.overlays; + +import net.minecraft.util.ResourceLocation; + +import com.mumfrey.liteloader.transformers.access.Accessor; +import com.mumfrey.liteloader.transformers.access.Invoker; + +/** + * Adapter for EntityRenderer to expose some private functionality + * + * @author Adam Mummery-Smith + */ +@Accessor("EntityRenderer") +public interface IEntityRenderer +{ + @Accessor("useShader") public abstract boolean getUseShader(); + @Accessor("useShader") public abstract void setUseShader(boolean useShader); + + @Accessor("shaderResourceLocations") public abstract ResourceLocation[] getShaders(); + + @Accessor("shaderIndex") public abstract int getShaderIndex(); + @Accessor("shaderIndex") public abstract void setShaderIndex(int shaderIndex); + + @Invoker("loadShader") public abstract void selectShader(ResourceLocation shader); + + @Invoker("getFOVModifier") public abstract float getFOV(float partialTicks, boolean armFOV); + + @Invoker("setupCameraTransform") public abstract void setupCamera(float partialTicks, int pass); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IGuiTextField.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IGuiTextField.java new file mode 100644 index 00000000..411792e4 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IGuiTextField.java @@ -0,0 +1,36 @@ +package com.mumfrey.liteloader.client.overlays; + +import com.mumfrey.liteloader.transformers.access.Accessor; + +/** + * Adapter for GuiTextField to expose internal properties, mainly to allow + * sensible subclassing. + * + * @author Adam Mummery-Smith + */ +@Accessor("GuiTextField") +public interface IGuiTextField +{ + @Accessor("#2") public abstract int getXPosition(); + @Accessor("#2") public abstract void setXPosition(int xPosition); + + @Accessor("#3") public abstract int getYPosition(); + @Accessor("#3") public abstract void setYPosition(int yPosition); + + @Accessor("#4") public abstract int getInternalWidth(); + @Accessor("#4") public abstract void setInternalWidth(int width); + + @Accessor("#5") public abstract int getHeight(); + @Accessor("#5") public abstract void setHeight(int height); + + @Accessor("#12") public abstract boolean isEnabled(); +// @Accessor("#12") public abstract void setEnabled(boolean enabled); // built in + + @Accessor("#13") public abstract int getLineScrollOffset(); + + @Accessor("#16") public abstract int getTextColor(); +// @Accessor("#16") public abstract void setTextColor(int color); // built in + + @Accessor("#17") public abstract int getDisabledTextColour(); +// @Accessor("#17") public abstract void setDisabledTextColour(int color); // built in +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IMinecraft.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IMinecraft.java new file mode 100644 index 00000000..6ce18247 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/IMinecraft.java @@ -0,0 +1,60 @@ +package com.mumfrey.liteloader.client.overlays; + +import java.util.List; + +import net.minecraft.client.resources.IResourcePack; +import net.minecraft.util.Timer; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.access.Accessor; +import com.mumfrey.liteloader.transformers.access.Invoker; +import com.mumfrey.liteloader.transformers.access.ObfTableClass; + +/** + * Interface containing injected accessors for Minecraft + * + * @author Adam Mummery-Smith + */ +@ObfTableClass(Obf.class) +@Accessor("Minecraft") +public interface IMinecraft +{ + /** + * Get the timer instance + */ + @Accessor("timer") + public abstract Timer getTimer(); + + /** + * Get the "running" flag + */ + @Accessor("running") + public abstract boolean isRunning(); + + /** + * Get the default resource packs set + */ + @Accessor("defaultResourcePacks") + public abstract List getDefaultResourcePacks(); + + /** + * Get the current server address (from connection) + */ + @Accessor("serverName") + public abstract String getServerName(); + + /** + * Get the current server port (from connection) + */ + @Accessor("serverPort") + public abstract int getServerPort(); + + /** + * Notify the client that the window was resized + * + * @param width + * @param height + */ + @Invoker("resize") + public abstract void onResizeWindow(int width, int height); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/ISoundHandler.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/ISoundHandler.java new file mode 100644 index 00000000..930f6d13 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/overlays/ISoundHandler.java @@ -0,0 +1,14 @@ +package com.mumfrey.liteloader.client.overlays; + +import net.minecraft.client.audio.SoundList; +import net.minecraft.util.ResourceLocation; + +import com.mumfrey.liteloader.transformers.access.Accessor; +import com.mumfrey.liteloader.transformers.access.Invoker; + +@Accessor("SoundHandler") +public interface ISoundHandler +{ + @Invoker("loadSoundResource") + public abstract void addSound(ResourceLocation sound, SoundList soundList); +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/transformers/CrashReportTransformer.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/transformers/CrashReportTransformer.java new file mode 100644 index 00000000..f82d795b --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/transformers/CrashReportTransformer.java @@ -0,0 +1,75 @@ +package com.mumfrey.liteloader.client.transformers; + +import java.util.ListIterator; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.ClassTransformer; + +public class CrashReportTransformer extends ClassTransformer +{ + @Override + public byte[] transform(String name, String transformedName, byte[] basicClass) + { + if (basicClass != null && (Obf.CrashReport$6.name.equals(name) || Obf.CrashReport$6.obf.equals(name))) + { + try + { + return this.transformCallableJVMFlags(basicClass); + } + catch (Exception ex) {} + } + + return basicClass; + } + + /** + * Inject the additional callback for populating the crash report into the + * CallableJVMFlags class. + * + * @param basicClass basic class + * @return transformed class + */ + private byte[] transformCallableJVMFlags(byte[] basicClass) + { + ClassNode classNode = this.readClass(basicClass, true); + + for (MethodNode method : classNode.methods) + { + if ("".equals(method.name)) + { + this.transformCallableJVMFlagsConstructor(method); + } + } + + return this.writeClass(classNode); + } + + /** + * @param ctor + */ + public void transformCallableJVMFlagsConstructor(MethodNode ctor) + { + InsnList code = new InsnList(); + code.add(new VarInsnNode(Opcodes.ALOAD, 1)); + code.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "com/mumfrey/liteloader/core/LiteLoader", "populateCrashReport", + "(Ljava/lang/Object;)V", false)); + + ListIterator insns = ctor.instructions.iterator(); + while (insns.hasNext()) + { + AbstractInsnNode insnNode = insns.next(); + if (insnNode.getOpcode() == Opcodes.RETURN) + { + ctor.instructions.insertBefore(insnNode, code); + } + } + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/transformers/MinecraftTransformer.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/transformers/MinecraftTransformer.java new file mode 100644 index 00000000..643ee18c --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/transformers/MinecraftTransformer.java @@ -0,0 +1,91 @@ +package com.mumfrey.liteloader.client.transformers; + +import java.util.Iterator; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.launch.LiteLoaderTweaker; +import com.mumfrey.liteloader.transformers.access.AccessorTransformer; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +public class MinecraftTransformer extends AccessorTransformer +{ + private static final String TWEAKCLASS = LiteLoaderTweaker.class.getName().replace('.', '/'); + + @Override + protected void addAccessors() + { + this.addAccessor(Obf.IMinecraft.name); + this.addAccessor(Obf.IGuiTextField.name); + this.addAccessor(Obf.IEntityRenderer.name); + this.addAccessor(Obf.ISoundHandler.name); + } + + @Override + protected void postTransform(String name, String transformedName, ClassNode classNode) + { + if ((Obf.Minecraft.name.equals(transformedName) || Obf.Minecraft.obf.equals(transformedName))) + { + for (MethodNode method : classNode.methods) + { + if (Obf.startGame.obf.equals(method.name) || Obf.startGame.srg.equals(method.name) || Obf.startGame.name.equals(method.name)) + { + this.transformStartGame(method); + } + } + } + } + + private void transformStartGame(MethodNode method) + { + InsnList insns = new InsnList(); + + boolean found = false; + + Iterator iter = method.instructions.iterator(); + while (iter.hasNext()) + { + AbstractInsnNode insn = iter.next(); + insns.add(insn); + + if (insn instanceof TypeInsnNode && insn.getOpcode() == Opcodes.NEW && insns.getLast() != null) + { + TypeInsnNode typeNode = (TypeInsnNode)insn; + if (!found && (Obf.EntityRenderer.obf.equals(typeNode.desc) || Obf.EntityRenderer.ref.equals(typeNode.desc))) + { + LiteLoaderLogger.info("MinecraftTransformer found INIT injection point, this is good."); + found = true; + + insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, MinecraftTransformer.TWEAKCLASS, Obf.init.name, "()V", false)); + insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, MinecraftTransformer.TWEAKCLASS, Obf.postInit.name, "()V", false)); + } + } + + if (LiteLoaderTweaker.loadingBarEnabled()) + { + if (insn instanceof LdcInsnNode) + { + LdcInsnNode ldcInsn = (LdcInsnNode)insn; + if ("textures/blocks".equals(ldcInsn.cst)) + { + insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Obf.LoadingBar.ref, "initTextures", "()V", false)); + } + } + + insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Obf.LoadingBar.ref, "incrementProgress", "()V", false)); + } + } + + method.instructions = insns; + + if (!found) LiteLoaderLogger.severe("MinecraftTransformer failed to find INIT injection point, the game will probably crash pretty soon."); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/util/PrivateFieldsClient.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/util/PrivateFieldsClient.java new file mode 100644 index 00000000..d8529eee --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/util/PrivateFieldsClient.java @@ -0,0 +1,22 @@ +package com.mumfrey.liteloader.client.util; + +import java.util.Map; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.util.PrivateFields; + +import net.minecraft.tileentity.TileEntity; + +@SuppressWarnings("rawtypes") +public final class PrivateFieldsClient extends PrivateFields +{ + private PrivateFieldsClient(Class

      owner, Obf obf) + { + super(owner, obf); + } + + // CHECKSTYLE:OFF + + public static final PrivateFieldsClient tileEntityNameToClassMap = new PrivateFieldsClient(TileEntity.class, Obf.tileEntityNameToClassMap); + public static final PrivateFieldsClient tileEntityClassToNameMap = new PrivateFieldsClient(TileEntity.class, Obf.tileEntityClassToNameMap); +} \ No newline at end of file diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconAbsolute.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconAbsolute.java new file mode 100644 index 00000000..1f01788c --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconAbsolute.java @@ -0,0 +1,128 @@ +package com.mumfrey.liteloader.client.util.render; + +import com.mumfrey.liteloader.util.render.IconTextured; + +import net.minecraft.util.ResourceLocation; + +public class IconAbsolute implements IconTextured +{ + private ResourceLocation textureResource; + + private String displayText; + + private int texMapSize = 256; + + private int width; + private int height; + + private int uPos, vPos; + + private float uCoord; + private float uCoord2; + private float vCoord; + private float vCoord2; + + public IconAbsolute(ResourceLocation textureResource, String displayText, int width, int height, float uCoord, float vCoord, float uCoord2, + float vCoord2) + { + this(textureResource, displayText, width, height, uCoord, vCoord, uCoord2, vCoord2, 256); + } + + public IconAbsolute(ResourceLocation textureResource, String displayText, int width, int height, float uCoord, float vCoord, float uCoord2, + float vCoord2, int texMapSize) + { + this.textureResource = textureResource; + this.displayText = displayText; + this.width = width; + this.height = height; + + this.uPos = (int)uCoord; + this.vPos = (int)vCoord; + + this.texMapSize = texMapSize; + this.uCoord = uCoord / this.texMapSize; + this.uCoord2 = uCoord2 / this.texMapSize; + this.vCoord = vCoord / this.texMapSize; + this.vCoord2 = vCoord2 / this.texMapSize; + } + + @Override + public String getDisplayText() + { + return this.displayText; + } + + @Override + public ResourceLocation getTextureResource() + { + return this.textureResource; + } + + @Override + public int getIconWidth() + { + return this.width; + } + + @Override + public int getIconHeight() + { + return this.height; + } + + @Override + public int getUPos() + { + return this.uPos; + } + + @Override + public int getVPos() + { + return this.vPos; + } + + @Override + public float getMinU() + { + return this.uCoord; + } + + @Override + public float getMaxU() + { + return this.uCoord2 - Float.MIN_VALUE; + } + + @Override + public float getInterpolatedU(double slice) + { + float uSize = this.uCoord2 - this.uCoord; + return this.uCoord + uSize * ((float)slice / 16.0F) - Float.MIN_VALUE; + } + + @Override + public float getMinV() + { + return this.vCoord; + } + + @Override + public float getMaxV() + { + return this.vCoord2 - Float.MIN_VALUE; + } + + @Override + public float getInterpolatedV(double slice) + { + float vSize = this.vCoord2 - this.vCoord; + return this.vCoord + vSize * ((float)slice / 16.0F) - Float.MIN_VALUE; + } + + @Override + public String getIconName() + { + return this.displayText; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconAbsoluteClickable.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconAbsoluteClickable.java new file mode 100644 index 00000000..26fbb032 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconAbsoluteClickable.java @@ -0,0 +1,20 @@ +package com.mumfrey.liteloader.client.util.render; + +import net.minecraft.util.ResourceLocation; + +import com.mumfrey.liteloader.util.render.IconClickable; + +public abstract class IconAbsoluteClickable extends IconAbsolute implements IconClickable +{ + public IconAbsoluteClickable(ResourceLocation textureResource, String displayText, int width, int height, float uCoord, float vCoord, + float uCoord2, float vCoord2) + { + super(textureResource, displayText, width, height, uCoord, vCoord, uCoord2, vCoord2); + } + + public IconAbsoluteClickable(ResourceLocation textureResource, String displayText, int width, int height, float uCoord, float vCoord, + float uCoord2, float vCoord2, int texMapSize) + { + super(textureResource, displayText, width, height, uCoord, vCoord, uCoord2, vCoord2, texMapSize); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconTiled.java b/liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconTiled.java new file mode 100644 index 00000000..83cf2a13 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/client/util/render/IconTiled.java @@ -0,0 +1,140 @@ +package com.mumfrey.liteloader.client.util.render; + +import com.mumfrey.liteloader.util.render.Icon; + +import net.minecraft.util.ResourceLocation; + +public class IconTiled implements Icon +{ + private ResourceLocation textureResource; + + protected int iconID; + + protected int iconU; + protected int iconV; + private int width; + private int height; + private float uCoord; + private float uCoord2; + private float vCoord; + private float vCoord2; + + private int textureWidth, textureHeight; + + public IconTiled(ResourceLocation textureResource, int id) + { + this(textureResource, id, 16); + } + + public IconTiled(ResourceLocation textureResource, int id, int iconSize) + { + this(textureResource, id, iconSize, 0); + } + + public IconTiled(ResourceLocation textureResource, int id, int iconSize, int yOffset) + { + this(textureResource, id, iconSize, (id % (256 / iconSize)) * iconSize, (id / (256 / iconSize)) * iconSize + yOffset); + } + + public IconTiled(ResourceLocation textureResource, int id, int iconSize, int iconU, int iconV) + { + this(textureResource, id, iconU, iconV, iconSize, iconSize, 256, 256); + } + + public IconTiled(ResourceLocation textureResource, int id, int iconU, int iconV, int width, int height, int textureWidth, int textureHeight) + { + this.iconID = id; + this.textureResource = textureResource; + + this.textureWidth = textureWidth; + this.textureHeight = textureHeight; + + this.width = width; + this.height = height; + + this.init(iconU, iconV); + } + + protected void init(int iconU, int iconV) + { + this.iconU = iconU; + this.iconV = iconV; + + this.uCoord = (float)iconU / (float)this.textureWidth; + this.uCoord2 = (float)(iconU + this.width) / (float)this.textureWidth; + this.vCoord = (float)iconV / (float)this.textureHeight; + this.vCoord2 = (float)(iconV + this.height) / (float)this.textureHeight; + } + + public ResourceLocation getTextureResource() + { + return this.textureResource; + } + + public int getIconID() + { + return this.iconID; + } + + public void setIconID(int id) + { + this.iconID = id; + this.init((id % 16) * 16, (id / 16) * 16); + } + + @Override + public int getIconWidth() + { + return this.width; + } + + @Override + public int getIconHeight() + { + return this.height; + } + + @Override + public float getMinU() + { + return this.uCoord; + } + + @Override + public float getMaxU() + { + return this.uCoord2 - Float.MIN_VALUE; + } + + @Override + public float getInterpolatedU(double slice) + { + float uSize = this.uCoord2 - this.uCoord; + return this.uCoord + uSize * ((float)slice / 16.0F) - Float.MIN_VALUE; + } + + @Override + public float getMinV() + { + return this.vCoord; + } + + @Override + public float getMaxV() + { + return this.vCoord2 - Float.MIN_VALUE; + } + + @Override + public float getInterpolatedV(double slice) + { + float vSize = this.vCoord2 - this.vCoord; + return this.vCoord + vSize * ((float)slice / 16.0F) - Float.MIN_VALUE; + } + + @Override + public String getIconName() + { + return this.textureResource + "_" + this.iconID; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/gl/GL.java b/liteloader/src/client/java/com/mumfrey/liteloader/gl/GL.java new file mode 100644 index 00000000..33d83415 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/gl/GL.java @@ -0,0 +1,1296 @@ +package com.mumfrey.liteloader.gl; + +import java.nio.ByteBuffer; +import java.nio.DoubleBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.GlStateManager.TexGen; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.util.glu.GLU; + +/** + * Convenience class for working with Mojang's GLStateManager: + * + *

      It would be pretty tolerable to work with GLStateManager as a static + * import were it not for the fact that you still need to import the GL + * namespaces themselves from LWJGL in order to get the constants, and also have + * to deal with the fact that GLStateManager's methods don't have "gl-style" + * names, making it annoying to work with. This class is designed to function as + * an adapter to allow changeover to be more painless. Using this class means + * that the following code:

      + * + *
      glEnable(GL_BLEND);
      + * glAlphaFunc(GL_GREATER, 0.0F);
      + * + *

      becomes:

      + * + *
      glEnableBlend();
      + * glAlphaFunc(GL_GREATER, 0.0F);
      + * + *

      Notice that the glAlphaFunc invocation remains unchanged, and the + * glEnable call simply gets replaced with a logical equivalent which + * invokes the GLStateManager method behind the scenes.

      + * + *

      To use this class, simply replace existing static imports in your classes + * with this single static import, then change glEnable and + * glDisable calls accordingly. + * + * @author Adam Mummery-Smith + */ +public class GL +{ + // GL11 + public static final int GL_ACCUM = 0x100; + public static final int GL_LOAD = 0x101; + public static final int GL_RETURN = 0x102; + public static final int GL_MULT = 0x103; + public static final int GL_ADD = 0x104; + public static final int GL_NEVER = 0x200; + public static final int GL_LESS = 0x201; + public static final int GL_EQUAL = 0x202; + public static final int GL_LEQUAL = 0x203; + public static final int GL_GREATER = 0x204; + public static final int GL_NOTEQUAL = 0x205; + public static final int GL_GEQUAL = 0x206; + public static final int GL_ALWAYS = 0x207; + public static final int GL_CURRENT_BIT = 0x1; + public static final int GL_POINT_BIT = 0x2; + public static final int GL_LINE_BIT = 0x4; + public static final int GL_POLYGON_BIT = 0x8; + public static final int GL_POLYGON_STIPPLE_BIT = 0x10; + public static final int GL_PIXEL_MODE_BIT = 0x20; + public static final int GL_LIGHTING_BIT = 0x40; + public static final int GL_FOG_BIT = 0x80; + public static final int GL_DEPTH_BUFFER_BIT = 0x100; + public static final int GL_ACCUM_BUFFER_BIT = 0x200; + public static final int GL_STENCIL_BUFFER_BIT = 0x400; + public static final int GL_VIEWPORT_BIT = 0x800; + public static final int GL_TRANSFORM_BIT = 0x1000; + public static final int GL_ENABLE_BIT = 0x2000; + public static final int GL_COLOR_BUFFER_BIT = 0x4000; + public static final int GL_HINT_BIT = 0x8000; + public static final int GL_EVAL_BIT = 0x10000; + public static final int GL_LIST_BIT = 0x20000; + public static final int GL_TEXTURE_BIT = 0x40000; + public static final int GL_SCISSOR_BIT = 0x80000; + public static final int GL_ALL_ATTRIB_BITS = 0xfffff; + public static final int GL_POINTS = 0x0; + public static final int GL_LINES = 0x1; + public static final int GL_LINE_LOOP = 0x2; + public static final int GL_LINE_STRIP = 0x3; + public static final int GL_TRIANGLES = 0x4; + public static final int GL_TRIANGLE_STRIP = 0x5; + public static final int GL_TRIANGLE_FAN = 0x6; + public static final int GL_QUADS = 0x7; + public static final int GL_QUAD_STRIP = 0x8; + public static final int GL_POLYGON = 0x9; + public static final int GL_ZERO = 0x0; + public static final int GL_ONE = 0x1; + public static final int GL_SRC_COLOR = 0x300; + public static final int GL_ONE_MINUS_SRC_COLOR = 0x301; + public static final int GL_SRC_ALPHA = 0x302; + public static final int GL_ONE_MINUS_SRC_ALPHA = 0x303; + public static final int GL_DST_ALPHA = 0x304; + public static final int GL_ONE_MINUS_DST_ALPHA = 0x305; + public static final int GL_DST_COLOR = 0x306; + public static final int GL_ONE_MINUS_DST_COLOR = 0x307; + public static final int GL_SRC_ALPHA_SATURATE = 0x308; + public static final int GL_CONSTANT_COLOR = 0x8001; + public static final int GL_ONE_MINUS_CONSTANT_COLOR = 0x8002; + public static final int GL_CONSTANT_ALPHA = 0x8003; + public static final int GL_ONE_MINUS_CONSTANT_ALPHA = 0x8004; + public static final int GL_TRUE = 0x1; + public static final int GL_FALSE = 0x0; + public static final int GL_CLIP_PLANE0 = 0x3000; + public static final int GL_CLIP_PLANE1 = 0x3001; + public static final int GL_CLIP_PLANE2 = 0x3002; + public static final int GL_CLIP_PLANE3 = 0x3003; + public static final int GL_CLIP_PLANE4 = 0x3004; + public static final int GL_CLIP_PLANE5 = 0x3005; + public static final int GL_BYTE = 0x1400; + public static final int GL_UNSIGNED_BYTE = 0x1401; + public static final int GL_SHORT = 0x1402; + public static final int GL_UNSIGNED_SHORT = 0x1403; + public static final int GL_INT = 0x1404; + public static final int GL_UNSIGNED_INT = 0x1405; + public static final int GL_FLOAT = 0x1406; + public static final int GL_2_BYTES = 0x1407; + public static final int GL_3_BYTES = 0x1408; + public static final int GL_4_BYTES = 0x1409; + public static final int GL_DOUBLE = 0x140a; + public static final int GL_NONE = 0x0; + public static final int GL_FRONT_LEFT = 0x400; + public static final int GL_FRONT_RIGHT = 0x401; + public static final int GL_BACK_LEFT = 0x402; + public static final int GL_BACK_RIGHT = 0x403; + public static final int GL_FRONT = 0x404; + public static final int GL_BACK = 0x405; + public static final int GL_LEFT = 0x406; + public static final int GL_RIGHT = 0x407; + public static final int GL_FRONT_AND_BACK = 0x408; + public static final int GL_AUX0 = 0x409; + public static final int GL_AUX1 = 0x40a; + public static final int GL_AUX2 = 0x40b; + public static final int GL_AUX3 = 0x40c; + public static final int GL_NO_ERROR = 0x0; + public static final int GL_INVALID_ENUM = 0x500; + public static final int GL_INVALID_VALUE = 0x501; + public static final int GL_INVALID_OPERATION = 0x502; + public static final int GL_STACK_OVERFLOW = 0x503; + public static final int GL_STACK_UNDERFLOW = 0x504; + public static final int GL_OUT_OF_MEMORY = 0x505; + public static final int GL_2D = 0x600; + public static final int GL_3D = 0x601; + public static final int GL_3D_COLOR = 0x602; + public static final int GL_3D_COLOR_TEXTURE = 0x603; + public static final int GL_4D_COLOR_TEXTURE = 0x604; + public static final int GL_PASS_THROUGH_TOKEN = 0x700; + public static final int GL_POINT_TOKEN = 0x701; + public static final int GL_LINE_TOKEN = 0x702; + public static final int GL_POLYGON_TOKEN = 0x703; + public static final int GL_BITMAP_TOKEN = 0x704; + public static final int GL_DRAW_PIXEL_TOKEN = 0x705; + public static final int GL_COPY_PIXEL_TOKEN = 0x706; + public static final int GL_LINE_RESET_TOKEN = 0x707; + public static final int GL_EXP = 0x800; + public static final int GL_EXP2 = 0x801; + public static final int GL_CW = 0x900; + public static final int GL_CCW = 0x901; + public static final int GL_COEFF = 0xa00; + public static final int GL_ORDER = 0xa01; + public static final int GL_DOMAIN = 0xa02; + public static final int GL_CURRENT_COLOR = 0xb00; + public static final int GL_CURRENT_INDEX = 0xb01; + public static final int GL_CURRENT_NORMAL = 0xb02; + public static final int GL_CURRENT_TEXTURE_COORDS = 0xb03; + public static final int GL_CURRENT_RASTER_COLOR = 0xb04; + public static final int GL_CURRENT_RASTER_INDEX = 0xb05; + public static final int GL_CURRENT_RASTER_TEXTURE_COORDS = 0xb06; + public static final int GL_CURRENT_RASTER_POSITION = 0xb07; + public static final int GL_CURRENT_RASTER_POSITION_VALID = 0xb08; + public static final int GL_CURRENT_RASTER_DISTANCE = 0xb09; + public static final int GL_POINT_SMOOTH = 0xb10; + public static final int GL_POINT_SIZE = 0xb11; + public static final int GL_POINT_SIZE_RANGE = 0xb12; + public static final int GL_POINT_SIZE_GRANULARITY = 0xb13; + public static final int GL_LINE_SMOOTH = 0xb20; + public static final int GL_LINE_WIDTH = 0xb21; + public static final int GL_LINE_WIDTH_RANGE = 0xb22; + public static final int GL_LINE_WIDTH_GRANULARITY = 0xb23; + public static final int GL_LINE_STIPPLE = 0xb24; + public static final int GL_LINE_STIPPLE_PATTERN = 0xb25; + public static final int GL_LINE_STIPPLE_REPEAT = 0xb26; + public static final int GL_LIST_MODE = 0xb30; + public static final int GL_MAX_LIST_NESTING = 0xb31; + public static final int GL_LIST_BASE = 0xb32; + public static final int GL_LIST_INDEX = 0xb33; + public static final int GL_POLYGON_MODE = 0xb40; + public static final int GL_POLYGON_SMOOTH = 0xb41; + public static final int GL_POLYGON_STIPPLE = 0xb42; + public static final int GL_EDGE_FLAG = 0xb43; + public static final int GL_CULL_FACE = 0xb44; + public static final int GL_CULL_FACE_MODE = 0xb45; + public static final int GL_FRONT_FACE = 0xb46; + public static final int GL_LIGHTING = 0xb50; + public static final int GL_LIGHT_MODEL_LOCAL_VIEWER = 0xb51; + public static final int GL_LIGHT_MODEL_TWO_SIDE = 0xb52; + public static final int GL_LIGHT_MODEL_AMBIENT = 0xb53; + public static final int GL_SHADE_MODEL = 0xb54; + public static final int GL_COLOR_MATERIAL_FACE = 0xb55; + public static final int GL_COLOR_MATERIAL_PARAMETER = 0xb56; + public static final int GL_COLOR_MATERIAL = 0xb57; + public static final int GL_FOG = 0xb60; + public static final int GL_FOG_INDEX = 0xb61; + public static final int GL_FOG_DENSITY = 0xb62; + public static final int GL_FOG_START = 0xb63; + public static final int GL_FOG_END = 0xb64; + public static final int GL_FOG_MODE = 0xb65; + public static final int GL_FOG_COLOR = 0xb66; + public static final int GL_DEPTH_RANGE = 0xb70; + public static final int GL_DEPTH_TEST = 0xb71; + public static final int GL_DEPTH_WRITEMASK = 0xb72; + public static final int GL_DEPTH_CLEAR_VALUE = 0xb73; + public static final int GL_DEPTH_FUNC = 0xb74; + public static final int GL_ACCUM_CLEAR_VALUE = 0xb80; + public static final int GL_STENCIL_TEST = 0xb90; + public static final int GL_STENCIL_CLEAR_VALUE = 0xb91; + public static final int GL_STENCIL_FUNC = 0xb92; + public static final int GL_STENCIL_VALUE_MASK = 0xb93; + public static final int GL_STENCIL_FAIL = 0xb94; + public static final int GL_STENCIL_PASS_DEPTH_FAIL = 0xb95; + public static final int GL_STENCIL_PASS_DEPTH_PASS = 0xb96; + public static final int GL_STENCIL_REF = 0xb97; + public static final int GL_STENCIL_WRITEMASK = 0xb98; + public static final int GL_MATRIX_MODE = 0xba0; + public static final int GL_NORMALIZE = 0xba1; + public static final int GL_VIEWPORT = 0xba2; + public static final int GL_MODELVIEW_STACK_DEPTH = 0xba3; + public static final int GL_PROJECTION_STACK_DEPTH = 0xba4; + public static final int GL_TEXTURE_STACK_DEPTH = 0xba5; + public static final int GL_MODELVIEW_MATRIX = 0xba6; + public static final int GL_PROJECTION_MATRIX = 0xba7; + public static final int GL_TEXTURE_MATRIX = 0xba8; + public static final int GL_ATTRIB_STACK_DEPTH = 0xbb0; + public static final int GL_CLIENT_ATTRIB_STACK_DEPTH = 0xbb1; + public static final int GL_ALPHA_TEST = 0xbc0; + public static final int GL_ALPHA_TEST_FUNC = 0xbc1; + public static final int GL_ALPHA_TEST_REF = 0xbc2; + public static final int GL_DITHER = 0xbd0; + public static final int GL_BLEND_DST = 0xbe0; + public static final int GL_BLEND_SRC = 0xbe1; + public static final int GL_BLEND = 0xbe2; + public static final int GL_LOGIC_OP_MODE = 0xbf0; + public static final int GL_INDEX_LOGIC_OP = 0xbf1; + public static final int GL_COLOR_LOGIC_OP = 0xbf2; + public static final int GL_AUX_BUFFERS = 0xc00; + public static final int GL_DRAW_BUFFER = 0xc01; + public static final int GL_READ_BUFFER = 0xc02; + public static final int GL_SCISSOR_BOX = 0xc10; + public static final int GL_SCISSOR_TEST = 0xc11; + public static final int GL_INDEX_CLEAR_VALUE = 0xc20; + public static final int GL_INDEX_WRITEMASK = 0xc21; + public static final int GL_COLOR_CLEAR_VALUE = 0xc22; + public static final int GL_COLOR_WRITEMASK = 0xc23; + public static final int GL_INDEX_MODE = 0xc30; + public static final int GL_RGBA_MODE = 0xc31; + public static final int GL_DOUBLEBUFFER = 0xc32; + public static final int GL_STEREO = 0xc33; + public static final int GL_RENDER_MODE = 0xc40; + public static final int GL_PERSPECTIVE_CORRECTION_HINT = 0xc50; + public static final int GL_POINT_SMOOTH_HINT = 0xc51; + public static final int GL_LINE_SMOOTH_HINT = 0xc52; + public static final int GL_POLYGON_SMOOTH_HINT = 0xc53; + public static final int GL_FOG_HINT = 0xc54; + public static final int GL_TEXTURE_GEN_S = 0xc60; + public static final int GL_TEXTURE_GEN_T = 0xc61; + public static final int GL_TEXTURE_GEN_R = 0xc62; + public static final int GL_TEXTURE_GEN_Q = 0xc63; + public static final int GL_PIXEL_MAP_I_TO_I = 0xc70; + public static final int GL_PIXEL_MAP_S_TO_S = 0xc71; + public static final int GL_PIXEL_MAP_I_TO_R = 0xc72; + public static final int GL_PIXEL_MAP_I_TO_G = 0xc73; + public static final int GL_PIXEL_MAP_I_TO_B = 0xc74; + public static final int GL_PIXEL_MAP_I_TO_A = 0xc75; + public static final int GL_PIXEL_MAP_R_TO_R = 0xc76; + public static final int GL_PIXEL_MAP_G_TO_G = 0xc77; + public static final int GL_PIXEL_MAP_B_TO_B = 0xc78; + public static final int GL_PIXEL_MAP_A_TO_A = 0xc79; + public static final int GL_PIXEL_MAP_I_TO_I_SIZE = 0xcb0; + public static final int GL_PIXEL_MAP_S_TO_S_SIZE = 0xcb1; + public static final int GL_PIXEL_MAP_I_TO_R_SIZE = 0xcb2; + public static final int GL_PIXEL_MAP_I_TO_G_SIZE = 0xcb3; + public static final int GL_PIXEL_MAP_I_TO_B_SIZE = 0xcb4; + public static final int GL_PIXEL_MAP_I_TO_A_SIZE = 0xcb5; + public static final int GL_PIXEL_MAP_R_TO_R_SIZE = 0xcb6; + public static final int GL_PIXEL_MAP_G_TO_G_SIZE = 0xcb7; + public static final int GL_PIXEL_MAP_B_TO_B_SIZE = 0xcb8; + public static final int GL_PIXEL_MAP_A_TO_A_SIZE = 0xcb9; + public static final int GL_UNPACK_SWAP_BYTES = 0xcf0; + public static final int GL_UNPACK_LSB_FIRST = 0xcf1; + public static final int GL_UNPACK_ROW_LENGTH = 0xcf2; + public static final int GL_UNPACK_SKIP_ROWS = 0xcf3; + public static final int GL_UNPACK_SKIP_PIXELS = 0xcf4; + public static final int GL_UNPACK_ALIGNMENT = 0xcf5; + public static final int GL_PACK_SWAP_BYTES = 0xd00; + public static final int GL_PACK_LSB_FIRST = 0xd01; + public static final int GL_PACK_ROW_LENGTH = 0xd02; + public static final int GL_PACK_SKIP_ROWS = 0xd03; + public static final int GL_PACK_SKIP_PIXELS = 0xd04; + public static final int GL_PACK_ALIGNMENT = 0xd05; + public static final int GL_MAP_COLOR = 0xd10; + public static final int GL_MAP_STENCIL = 0xd11; + public static final int GL_INDEX_SHIFT = 0xd12; + public static final int GL_INDEX_OFFSET = 0xd13; + public static final int GL_RED_SCALE = 0xd14; + public static final int GL_RED_BIAS = 0xd15; + public static final int GL_ZOOM_X = 0xd16; + public static final int GL_ZOOM_Y = 0xd17; + public static final int GL_GREEN_SCALE = 0xd18; + public static final int GL_GREEN_BIAS = 0xd19; + public static final int GL_BLUE_SCALE = 0xd1a; + public static final int GL_BLUE_BIAS = 0xd1b; + public static final int GL_ALPHA_SCALE = 0xd1c; + public static final int GL_ALPHA_BIAS = 0xd1d; + public static final int GL_DEPTH_SCALE = 0xd1e; + public static final int GL_DEPTH_BIAS = 0xd1f; + public static final int GL_MAX_EVAL_ORDER = 0xd30; + public static final int GL_MAX_LIGHTS = 0xd31; + public static final int GL_MAX_CLIP_PLANES = 0xd32; + public static final int GL_MAX_TEXTURE_SIZE = 0xd33; + public static final int GL_MAX_PIXEL_MAP_TABLE = 0xd34; + public static final int GL_MAX_ATTRIB_STACK_DEPTH = 0xd35; + public static final int GL_MAX_MODELVIEW_STACK_DEPTH = 0xd36; + public static final int GL_MAX_NAME_STACK_DEPTH = 0xd37; + public static final int GL_MAX_PROJECTION_STACK_DEPTH = 0xd38; + public static final int GL_MAX_TEXTURE_STACK_DEPTH = 0xd39; + public static final int GL_MAX_VIEWPORT_DIMS = 0xd3a; + public static final int GL_MAX_CLIENT_ATTRIB_STACK_DEPTH = 0xd3b; + public static final int GL_SUBPIXEL_BITS = 0xd50; + public static final int GL_INDEX_BITS = 0xd51; + public static final int GL_RED_BITS = 0xd52; + public static final int GL_GREEN_BITS = 0xd53; + public static final int GL_BLUE_BITS = 0xd54; + public static final int GL_ALPHA_BITS = 0xd55; + public static final int GL_DEPTH_BITS = 0xd56; + public static final int GL_STENCIL_BITS = 0xd57; + public static final int GL_ACCUM_RED_BITS = 0xd58; + public static final int GL_ACCUM_GREEN_BITS = 0xd59; + public static final int GL_ACCUM_BLUE_BITS = 0xd5a; + public static final int GL_ACCUM_ALPHA_BITS = 0xd5b; + public static final int GL_NAME_STACK_DEPTH = 0xd70; + public static final int GL_AUTO_NORMAL = 0xd80; + public static final int GL_MAP1_COLOR_4 = 0xd90; + public static final int GL_MAP1_INDEX = 0xd91; + public static final int GL_MAP1_NORMAL = 0xd92; + public static final int GL_MAP1_TEXTURE_COORD_1 = 0xd93; + public static final int GL_MAP1_TEXTURE_COORD_2 = 0xd94; + public static final int GL_MAP1_TEXTURE_COORD_3 = 0xd95; + public static final int GL_MAP1_TEXTURE_COORD_4 = 0xd96; + public static final int GL_MAP1_VERTEX_3 = 0xd97; + public static final int GL_MAP1_VERTEX_4 = 0xd98; + public static final int GL_MAP2_COLOR_4 = 0xdb0; + public static final int GL_MAP2_INDEX = 0xdb1; + public static final int GL_MAP2_NORMAL = 0xdb2; + public static final int GL_MAP2_TEXTURE_COORD_1 = 0xdb3; + public static final int GL_MAP2_TEXTURE_COORD_2 = 0xdb4; + public static final int GL_MAP2_TEXTURE_COORD_3 = 0xdb5; + public static final int GL_MAP2_TEXTURE_COORD_4 = 0xdb6; + public static final int GL_MAP2_VERTEX_3 = 0xdb7; + public static final int GL_MAP2_VERTEX_4 = 0xdb8; + public static final int GL_MAP1_GRID_DOMAIN = 0xdd0; + public static final int GL_MAP1_GRID_SEGMENTS = 0xdd1; + public static final int GL_MAP2_GRID_DOMAIN = 0xdd2; + public static final int GL_MAP2_GRID_SEGMENTS = 0xdd3; + public static final int GL_TEXTURE_1D = 0xde0; + public static final int GL_TEXTURE_2D = 0xde1; + public static final int GL_FEEDBACK_BUFFER_POINTER = 0xdf0; + public static final int GL_FEEDBACK_BUFFER_SIZE = 0xdf1; + public static final int GL_FEEDBACK_BUFFER_TYPE = 0xdf2; + public static final int GL_SELECTION_BUFFER_POINTER = 0xdf3; + public static final int GL_SELECTION_BUFFER_SIZE = 0xdf4; + public static final int GL_TEXTURE_WIDTH = 0x1000; + public static final int GL_TEXTURE_HEIGHT = 0x1001; + public static final int GL_TEXTURE_INTERNAL_FORMAT = 0x1003; + public static final int GL_TEXTURE_BORDER_COLOR = 0x1004; + public static final int GL_TEXTURE_BORDER = 0x1005; + public static final int GL_DONT_CARE = 0x1100; + public static final int GL_FASTEST = 0x1101; + public static final int GL_NICEST = 0x1102; + public static final int GL_LIGHT0 = 0x4000; + public static final int GL_LIGHT1 = 0x4001; + public static final int GL_LIGHT2 = 0x4002; + public static final int GL_LIGHT3 = 0x4003; + public static final int GL_LIGHT4 = 0x4004; + public static final int GL_LIGHT5 = 0x4005; + public static final int GL_LIGHT6 = 0x4006; + public static final int GL_LIGHT7 = 0x4007; + public static final int GL_AMBIENT = 0x1200; + public static final int GL_DIFFUSE = 0x1201; + public static final int GL_SPECULAR = 0x1202; + public static final int GL_POSITION = 0x1203; + public static final int GL_SPOT_DIRECTION = 0x1204; + public static final int GL_SPOT_EXPONENT = 0x1205; + public static final int GL_SPOT_CUTOFF = 0x1206; + public static final int GL_CONSTANT_ATTENUATION = 0x1207; + public static final int GL_LINEAR_ATTENUATION = 0x1208; + public static final int GL_QUADRATIC_ATTENUATION = 0x1209; + public static final int GL_COMPILE = 0x1300; + public static final int GL_COMPILE_AND_EXECUTE = 0x1301; + public static final int GL_CLEAR = 0x1500; + public static final int GL_AND = 0x1501; + public static final int GL_AND_REVERSE = 0x1502; + public static final int GL_COPY = 0x1503; + public static final int GL_AND_INVERTED = 0x1504; + public static final int GL_NOOP = 0x1505; + public static final int GL_XOR = 0x1506; + public static final int GL_OR = 0x1507; + public static final int GL_NOR = 0x1508; + public static final int GL_EQUIV = 0x1509; + public static final int GL_INVERT = 0x150a; + public static final int GL_OR_REVERSE = 0x150b; + public static final int GL_COPY_INVERTED = 0x150c; + public static final int GL_OR_INVERTED = 0x150d; + public static final int GL_NAND = 0x150e; + public static final int GL_SET = 0x150f; + public static final int GL_EMISSION = 0x1600; + public static final int GL_SHININESS = 0x1601; + public static final int GL_AMBIENT_AND_DIFFUSE = 0x1602; + public static final int GL_COLOR_INDEXES = 0x1603; + public static final int GL_MODELVIEW = 0x1700; + public static final int GL_PROJECTION = 0x1701; + public static final int GL_TEXTURE = 0x1702; + public static final int GL_COLOR = 0x1800; + public static final int GL_DEPTH = 0x1801; + public static final int GL_STENCIL = 0x1802; + public static final int GL_COLOR_INDEX = 0x1900; + public static final int GL_STENCIL_INDEX = 0x1901; + public static final int GL_DEPTH_COMPONENT = 0x1902; + public static final int GL_RED = 0x1903; + public static final int GL_GREEN = 0x1904; + public static final int GL_BLUE = 0x1905; + public static final int GL_ALPHA = 0x1906; + public static final int GL_RGB = 0x1907; + public static final int GL_RGBA = 0x1908; + public static final int GL_LUMINANCE = 0x1909; + public static final int GL_LUMINANCE_ALPHA = 0x190a; + public static final int GL_BITMAP = 0x1a00; + public static final int GL_POINT = 0x1b00; + public static final int GL_LINE = 0x1b01; + public static final int GL_FILL = 0x1b02; + public static final int GL_RENDER = 0x1c00; + public static final int GL_FEEDBACK = 0x1c01; + public static final int GL_SELECT = 0x1c02; + public static final int GL_FLAT = 0x1d00; + public static final int GL_SMOOTH = 0x1d01; + public static final int GL_KEEP = 0x1e00; + public static final int GL_REPLACE = 0x1e01; + public static final int GL_INCR = 0x1e02; + public static final int GL_DECR = 0x1e03; + public static final int GL_VENDOR = 0x1f00; + public static final int GL_RENDERER = 0x1f01; + public static final int GL_VERSION = 0x1f02; + public static final int GL_EXTENSIONS = 0x1f03; + public static final int GL_S = 0x2000; + public static final int GL_T = 0x2001; + public static final int GL_R = 0x2002; + public static final int GL_Q = 0x2003; + public static final int GL_MODULATE = 0x2100; + public static final int GL_DECAL = 0x2101; + public static final int GL_TEXTURE_ENV_MODE = 0x2200; + public static final int GL_TEXTURE_ENV_COLOR = 0x2201; + public static final int GL_TEXTURE_ENV = 0x2300; + public static final int GL_EYE_LINEAR = 0x2400; + public static final int GL_OBJECT_LINEAR = 0x2401; + public static final int GL_SPHERE_MAP = 0x2402; + public static final int GL_TEXTURE_GEN_MODE = 0x2500; + public static final int GL_OBJECT_PLANE = 0x2501; + public static final int GL_EYE_PLANE = 0x2502; + public static final int GL_NEAREST = 0x2600; + public static final int GL_LINEAR = 0x2601; + public static final int GL_NEAREST_MIPMAP_NEAREST = 0x2700; + public static final int GL_LINEAR_MIPMAP_NEAREST = 0x2701; + public static final int GL_NEAREST_MIPMAP_LINEAR = 0x2702; + public static final int GL_LINEAR_MIPMAP_LINEAR = 0x2703; + public static final int GL_TEXTURE_MAG_FILTER = 0x2800; + public static final int GL_TEXTURE_MIN_FILTER = 0x2801; + public static final int GL_TEXTURE_WRAP_S = 0x2802; + public static final int GL_TEXTURE_WRAP_T = 0x2803; + public static final int GL_CLAMP = 0x2900; + public static final int GL_REPEAT = 0x2901; + public static final int GL_CLIENT_PIXEL_STORE_BIT = 0x1; + public static final int GL_CLIENT_VERTEX_ARRAY_BIT = 0x2; + public static final int GL_ALL_CLIENT_ATTRIB_BITS = 0xffffffff; + public static final int GL_POLYGON_OFFSET_FACTOR = 0x8038; + public static final int GL_POLYGON_OFFSET_UNITS = 0x2a00; + public static final int GL_POLYGON_OFFSET_POINT = 0x2a01; + public static final int GL_POLYGON_OFFSET_LINE = 0x2a02; + public static final int GL_POLYGON_OFFSET_FILL = 0x8037; + public static final int GL_ALPHA4 = 0x803b; + public static final int GL_ALPHA8 = 0x803c; + public static final int GL_ALPHA12 = 0x803d; + public static final int GL_ALPHA16 = 0x803e; + public static final int GL_LUMINANCE4 = 0x803f; + public static final int GL_LUMINANCE8 = 0x8040; + public static final int GL_LUMINANCE12 = 0x8041; + public static final int GL_LUMINANCE16 = 0x8042; + public static final int GL_LUMINANCE4_ALPHA4 = 0x8043; + public static final int GL_LUMINANCE6_ALPHA2 = 0x8044; + public static final int GL_LUMINANCE8_ALPHA8 = 0x8045; + public static final int GL_LUMINANCE12_ALPHA4 = 0x8046; + public static final int GL_LUMINANCE12_ALPHA12 = 0x8047; + public static final int GL_LUMINANCE16_ALPHA16 = 0x8048; + public static final int GL_INTENSITY = 0x8049; + public static final int GL_INTENSITY4 = 0x804a; + public static final int GL_INTENSITY8 = 0x804b; + public static final int GL_INTENSITY12 = 0x804c; + public static final int GL_INTENSITY16 = 0x804d; + public static final int GL_R3_G3_B2 = 0x2a10; + public static final int GL_RGB4 = 0x804f; + public static final int GL_RGB5 = 0x8050; + public static final int GL_RGB8 = 0x8051; + public static final int GL_RGB10 = 0x8052; + public static final int GL_RGB12 = 0x8053; + public static final int GL_RGB16 = 0x8054; + public static final int GL_RGBA2 = 0x8055; + public static final int GL_RGBA4 = 0x8056; + public static final int GL_RGB5_A1 = 0x8057; + public static final int GL_RGBA8 = 0x8058; + public static final int GL_RGB10_A2 = 0x8059; + public static final int GL_RGBA12 = 0x805a; + public static final int GL_RGBA16 = 0x805b; + public static final int GL_TEXTURE_RED_SIZE = 0x805c; + public static final int GL_TEXTURE_GREEN_SIZE = 0x805d; + public static final int GL_TEXTURE_BLUE_SIZE = 0x805e; + public static final int GL_TEXTURE_ALPHA_SIZE = 0x805f; + public static final int GL_TEXTURE_LUMINANCE_SIZE = 0x8060; + public static final int GL_TEXTURE_INTENSITY_SIZE = 0x8061; + public static final int GL_PROXY_TEXTURE_1D = 0x8063; + public static final int GL_PROXY_TEXTURE_2D = 0x8064; + public static final int GL_TEXTURE_PRIORITY = 0x8066; + public static final int GL_TEXTURE_RESIDENT = 0x8067; + public static final int GL_TEXTURE_BINDING_1D = 0x8068; + public static final int GL_TEXTURE_BINDING_2D = 0x8069; + public static final int GL_VERTEX_ARRAY = 0x8074; + public static final int GL_NORMAL_ARRAY = 0x8075; + public static final int GL_COLOR_ARRAY = 0x8076; + public static final int GL_INDEX_ARRAY = 0x8077; + public static final int GL_TEXTURE_COORD_ARRAY = 0x8078; + public static final int GL_EDGE_FLAG_ARRAY = 0x8079; + public static final int GL_VERTEX_ARRAY_SIZE = 0x807a; + public static final int GL_VERTEX_ARRAY_TYPE = 0x807b; + public static final int GL_VERTEX_ARRAY_STRIDE = 0x807c; + public static final int GL_NORMAL_ARRAY_TYPE = 0x807e; + public static final int GL_NORMAL_ARRAY_STRIDE = 0x807f; + public static final int GL_COLOR_ARRAY_SIZE = 0x8081; + public static final int GL_COLOR_ARRAY_TYPE = 0x8082; + public static final int GL_COLOR_ARRAY_STRIDE = 0x8083; + public static final int GL_INDEX_ARRAY_TYPE = 0x8085; + public static final int GL_INDEX_ARRAY_STRIDE = 0x8086; + public static final int GL_TEXTURE_COORD_ARRAY_SIZE = 0x8088; + public static final int GL_TEXTURE_COORD_ARRAY_TYPE = 0x8089; + public static final int GL_TEXTURE_COORD_ARRAY_STRIDE = 0x808a; + public static final int GL_EDGE_FLAG_ARRAY_STRIDE = 0x808c; + public static final int GL_VERTEX_ARRAY_POINTER = 0x808e; + public static final int GL_NORMAL_ARRAY_POINTER = 0x808f; + public static final int GL_COLOR_ARRAY_POINTER = 0x8090; + public static final int GL_INDEX_ARRAY_POINTER = 0x8091; + public static final int GL_TEXTURE_COORD_ARRAY_POINTER = 0x8092; + public static final int GL_EDGE_FLAG_ARRAY_POINTER = 0x8093; + public static final int GL_V2F = 0x2a20; + public static final int GL_V3F = 0x2a21; + public static final int GL_C4UB_V2F = 0x2a22; + public static final int GL_C4UB_V3F = 0x2a23; + public static final int GL_C3F_V3F = 0x2a24; + public static final int GL_N3F_V3F = 0x2a25; + public static final int GL_C4F_N3F_V3F = 0x2a26; + public static final int GL_T2F_V3F = 0x2a27; + public static final int GL_T4F_V4F = 0x2a28; + public static final int GL_T2F_C4UB_V3F = 0x2a29; + public static final int GL_T2F_C3F_V3F = 0x2a2a; + public static final int GL_T2F_N3F_V3F = 0x2a2b; + public static final int GL_T2F_C4F_N3F_V3F = 0x2a2c; + public static final int GL_T4F_C4F_N3F_V4F = 0x2a2d; + public static final int GL_LOGIC_OP = 0xbf1; + public static final int GL_TEXTURE_COMPONENTS = 0x1003; + + // GL12 + public static final int GL_TEXTURE_BINDING_3D = 0x806a; + public static final int GL_PACK_SKIP_IMAGES = 0x806b; + public static final int GL_PACK_IMAGE_HEIGHT = 0x806c; + public static final int GL_UNPACK_SKIP_IMAGES = 0x806d; + public static final int GL_UNPACK_IMAGE_HEIGHT = 0x806e; + public static final int GL_TEXTURE_3D = 0x806f; + public static final int GL_PROXY_TEXTURE_3D = 0x8070; + public static final int GL_TEXTURE_DEPTH = 0x8071; + public static final int GL_TEXTURE_WRAP_R = 0x8072; + public static final int GL_MAX_3D_TEXTURE_SIZE = 0x8073; + public static final int GL_BGR = 0x80e0; + public static final int GL_BGRA = 0x80e1; + public static final int GL_UNSIGNED_BYTE_3_3_2 = 0x8032; + public static final int GL_UNSIGNED_BYTE_2_3_3_REV = 0x8362; + public static final int GL_UNSIGNED_SHORT_5_6_5 = 0x8363; + public static final int GL_UNSIGNED_SHORT_5_6_5_REV = 0x8364; + public static final int GL_UNSIGNED_SHORT_4_4_4_4 = 0x8033; + public static final int GL_UNSIGNED_SHORT_4_4_4_4_REV = 0x8365; + public static final int GL_UNSIGNED_SHORT_5_5_5_1 = 0x8034; + public static final int GL_UNSIGNED_SHORT_1_5_5_5_REV = 0x8366; + public static final int GL_UNSIGNED_INT_8_8_8_8 = 0x8035; + public static final int GL_UNSIGNED_INT_8_8_8_8_REV = 0x8367; + public static final int GL_UNSIGNED_INT_10_10_10_2 = 0x8036; + public static final int GL_UNSIGNED_INT_2_10_10_10_REV = 0x8368; + public static final int GL_RESCALE_NORMAL = 0x803a; + public static final int GL_LIGHT_MODEL_COLOR_CONTROL = 0x81f8; + public static final int GL_SINGLE_COLOR = 0x81f9; + public static final int GL_SEPARATE_SPECULAR_COLOR = 0x81fa; + public static final int GL_CLAMP_TO_EDGE = 0x812f; + public static final int GL_TEXTURE_MIN_LOD = 0x813a; + public static final int GL_TEXTURE_MAX_LOD = 0x813b; + public static final int GL_TEXTURE_BASE_LEVEL = 0x813c; + public static final int GL_TEXTURE_MAX_LEVEL = 0x813d; + public static final int GL_MAX_ELEMENTS_VERTICES = 0x80e8; + public static final int GL_MAX_ELEMENTS_INDICES = 0x80e9; + public static final int GL_ALIASED_POINT_SIZE_RANGE = 0x846d; + public static final int GL_ALIASED_LINE_WIDTH_RANGE = 0x846e; + public static final int GL_SMOOTH_POINT_SIZE_RANGE = 0xb12; + public static final int GL_SMOOTH_POINT_SIZE_GRANULARITY = 0xb13; + public static final int GL_SMOOTH_LINE_WIDTH_RANGE = 0xb22; + public static final int GL_SMOOTH_LINE_WIDTH_GRANULARITY = 0xb23; + + // GL13 + public static final int GL_TEXTURE0 = 0x84c0; + public static final int GL_TEXTURE1 = 0x84c1; + public static final int GL_TEXTURE2 = 0x84c2; + public static final int GL_TEXTURE3 = 0x84c3; + public static final int GL_TEXTURE4 = 0x84c4; + public static final int GL_TEXTURE5 = 0x84c5; + public static final int GL_TEXTURE6 = 0x84c6; + public static final int GL_TEXTURE7 = 0x84c7; + public static final int GL_TEXTURE8 = 0x84c8; + public static final int GL_TEXTURE9 = 0x84c9; + public static final int GL_TEXTURE10 = 0x84ca; + public static final int GL_TEXTURE11 = 0x84cb; + public static final int GL_TEXTURE12 = 0x84cc; + public static final int GL_TEXTURE13 = 0x84cd; + public static final int GL_TEXTURE14 = 0x84ce; + public static final int GL_TEXTURE15 = 0x84cf; + public static final int GL_TEXTURE16 = 0x84d0; + public static final int GL_TEXTURE17 = 0x84d1; + public static final int GL_TEXTURE18 = 0x84d2; + public static final int GL_TEXTURE19 = 0x84d3; + public static final int GL_TEXTURE20 = 0x84d4; + public static final int GL_TEXTURE21 = 0x84d5; + public static final int GL_TEXTURE22 = 0x84d6; + public static final int GL_TEXTURE23 = 0x84d7; + public static final int GL_TEXTURE24 = 0x84d8; + public static final int GL_TEXTURE25 = 0x84d9; + public static final int GL_TEXTURE26 = 0x84da; + public static final int GL_TEXTURE27 = 0x84db; + public static final int GL_TEXTURE28 = 0x84dc; + public static final int GL_TEXTURE29 = 0x84dd; + public static final int GL_TEXTURE30 = 0x84de; + public static final int GL_TEXTURE31 = 0x84df; + public static final int GL_ACTIVE_TEXTURE = 0x84e0; + public static final int GL_CLIENT_ACTIVE_TEXTURE = 0x84e1; + public static final int GL_MAX_TEXTURE_UNITS = 0x84e2; + public static final int GL_NORMAL_MAP = 0x8511; + public static final int GL_REFLECTION_MAP = 0x8512; + public static final int GL_TEXTURE_CUBE_MAP = 0x8513; + public static final int GL_TEXTURE_BINDING_CUBE_MAP = 0x8514; + public static final int GL_TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515; + public static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516; + public static final int GL_TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517; + public static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518; + public static final int GL_TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519; + public static final int GL_TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851a; + public static final int GL_PROXY_TEXTURE_CUBE_MAP = 0x851b; + public static final int GL_MAX_CUBE_MAP_TEXTURE_SIZE = 0x851c; + public static final int GL_COMPRESSED_ALPHA = 0x84e9; + public static final int GL_COMPRESSED_LUMINANCE = 0x84ea; + public static final int GL_COMPRESSED_LUMINANCE_ALPHA = 0x84eb; + public static final int GL_COMPRESSED_INTENSITY = 0x84ec; + public static final int GL_COMPRESSED_RGB = 0x84ed; + public static final int GL_COMPRESSED_RGBA = 0x84ee; + public static final int GL_TEXTURE_COMPRESSION_HINT = 0x84ef; + public static final int GL_TEXTURE_COMPRESSED_IMAGE_SIZE = 0x86a0; + public static final int GL_TEXTURE_COMPRESSED = 0x86a1; + public static final int GL_NUM_COMPRESSED_TEXTURE_FORMATS = 0x86a2; + public static final int GL_COMPRESSED_TEXTURE_FORMATS = 0x86a3; + public static final int GL_MULTISAMPLE = 0x809d; + public static final int GL_SAMPLE_ALPHA_TO_COVERAGE = 0x809e; + public static final int GL_SAMPLE_ALPHA_TO_ONE = 0x809f; + public static final int GL_SAMPLE_COVERAGE = 0x80a0; + public static final int GL_SAMPLE_BUFFERS = 0x80a8; + public static final int GL_SAMPLES = 0x80a9; + public static final int GL_SAMPLE_COVERAGE_VALUE = 0x80aa; + public static final int GL_SAMPLE_COVERAGE_INVERT = 0x80ab; + public static final int GL_MULTISAMPLE_BIT = 0x20000000; + public static final int GL_TRANSPOSE_MODELVIEW_MATRIX = 0x84e3; + public static final int GL_TRANSPOSE_PROJECTION_MATRIX = 0x84e4; + public static final int GL_TRANSPOSE_TEXTURE_MATRIX = 0x84e5; + public static final int GL_TRANSPOSE_COLOR_MATRIX = 0x84e6; + public static final int GL_COMBINE = 0x8570; + public static final int GL_COMBINE_RGB = 0x8571; + public static final int GL_COMBINE_ALPHA = 0x8572; + public static final int GL_SOURCE0_RGB = 0x8580; + public static final int GL_SOURCE1_RGB = 0x8581; + public static final int GL_SOURCE2_RGB = 0x8582; + public static final int GL_SOURCE0_ALPHA = 0x8588; + public static final int GL_SOURCE1_ALPHA = 0x8589; + public static final int GL_SOURCE2_ALPHA = 0x858a; + public static final int GL_OPERAND0_RGB = 0x8590; + public static final int GL_OPERAND1_RGB = 0x8591; + public static final int GL_OPERAND2_RGB = 0x8592; + public static final int GL_OPERAND0_ALPHA = 0x8598; + public static final int GL_OPERAND1_ALPHA = 0x8599; + public static final int GL_OPERAND2_ALPHA = 0x859a; + public static final int GL_RGB_SCALE = 0x8573; + public static final int GL_ADD_SIGNED = 0x8574; + public static final int GL_INTERPOLATE = 0x8575; + public static final int GL_SUBTRACT = 0x84e7; + public static final int GL_CONSTANT = 0x8576; + public static final int GL_PRIMARY_COLOR = 0x8577; + public static final int GL_PREVIOUS = 0x8578; + public static final int GL_DOT3_RGB = 0x86ae; + public static final int GL_DOT3_RGBA = 0x86af; + public static final int GL_CLAMP_TO_BORDER = 0x812d; + + // GL14 + public static final int GL_GENERATE_MIPMAP = 0x8191; + public static final int GL_GENERATE_MIPMAP_HINT = 0x8192; + public static final int GL_DEPTH_COMPONENT16 = 0x81a5; + public static final int GL_DEPTH_COMPONENT24 = 0x81a6; + public static final int GL_DEPTH_COMPONENT32 = 0x81a7; + public static final int GL_TEXTURE_DEPTH_SIZE = 0x884a; + public static final int GL_DEPTH_TEXTURE_MODE = 0x884b; + public static final int GL_TEXTURE_COMPARE_MODE = 0x884c; + public static final int GL_TEXTURE_COMPARE_FUNC = 0x884d; + public static final int GL_COMPARE_R_TO_TEXTURE = 0x884e; + public static final int GL_FOG_COORDINATE_SOURCE = 0x8450; + public static final int GL_FOG_COORDINATE = 0x8451; + public static final int GL_FRAGMENT_DEPTH = 0x8452; + public static final int GL_CURRENT_FOG_COORDINATE = 0x8453; + public static final int GL_FOG_COORDINATE_ARRAY_TYPE = 0x8454; + public static final int GL_FOG_COORDINATE_ARRAY_STRIDE = 0x8455; + public static final int GL_FOG_COORDINATE_ARRAY_POINTER = 0x8456; + public static final int GL_FOG_COORDINATE_ARRAY = 0x8457; + public static final int GL_POINT_SIZE_MIN = 0x8126; + public static final int GL_POINT_SIZE_MAX = 0x8127; + public static final int GL_POINT_FADE_THRESHOLD_SIZE = 0x8128; + public static final int GL_POINT_DISTANCE_ATTENUATION = 0x8129; + public static final int GL_COLOR_SUM = 0x8458; + public static final int GL_CURRENT_SECONDARY_COLOR = 0x8459; + public static final int GL_SECONDARY_COLOR_ARRAY_SIZE = 0x845a; + public static final int GL_SECONDARY_COLOR_ARRAY_TYPE = 0x845b; + public static final int GL_SECONDARY_COLOR_ARRAY_STRIDE = 0x845c; + public static final int GL_SECONDARY_COLOR_ARRAY_POINTER = 0x845d; + public static final int GL_SECONDARY_COLOR_ARRAY = 0x845e; + public static final int GL_BLEND_DST_RGB = 0x80c8; + public static final int GL_BLEND_SRC_RGB = 0x80c9; + public static final int GL_BLEND_DST_ALPHA = 0x80ca; + public static final int GL_BLEND_SRC_ALPHA = 0x80cb; + public static final int GL_INCR_WRAP = 0x8507; + public static final int GL_DECR_WRAP = 0x8508; + public static final int GL_TEXTURE_FILTER_CONTROL = 0x8500; + public static final int GL_TEXTURE_LOD_BIAS = 0x8501; + public static final int GL_MAX_TEXTURE_LOD_BIAS = 0x84fd; + public static final int GL_MIRRORED_REPEAT = 0x8370; + public static final int GL_BLEND_COLOR = 0x8005; + public static final int GL_BLEND_EQUATION = 0x8009; + public static final int GL_FUNC_ADD = 0x8006; + public static final int GL_FUNC_SUBTRACT = 0x800a; + public static final int GL_FUNC_REVERSE_SUBTRACT = 0x800b; + public static final int GL_MIN = 0x8007; + public static final int GL_MAX = 0x8008; + + // GL15 + public static final int GL_ARRAY_BUFFER = 0x8892; + public static final int GL_ELEMENT_ARRAY_BUFFER = 0x8893; + public static final int GL_ARRAY_BUFFER_BINDING = 0x8894; + public static final int GL_ELEMENT_ARRAY_BUFFER_BINDING = 0x8895; + public static final int GL_VERTEX_ARRAY_BUFFER_BINDING = 0x8896; + public static final int GL_NORMAL_ARRAY_BUFFER_BINDING = 0x8897; + public static final int GL_COLOR_ARRAY_BUFFER_BINDING = 0x8898; + public static final int GL_INDEX_ARRAY_BUFFER_BINDING = 0x8899; + public static final int GL_TEXTURE_COORD_ARRAY_BUFFER_BINDING = 0x889a; + public static final int GL_EDGE_FLAG_ARRAY_BUFFER_BINDING = 0x889b; + public static final int GL_SECONDARY_COLOR_ARRAY_BUFFER_BINDING = 0x889c; + public static final int GL_FOG_COORDINATE_ARRAY_BUFFER_BINDING = 0x889d; + public static final int GL_WEIGHT_ARRAY_BUFFER_BINDING = 0x889e; + public static final int GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING = 0x889f; + public static final int GL_STREAM_DRAW = 0x88e0; + public static final int GL_STREAM_READ = 0x88e1; + public static final int GL_STREAM_COPY = 0x88e2; + public static final int GL_STATIC_DRAW = 0x88e4; + public static final int GL_STATIC_READ = 0x88e5; + public static final int GL_STATIC_COPY = 0x88e6; + public static final int GL_DYNAMIC_DRAW = 0x88e8; + public static final int GL_DYNAMIC_READ = 0x88e9; + public static final int GL_DYNAMIC_COPY = 0x88ea; + public static final int GL_READ_ONLY = 0x88b8; + public static final int GL_WRITE_ONLY = 0x88b9; + public static final int GL_READ_WRITE = 0x88ba; + public static final int GL_BUFFER_SIZE = 0x8764; + public static final int GL_BUFFER_USAGE = 0x8765; + public static final int GL_BUFFER_ACCESS = 0x88bb; + public static final int GL_BUFFER_MAPPED = 0x88bc; + public static final int GL_BUFFER_MAP_POINTER = 0x88bd; + public static final int FOG_COORD_SRC = 0x8450; + public static final int GL_FOG_COORD = 0x8451; + public static final int GL_CURRENT_FOG_COORD = 0x8453; + public static final int GL_FOG_COORD_ARRAY_TYPE = 0x8454; + public static final int GL_FOG_COORD_ARRAY_STRIDE = 0x8455; + public static final int GL_FOG_COORD_ARRAY_POINTER = 0x8456; + public static final int GL_FOG_COORD_ARRAY = 0x8457; + public static final int GL_FOG_COORD_ARRAY_BUFFER_BINDING = 0x889d; + public static final int GL_SRC0_RGB = 0x8580; + public static final int GL_SRC1_RGB = 0x8581; + public static final int GL_SRC2_RGB = 0x8582; + public static final int GL_SRC0_ALPHA = 0x8588; + public static final int GL_SRC1_ALPHA = 0x8589; + public static final int GL_SRC2_ALPHA = 0x858a; + + public static void glPushAttrib() + { + // GL_ENABLE_BIT | GL_LIGHTING_BIT + GlStateManager.pushAttrib(); + } + + public static void glPushAttrib(int mask) + { + GL11.glPushAttrib(mask); + } + + public static void glPopAttrib() + { + GlStateManager.popAttrib(); + } + + public static void glDisableAlphaTest() + { + GlStateManager.disableAlpha(); + } + + public static void glEnableAlphaTest() + { + GlStateManager.enableAlpha(); + } + + public static void glAlphaFunc(int func, float ref) + { + GlStateManager.alphaFunc(func, ref); + } + + public static void glEnableLighting() + { + GlStateManager.enableLighting(); + } + + public static void glDisableLighting() + { + GlStateManager.disableLighting(); + } + + public static void glEnableLight(int light) + { + GlStateManager.enableLight(light); // TODO OBF MCPTEST enableBooleanStateAt - enableLight + } + + public static void glDisableLight(int light) + { + GlStateManager.disableLight(light); // TODO OBF MCPTEST disableBooleanStateAt - disableLight + } + + public static void glLight(int light, int pname, FloatBuffer params) + { + GL11.glLight(light, pname, params); + } + + public static void glLightModel(int pname, FloatBuffer params) + { + GL11.glLightModel(pname, params); + } + + public static void glLightModeli(int pname, int param) + { + GL11.glLightModeli(pname, param); + } + + public static void glEnableColorMaterial() + { + GlStateManager.enableColorMaterial(); + } + + public static void glDisableColorMaterial() + { + GlStateManager.disableColorMaterial(); + } + + public static void glColorMaterial(int face, int mode) + { + GlStateManager.colorMaterial(face, mode); + } + + public static void glDisableDepthTest() + { + GlStateManager.disableDepth(); + } + + public static void glEnableDepthTest() + { + GlStateManager.enableDepth(); + } + + public static void glDepthFunc(int func) + { + GlStateManager.depthFunc(func); + } + + public static void glDepthMask(boolean flag) + { + GlStateManager.depthMask(flag); + } + + public static void glDisableBlend() + { + GlStateManager.disableBlend(); + } + + public static void glEnableBlend() + { + GlStateManager.enableBlend(); + } + + public static void glBlendFunc(int sfactor, int dfactor) + { + GlStateManager.blendFunc(sfactor, dfactor); + } + + public static void glBlendFuncSeparate(int sfactorRGB, int dfactorRGB, int sfactorAlpha, int dfactorAlpha) + { + GlStateManager.tryBlendFuncSeparate(sfactorRGB, dfactorRGB, sfactorAlpha, dfactorAlpha); + } + + public static void glEnableFog() + { + GlStateManager.enableFog(); + } + + public static void glDisableFog() + { + GlStateManager.disableFog(); + } + + public static void glSetFogMode(int mode) + { + GlStateManager.setFog(mode); + } + + public static void glSetFogDensity(float density) + { + GlStateManager.setFogDensity(density); + } + + public static void glSetFogStart(float start) + { + GlStateManager.setFogStart(start); + } + + public static void glSetFogEnd(float end) + { + GlStateManager.setFogEnd(end); + } + + public static void glSetFogColour(FloatBuffer colour) + { + GL11.glFog(GL_FOG_COLOR, colour); + } + + public static void glFogi(int pname, int param) + { + GL11.glFogi(pname, param); + } + + public static void glFogf(int pname, float param) + { + GL11.glFogf(pname, param); + } + + public static void glEnableCulling() + { + GlStateManager.enableCull(); + } + + public static void glDisableCulling() + { + GlStateManager.disableCull(); + } + + public static void glCullFace(int mode) + { + GlStateManager.cullFace(mode); + } + + public static void glEnablePolygonOffset() + { + GlStateManager.enablePolygonOffset(); + } + + public static void glDisablePolygonOffset() + { + GlStateManager.disablePolygonOffset(); + } + + public static void glPolygonOffset(float factor, float units) + { + GlStateManager.doPolygonOffset(factor, units); + } + + public static void glEnableColorLogic() + { + GlStateManager.enableColorLogic(); + } + + public static void glDisableColorLogic() + { + GlStateManager.disableColorLogic(); + } + + public static void glLogicOp(int opcode) + { + GlStateManager.colorLogicOp(opcode); + } + + public static void glEnableTexGenCoord(TexGen tex) + { + GlStateManager.enableTexGenCoord(tex); + } + + public static void glDisableTexGenCoord(TexGen tex) + { + GlStateManager.disableTexGenCoord(tex); + } + + public static void glTexGeni(TexGen tex, int mode) + { + GlStateManager.texGen(tex, mode); + } + + public static void glTexGen(TexGen tex, int pname, FloatBuffer params) + { + GlStateManager.func_179105_a(tex, pname, params); + } + + public static void glSetActiveTextureUnit(int texture) + { + GlStateManager.setActiveTexture(texture); + } + + public static void glEnableTexture2D() + { + GlStateManager.enableTexture2D(); // TODO OBF MCPTEST func_179098_w - enableTexture2D + } + + public static void glDisableTexture2D() + { + GlStateManager.disableTexture2D(); // TODO OBF MCPTEST func_179090_x - disableTexture2D + } + + public static int glGenTextures() + { + return GlStateManager.generateTexture(); // TODO OBF MCPTEST func_179146_y - generateTexture + } + + public static void glDeleteTextures(int textureName) + { + GlStateManager.deleteTexture(textureName); // TODO OBF MCPTEST func_179150_h - deleteTexture + } + + public static void glBindTexture2D(int textureName) + { + GlStateManager.bindTexture(textureName); // TODO OBF MCPTEST func_179144_i - bindTexture + } + + public static void glEnableNormalize() + { + GlStateManager.enableNormalize(); + } + + public static void glDisableNormalize() + { + GlStateManager.disableNormalize(); + } + + public static void glShadeModel(int mode) + { + GlStateManager.shadeModel(mode); + } + + public static void glEnableRescaleNormal() + { + GlStateManager.enableRescaleNormal(); + } + + public static void glDisableRescaleNormal() + { + GlStateManager.disableRescaleNormal(); + } + + public static void glViewport(int x, int y, int width, int height) + { + GlStateManager.viewport(x, y, width, height); + } + + public static void glColorMask(boolean red, boolean green, boolean blue, boolean alpha) + { + GlStateManager.colorMask(red, green, blue, alpha); + } + + public static void glClearDepth(double depth) + { + GlStateManager.clearDepth(depth); + } + + public static void glClearColor(float red, float green, float blue, float alpha) + { + GlStateManager.clearColor(red, green, blue, alpha); + } + + public static void glClear(int mask) + { + GlStateManager.clear(mask); + } + + public static void glMatrixMode(int mode) + { + GlStateManager.matrixMode(mode); + } + + public static void glLoadIdentity() + { + GlStateManager.loadIdentity(); + } + + public static void glPushMatrix() + { + GlStateManager.pushMatrix(); + } + + public static void glPopMatrix() + { + GlStateManager.popMatrix(); + } + + public static void glGetFloat(int pname, FloatBuffer params) + { + GlStateManager.getFloat(pname, params); + } + + public static float glGetFloat(int pname) + { + return GL11.glGetFloat(pname); + } + + public static void glGetDouble(int pname, DoubleBuffer params) + { + GL11.glGetDouble(pname, params); + } + + public static double glGetDouble(int pname) + { + return GL11.glGetDouble(pname); + } + + public static void glGetInteger(int pname, IntBuffer params) + { + GL11.glGetInteger(pname, params); + } + + public static int glGetInteger(int pname) + { + return GL11.glGetInteger(pname); + } + + public static void glGetBoolean(int pname, ByteBuffer params) + { + GL11.glGetBoolean(pname, params); + } + + public static boolean glGetBoolean(int pname) + { + return GL11.glGetBoolean(pname); + } + + public static void gluProject(float objx, float objy, float objz, FloatBuffer modelMatrix, FloatBuffer projMatrix, IntBuffer viewport, + FloatBuffer winPos) + { + GLU.gluProject(objx, objy, objz, modelMatrix, projMatrix, viewport, winPos); + } + + public static void gluPerspective(float fovy, float aspect, float zNear, float zFar) + { + GLU.gluPerspective(fovy, aspect, zNear, zFar); + } + + public static void glOrtho(double left, double right, double bottom, double top, double zNear, double zFar) + { + GlStateManager.ortho(left, right, bottom, top, zNear, zFar); + } + + public static void glRotatef(float angle, float x, float y, float z) + { + GlStateManager.rotate(angle, x, y, z); + } + + public static void glRotated(double angle, double x, double y, double z) + { + GL11.glRotated(angle, x, y, z); + } + + public static void glScalef(float x, float y, float z) + { + GlStateManager.scale(x, y, z); + } + + public static void glScaled(double x, double y, double z) + { + GlStateManager.scale(x, y, z); + } + + public static void glTranslatef(float x, float y, float z) + { + GlStateManager.translate(x, y, z); + } + + public static void glTranslated(double x, double y, double z) + { + GlStateManager.translate(x, y, z); + } + + public static void glMultMatrix(FloatBuffer m) + { + GlStateManager.multMatrix(m); + } + + public static void glColor4f(float red, float green, float blue, float alpha) + { + GlStateManager.color(red, green, blue, alpha); + } + + public static void glColor3f(float red, float green, float blue) + { + GlStateManager.color(red, green, blue, 1.0F); + } + + public static void glResetColor() + { + GlStateManager.resetColor(); + } + + public static void glCallList(int list) + { + GlStateManager.callList(list); + } + + public static void glCallLists(IntBuffer lists) + { + GL11.glCallLists(lists); + } + + public static void glNewList(int list, int mode) + { + GL11.glNewList(list, mode); + } + + public static void glEndList() + { + GL11.glEndList(); + } + + public static void glLineWidth(float width) + { + GL11.glLineWidth(width); + } + + public static void glPolygonMode(int face, int mode) + { + GL11.glPolygonMode(face, mode); + } + + public static void glPixelStorei(int pname, int param) + { + GL11.glPixelStorei(pname, param); + } + + public static void glReadPixels(int x, int y, int width, int height, int format, int type, ByteBuffer pixels) + { + GL11.glReadPixels(x, y, width, height, format, type, pixels); + } + + public static void glGetTexImage(int target, int level, int format, int type, ByteBuffer pixels) + { + GL11.glGetTexImage(target, level, format, type, pixels); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/gl/GLClippingPlanes.java b/liteloader/src/client/java/com/mumfrey/liteloader/gl/GLClippingPlanes.java new file mode 100644 index 00000000..a7d4c76b --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/gl/GLClippingPlanes.java @@ -0,0 +1,193 @@ +package com.mumfrey.liteloader.gl; + +import static org.lwjgl.opengl.GL11.*; + +import java.nio.DoubleBuffer; + +import org.lwjgl.BufferUtils; +import org.lwjgl.util.Rectangle; + +/** + * OpenGL clipping plane convenience functions. We prefer to clip rectangular + * GUI regions in Minecraft using clipping rather than scissor because scissor + * is a nuisance to work with, primarily because it works in "window" (OpenGL + * window) coordinates and doesn't respect the current transformation matrix. + * Using clipping planes we can specify clipping edges in "Minecraft screen + * coordinates", can optionally clip on only one or two axes, and also don't + * need to worry about the current transform. + * + * @author Adam Mummery-Smith + */ +public final class GLClippingPlanes +{ + public enum Plane + { + LEFT(GL_CLIP_PLANE0), + RIGHT(GL_CLIP_PLANE1), + TOP(GL_CLIP_PLANE2), + BOTTOM(GL_CLIP_PLANE3); + + final int plane; + final int sign; + + private Plane(int plane) + { + this.plane = plane; + this.sign = (plane % 2 == 0) ? -1 : 1; + } + } + + private static final int STACK_DEPTH = 1; + private static final int STACK_FRAME_SIZE = 128; + + private static DoubleBuffer doubleBuffer = BufferUtils.createByteBuffer(STACK_FRAME_SIZE * STACK_DEPTH).asDoubleBuffer(); + + private static int clippingPlaneFlags = 0; + + private static int totalClippingPlanes = glGetInteger(GL_MAX_CLIP_PLANES); + +// private static int frame = 0; + + static + { + for (int f = 0; f < STACK_DEPTH; f++) + { + // Clipping normals + GLClippingPlanes.doubleBuffer.put(1).put(0).put(0).put(0); + GLClippingPlanes.doubleBuffer.put(-1).put(0).put(0).put(0); + GLClippingPlanes.doubleBuffer.put(0).put(1).put(0).put(0); + GLClippingPlanes.doubleBuffer.put(0).put(-1).put(0).put(0); + } + } + + private GLClippingPlanes() {} + + /** + * Get the total number of available clipping planes on the platform + */ + public static int glGetTotalClippingPlanes() + { + return GLClippingPlanes.totalClippingPlanes; + } + + /** + * Enable OpenGL clipping planes (uses planes 0, 1, 2 and 3) + * + * @param left Left edge clip or -1 to not use this plane + * @param right Right edge clip or -1 to not use this plane + * @param top Top edge clip or -1 to not use this plane + * @param bottom Bottom edge clip or -1 to not use this plane + */ + public static void glEnableClipping(int left, int right, int top, int bottom) + { + GLClippingPlanes.clippingPlaneFlags = 0; + + glEnableClipping(GL_CLIP_PLANE0, left, -1); + glEnableClipping(GL_CLIP_PLANE1, right, 1); + glEnableClipping(GL_CLIP_PLANE2, top, -1); + glEnableClipping(GL_CLIP_PLANE3, bottom, 1); + } + + /** + * Enable OpenGL clipping planes (uses planes 0, 1, 2 and 3) + * + * @param rect Clipping rectangle + */ + public static void glEnableClipping(Rectangle rect) + { + GLClippingPlanes.clippingPlaneFlags = 0; + + glEnableClipping(GL_CLIP_PLANE0, rect.getX(), -1); + glEnableClipping(GL_CLIP_PLANE1, rect.getX() + rect.getWidth(), 1); + glEnableClipping(GL_CLIP_PLANE2, rect.getY(), -1); + glEnableClipping(GL_CLIP_PLANE3, rect.getY() + rect.getHeight(), 1); + } + + /** + * Enable horizontal clipping planes (left and right) (uses planes 0, 1) + * + * @param left Left edge clip or -1 to not use this plane + * @param right Right edge clip or -1 to not use this plane + */ + public static void glEnableHorizontalClipping(int left, int right) + { + glEnableClipping(GL_CLIP_PLANE0, left, -1); + glEnableClipping(GL_CLIP_PLANE1, right, 1); + } + + /** + * Enable vertical clipping planes (top and bottom) (uses planes 2, 3) + * + * @param top Top edge clip or -1 to not use this plane + * @param bottom Bottom edge clip or -1 to not use this plane + */ + public static void glEnableVerticalClipping(int top, int bottom) + { + glEnableClipping(GL_CLIP_PLANE2, top, -1); + glEnableClipping(GL_CLIP_PLANE3, bottom, 1); + } + + /** + * @param plane + * @param value + */ + public static void glEnableClipping(int plane, int value) + { + if (plane < GL_CLIP_PLANE0 || plane >= (GL_CLIP_PLANE0 + GLClippingPlanes.totalClippingPlanes)) + { + throw new IllegalArgumentException("Invalid clipping plane enum specified GL_CLIP_PLANE" + (plane - GL_CLIP_PLANE0)); + } + + glEnableClipping(plane, value, (plane % 2 == 0) ? -1 : 1); + } + + /** + * @param plane + * @param value + */ + public static void glEnableClipping(Plane plane, int value) + { + glEnableClipping(plane.plane, value, plane.sign); + } + + /** + * Enable clipping on a particular axis + * + * @param plane Clipping plane to enable + * @param value Clipping plane position + * @param sign Sign of the position + */ + private static void glEnableClipping(int plane, int value, int sign) + { + if (value == -1) return; + + int offset = (plane - GL_CLIP_PLANE0) << 2; + GLClippingPlanes.doubleBuffer.put(offset + 3, value * sign).position(offset); + GLClippingPlanes.clippingPlaneFlags |= plane; + + glClipPlane(plane, GLClippingPlanes.doubleBuffer); + glEnable(plane); + } + + /** + * Enable clipping planes which were previously enabled + */ + public static void glEnableClipping() + { + if ((GLClippingPlanes.clippingPlaneFlags & GL_CLIP_PLANE0) == GL_CLIP_PLANE0) glEnable(GL_CLIP_PLANE0); + if ((GLClippingPlanes.clippingPlaneFlags & GL_CLIP_PLANE1) == GL_CLIP_PLANE1) glEnable(GL_CLIP_PLANE1); + if ((GLClippingPlanes.clippingPlaneFlags & GL_CLIP_PLANE2) == GL_CLIP_PLANE2) glEnable(GL_CLIP_PLANE2); + if ((GLClippingPlanes.clippingPlaneFlags & GL_CLIP_PLANE3) == GL_CLIP_PLANE3) glEnable(GL_CLIP_PLANE3); + } + + /** + * Disable OpenGL clipping planes (uses planes 2, 3, 4 and 5) + */ + public static void glDisableClipping() + { + glDisable(GL_CLIP_PLANE3); + glDisable(GL_CLIP_PLANE2); + glDisable(GL_CLIP_PLANE1); + glDisable(GL_CLIP_PLANE0); + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/resources/InternalResourcePack.java b/liteloader/src/client/java/com/mumfrey/liteloader/resources/InternalResourcePack.java new file mode 100644 index 00000000..331221e7 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/resources/InternalResourcePack.java @@ -0,0 +1,119 @@ +package com.mumfrey.liteloader.resources; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +import net.minecraft.client.resources.IResourcePack; +import net.minecraft.client.resources.data.IMetadataSection; +import net.minecraft.client.resources.data.IMetadataSerializer; +import net.minecraft.util.ResourceLocation; + +/** + * Resource pack which returns resources using Class::getResourceAsStream() on + * the specified class. + * + * @author Adam Mummery-Smith + */ +public class InternalResourcePack implements IResourcePack +{ + /** + * Domains supported by this resource pack + */ + private final Set resourceDomains = new HashSet(); + + /** + * Name of this resource pack + */ + private final String packName; + + /** + * Class to execute getResourceAsStream() upon + */ + private final Class resourceClass; + + /** + * @param name + * @param resourceClass + * @param domains + */ + public InternalResourcePack(String name, Class resourceClass, String... domains) + { + if (domains.length < 1) throw new IllegalArgumentException("No domains specified for internal resource pack"); + + this.packName = name; + this.resourceClass = resourceClass; + + for (String domain : domains) + this.resourceDomains.add(domain); + } + + /* (non-Javadoc) + * @see net.minecraft.client.resources.IResourcePack + * #getInputStream(net.minecraft.util.ResourceLocation) + */ + @Override + public InputStream getInputStream(ResourceLocation resourceLocation) throws IOException + { + return this.getResourceStream(resourceLocation); + } + + /** + * @param resourceLocation + */ + private InputStream getResourceStream(ResourceLocation resourceLocation) + { + return this.resourceClass.getResourceAsStream(String.format("/assets/%s/%s", + resourceLocation.getResourceDomain(), resourceLocation.getResourcePath())); + } + + /* (non-Javadoc) + * @see net.minecraft.client.resources.IResourcePack#resourceExists( + * net.minecraft.util.ResourceLocation) + */ + @Override + public boolean resourceExists(ResourceLocation resourceLocation) + { + return this.getResourceStream(resourceLocation) != null; + } + + /* (non-Javadoc) + * @see net.minecraft.client.resources.IResourcePack#getResourceDomains() + */ + @Override + public Set getResourceDomains() + { + return this.resourceDomains; + } + + /* (non-Javadoc) + * @see net.minecraft.client.resources.IResourcePack#getPackMetadata( + * net.minecraft.client.resources.data.IMetadataSerializer, + * java.lang.String) + */ + @Override + public IMetadataSection getPackMetadata(IMetadataSerializer par1MetadataSerializer, String par2Str) throws IOException + { + return null; + } + + /* (non-Javadoc) + * @see net.minecraft.client.resources.IResourcePack#getPackImage() + */ + @Override + public BufferedImage getPackImage() throws IOException + { + return null; + } + + /* (non-Javadoc) + * @see net.minecraft.client.resources.IResourcePack#getPackName() + */ + @Override + public String getPackName() + { + return this.packName; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/resources/ModResourcePack.java b/liteloader/src/client/java/com/mumfrey/liteloader/resources/ModResourcePack.java new file mode 100644 index 00000000..f8fa5acc --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/resources/ModResourcePack.java @@ -0,0 +1,59 @@ +package com.mumfrey.liteloader.resources; + +import java.io.File; +import java.io.IOException; + +import net.minecraft.client.resources.FileResourcePack; +import net.minecraft.client.resources.data.IMetadataSection; +import net.minecraft.client.resources.data.IMetadataSerializer; + +/** + * Resource pack which wraps a mod file + * + * @author Adam Mummery-Smith + */ +public class ModResourcePack extends FileResourcePack +{ + /** + * Display name, only shows up in debug output + */ + private final String name; + + /** + * @param name Friendly name + * @param modFile + */ + public ModResourcePack(String name, File modFile) + { + super(modFile); + this.name = name; + } + + /* (non-Javadoc) + * @see net.minecraft.client.resources.AbstractResourcePack#getPackMetadata( + * net.minecraft.client.resources.data.IMetadataSerializer, + * java.lang.String) + */ + @Override + public IMetadataSection getPackMetadata(IMetadataSerializer metadataSerializer, String metadataSectionName) throws IOException + { + try + { + // This will fail when fetching pack.mcmeta if there isn't one in the mod file, since we don't care we + // just catch the exception and return null instead + return super.getPackMetadata(metadataSerializer, metadataSectionName); + } + catch (Exception ex) {} + + return null; + } + + /* (non-Javadoc) + * @see net.minecraft.client.resources.AbstractResourcePack#getPackName() + */ + @Override + public String getPackName() + { + return this.name; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/resources/ModResourcePackDir.java b/liteloader/src/client/java/com/mumfrey/liteloader/resources/ModResourcePackDir.java new file mode 100644 index 00000000..5efba35f --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/resources/ModResourcePackDir.java @@ -0,0 +1,59 @@ +package com.mumfrey.liteloader.resources; + +import java.io.File; +import java.io.IOException; + +import net.minecraft.client.resources.FolderResourcePack; +import net.minecraft.client.resources.data.IMetadataSection; +import net.minecraft.client.resources.data.IMetadataSerializer; + +/** + * Resource pack which wraps a mod directory on the classpath + * + * @author Adam Mummery-Smith + */ +public class ModResourcePackDir extends FolderResourcePack +{ + /** + * Display name, only shows up in debug output + */ + private final String name; + + /** + * @param name Friendly name + * @param modFile + */ + public ModResourcePackDir(String name, File modFile) + { + super(modFile); + this.name = name; + } + + /* (non-Javadoc) + * @see net.minecraft.client.resources.AbstractResourcePack#getPackMetadata( + * net.minecraft.client.resources.data.IMetadataSerializer, + * java.lang.String) + */ + @Override + public IMetadataSection getPackMetadata(IMetadataSerializer metadataSerializer, String metadataSectionName) throws IOException + { + try + { + // This will fail when fetching pack.mcmeta if there isn't one in the mod file, since we don't care we + // just catch the exception and return null instead + return super.getPackMetadata(metadataSerializer, metadataSectionName); + } + catch (Exception ex) {} + + return null; + } + + /* (non-Javadoc) + * @see net.minecraft.client.resources.AbstractResourcePack#getPackName() + */ + @Override + public String getPackName() + { + return this.name; + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/util/InputManager.java b/liteloader/src/client/java/com/mumfrey/liteloader/util/InputManager.java new file mode 100644 index 00000000..76be35bd --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/util/InputManager.java @@ -0,0 +1,361 @@ +package com.mumfrey.liteloader.util; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import net.java.games.input.Component; +import net.java.games.input.Controller; +import net.java.games.input.Event; +import net.java.games.input.EventQueue; +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.profiler.Profiler; + +import com.mumfrey.liteloader.common.GameEngine; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.util.jinput.ComponentRegistry; + +/** + * Mod input class, aggregates functionality from LiteLoader's mod key + * registration functions and JInputLib. + * + * @author Adam Mummery-Smith + */ +public final class InputManager extends Input +{ + private GameEngine engine; + + /** + * + */ + private Profiler profiler; + + /** + * File in which we will store mod key mappings + */ + private final File keyMapSettingsFile; + + /** + * Properties object which stores mod key mappings + */ + private final Properties keyMapSettings = new Properties(); + + /** + * List of all registered mod keys + */ + private final List modKeyBindings = new ArrayList(); + + /** + * Map of mod key bindings to their key codes, stored so that we don't need + * to cast from string in the properties file every tick. + */ + private final Map storedModKeyBindings = new HashMap(); + + /** + * JInput component registry + */ + private final ComponentRegistry jInputComponentRegistry; + + /** + * List of handlers for JInput components + */ + private final Map componentEvents = new HashMap(); + + /** + * JInput Controllers to poll + */ + private Controller[] pollControllers = new Controller[0]; + + /** + * + */ + public InputManager(LoaderEnvironment environment, LoaderProperties properties) + { + if (LiteLoader.getInstance() != null && LiteLoader.getInput() != null) + { + throw new IllegalStateException("Only one instance of Input is allowed, use LiteLoader.getInput() to get the active instance"); + } + + this.keyMapSettingsFile = new File(environment.getCommonConfigFolder(), "liteloader.keys.properties"); + this.jInputComponentRegistry = new ComponentRegistry(); + + if (!properties.getAndStoreBooleanProperty(LoaderProperties.OPTION_JINPUT_DISABLE, false)) + { + this.jInputComponentRegistry.enumerate(); + } + } + + @Override + public void onInit() + { + if (this.keyMapSettingsFile.exists()) + { + try + { + this.keyMapSettings.load(new FileReader(this.keyMapSettingsFile)); + } + catch (Exception ex) {} + } + } + + @Override + public void onPostInit(GameEngine engine) + { + this.engine = engine; + this.profiler = engine.getProfiler(); + } + + /** + * Register a key for a mod + * + * @param binding + */ + @Override + public void registerKeyBinding(KeyBinding binding) + { + List keyBindings = this.engine.getKeyBindings(); + + if (!keyBindings.contains(binding)) + { + if (this.keyMapSettings.containsKey(binding.getKeyDescription())) + { + try + { + int code = Integer.parseInt(this.keyMapSettings.getProperty(binding.getKeyDescription(), String.valueOf(binding.getKeyCode()))); + binding.setKeyCode(code); + } + catch (NumberFormatException ex) {} + } + + keyBindings.add(binding); + + this.engine.setKeyBindings(keyBindings); + this.modKeyBindings.add(binding); + + this.updateBinding(binding); + this.storeBindings(); + + KeyBinding.resetKeyBindingArrayAndHash(); + } + } + + /** + * Unregisters a registered keybind with the game settings class, thus + * removing it from the "controls" screen. + * + * @param binding + */ + @Override + public void unRegisterKeyBinding(KeyBinding binding) + { + List keyBindings = this.engine.getKeyBindings(); + + if (keyBindings.contains(binding)) + { + keyBindings.remove(binding); + this.engine.setKeyBindings(keyBindings); + + this.modKeyBindings.remove(binding); + + KeyBinding.resetKeyBindingArrayAndHash(); + } + } + + /** + * Checks for changed mod keybindings and stores any that have changed + */ + @Override + public void onTick(boolean clock, float partialTicks, boolean inGame) + { + this.profiler.startSection("keybindings"); + if (clock) + { + boolean updated = false; + + for (KeyBinding binding : this.modKeyBindings) + { + if (binding.getKeyCode() != this.storedModKeyBindings.get(binding)) + { + this.updateBinding(binding); + updated = true; + } + } + + if (updated) this.storeBindings(); + } + + this.pollControllers(); + this.profiler.endSection(); + } + + /** + * @param binding + */ + private void updateBinding(KeyBinding binding) + { + this.keyMapSettings.setProperty(binding.getKeyDescription(), String.valueOf(binding.getKeyCode())); + this.storedModKeyBindings.put(binding, Integer.valueOf(binding.getKeyCode())); + } + + @Override + public void onShutDown() + { + this.storeBindings(); + } + + /** + * Writes mod bindings to disk + */ + @Override + public void storeBindings() + { + try + { + this.keyMapSettings.store(new FileWriter(this.keyMapSettingsFile), + "Mod key mappings for LiteLoader mods, stored here to avoid losing settings stored in options.txt"); + } + catch (IOException ex) {} + } + + /** + * Gets the underlying JInput component registry + */ + @Override + public ComponentRegistry getComponentRegistry() + { + return this.jInputComponentRegistry; + } + + /** + * Returns a handle to the event described by descriptor (or null if no + * component is found matching the descriptor. Retrieving an event via this + * method adds the controller (if found) to the polling list and causes it + * to raise events against the specified handler. + * + *

      This method returns an {@link InputEvent} which is passed as an + * argument to the relevant callback on the supplied handler in order to + * identify the event. For example:

      + * + * this.joystickButton = input.getEvent(descriptor, this); + * + *

      then in onAxisEvent

      + * + * if (source == this.joystickButton) // do something with button + * + * + * @param descriptor + * @param handler + */ + @Override + public InputEvent getEvent(String descriptor, InputHandler handler) + { + if (handler == null) return null; + Component component = this.jInputComponentRegistry.getComponent(descriptor); + Controller controller = this.jInputComponentRegistry.getController(descriptor); + return this.addEventHandler(controller, component, handler); + } + + /** + * Get events for all components which match the supplied descriptor + * + * @param descriptor + * @param handler + */ + @Override + public InputEvent[] getEvents(String descriptor, InputHandler handler) + { + List events = new ArrayList(); + Controller controller = this.jInputComponentRegistry.getController(descriptor); + if (controller != null) + { + for (Component component : controller.getComponents()) + { + events.add(this.addEventHandler(controller, component, handler)); + } + } + + return events.toArray(new InputEvent[0]); + } + + /** + * @param controller + * @param component + * @param handler + */ + private InputEvent addEventHandler(Controller controller, Component component, InputHandler handler) + { + if (controller != null && component != null && handler != null) + { + this.addController(controller); + + InputEvent event = new InputEvent(controller, component, handler); + this.componentEvents.put(component, event.link(this.componentEvents.get(component))); + + return event; + } + + return null; + } + + /** + * @param controller + */ + private void addController(Controller controller) + { + Set controllers = this.getActiveControllers(); + controllers.add(controller); + this.setActiveControllers(controllers); + } + + /** + * + */ + private Set getActiveControllers() + { + Set allControllers = new HashSet(); + for (Controller controller : this.pollControllers) + allControllers.add(controller); + return allControllers; + } + + /** + * @param controllers + */ + private void setActiveControllers(Set controllers) + { + this.pollControllers = controllers.toArray(new Controller[controllers.size()]); + } + + /** + * + */ + private void pollControllers() + { + for (Controller controller : this.pollControllers) + { + controller.poll(); + EventQueue controllerQueue = controller.getEventQueue(); + + for (Event event = new Event(); controllerQueue.getNextEvent(event); ) + { + Component cmp = event.getComponent(); + + InputEvent inputEvent = this.componentEvents.get(cmp); + if (inputEvent != null) + { + inputEvent.onEvent(event); + } + } + } + } +} diff --git a/liteloader/src/client/java/com/mumfrey/liteloader/util/ModUtilities.java b/liteloader/src/client/java/com/mumfrey/liteloader/util/ModUtilities.java new file mode 100644 index 00000000..e28fb761 --- /dev/null +++ b/liteloader/src/client/java/com/mumfrey/liteloader/util/ModUtilities.java @@ -0,0 +1,253 @@ +package com.mumfrey.liteloader.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import org.lwjgl.LWJGLException; +import org.lwjgl.opengl.Display; +import org.lwjgl.opengl.DisplayMode; + +import com.mumfrey.liteloader.client.ducks.*; +import com.mumfrey.liteloader.client.overlays.IMinecraft; +import com.mumfrey.liteloader.client.util.PrivateFieldsClient; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +import net.minecraft.block.Block; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer; +import net.minecraft.entity.Entity; +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; +import net.minecraft.item.Item; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.RegistrySimple; +import net.minecraft.util.ResourceLocation; + +/** + * A small collection of useful functions for mods + * + * @author Adam Mummery-Smith + */ +public abstract class ModUtilities +{ + /** + * @return true if FML is present in the current environment + */ + public static boolean fmlIsPresent() + { + return ObfuscationUtilities.fmlIsPresent(); + } + + public static void setWindowSize(int width, int height) + { + try + { + Minecraft mc = Minecraft.getMinecraft(); + Display.setDisplayMode(new DisplayMode(width, height)); + ((IMinecraft)mc).onResizeWindow(width, height); + Display.setVSyncEnabled(mc.gameSettings.enableVsync); + } + catch (LWJGLException ex) + { + ex.printStackTrace(); + } + } + + /** + * Add a renderer map entry for the specified entity class + * + * @param entityClass + * @param renderer + */ + public static void addRenderer(Class entityClass, Render renderer) + { + RenderManager renderManager = Minecraft.getMinecraft().getRenderManager(); + + Map, Render> entityRenderMap = ((IRenderManager)renderManager).getRenderMap(); + if (entityRenderMap != null) + { + entityRenderMap.put(entityClass, renderer); + } + else + { + LiteLoaderLogger.warning("Attempted to set renderer %s for entity class %s but the operation failed", + renderer.getClass().getSimpleName(), entityClass.getSimpleName()); + } + } + + public static void addRenderer(Class tileEntityClass, TileEntitySpecialRenderer renderer) + { + TileEntityRendererDispatcher tileEntityRenderer = TileEntityRendererDispatcher.instance; + + try + { + Map, TileEntitySpecialRenderer> specialRendererMap + = ((ITileEntityRendererDispatcher)tileEntityRenderer).getSpecialRenderMap(); + specialRendererMap.put(tileEntityClass, renderer); + renderer.setRendererDispatcher(tileEntityRenderer); + } + catch (Exception ex) + { + LiteLoaderLogger.warning("Attempted to set renderer %s for tile entity class %s but the operation failed", + renderer.getClass().getSimpleName(), tileEntityClass.getSimpleName()); + } + } + + /** + * Add a block to the blocks registry + * + * @param blockId Block ID to insert + * @param blockName Block identifier + * @param block Block to register + * @param force Force insertion even if the operation is blocked by FMl + */ + public static void addBlock(int blockId, ResourceLocation blockName, Block block, boolean force) + { + Block existingBlock = (Block)Block.blockRegistry.getObject(blockName); + + try + { + Block.blockRegistry.register(blockId, blockName, block); + } + catch (IllegalArgumentException ex) + { + if (!force) throw new IllegalArgumentException("Could not register block '" + blockName + "', the operation was blocked by FML.", ex); + + ModUtilities.removeObjectFromRegistry(Block.blockRegistry, blockName); + Block.blockRegistry.register(blockId, blockName, block); + } + + if (existingBlock != null) + { + try + { + for (Field field : Blocks.class.getDeclaredFields()) + { + field.setAccessible(true); + if (field.isAccessible() && Block.class.isAssignableFrom(field.getType())) + { + Block fieldValue = (Block)field.get(null); + if (fieldValue == existingBlock) + { + ModUtilities.setFinalStaticField(field, block); + } + } + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + } + + /** + * Add an item to the items registry + * + * @param itemId Item ID to insert + * @param itemName Item identifier + * @param item Item to register + * @param force Force insertion even if the operation is blocked by FMl + */ + public static void addItem(int itemId, ResourceLocation itemName, Item item, boolean force) + { + Item existingItem = (Item)Item.itemRegistry.getObject(itemName); + + try + { + Item.itemRegistry.register(itemId, itemName, item); + } + catch (IllegalArgumentException ex) + { + if (!force) throw new IllegalArgumentException("Could not register item '" + itemName + "', the operation was blocked by FML.", ex); + + ModUtilities.removeObjectFromRegistry(Block.blockRegistry, itemName); + Item.itemRegistry.register(itemId, itemName, item); + } + + if (existingItem != null) + { + try + { + for (Field field : Items.class.getDeclaredFields()) + { + field.setAccessible(true); + if (field.isAccessible() && Item.class.isAssignableFrom(field.getType())) + { + Item fieldValue = (Item)field.get(null); + if (fieldValue == existingItem) + { + ModUtilities.setFinalStaticField(field, item); + } + } + } + } + catch (Exception ex) {} + } + } + + @SuppressWarnings("unchecked") + public static void addTileEntity(String entityName, Class tileEntityClass) + { + try + { + Map> nameToClassMap = PrivateFieldsClient.tileEntityNameToClassMap.get(null); + Map, String> classToNameMap = PrivateFieldsClient.tileEntityClassToNameMap.get(null); + nameToClassMap.put(entityName, tileEntityClass); + classToNameMap.put(tileEntityClass, entityName); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + private static V removeObjectFromRegistry(RegistrySimple registry, K key) + { + if (registry == null) return null; + + IObjectIntIdentityMap underlyingIntegerMap = null; + + if (registry instanceof INamespacedRegistry) + { + underlyingIntegerMap = ((INamespacedRegistry)registry).getUnderlyingMap(); + } + + Map registryObjects = ((IRegistrySimple)registry).getRegistryObjects(); + if (registryObjects != null) + { + V existingValue = registryObjects.get(key); + if (existingValue != null) + { + registryObjects.remove(key); + + if (underlyingIntegerMap != null) + { + IdentityHashMap identityMap = underlyingIntegerMap.getIdentityMap(); + List objectList = underlyingIntegerMap.getObjectList(); + if (identityMap != null) identityMap.remove(existingValue); + if (objectList != null) objectList.remove(existingValue); + } + + return existingValue; + } + } + + return null; + } + + private static void setFinalStaticField(Field field, Object value) + throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException + { + Field modifiers = Field.class.getDeclaredField("modifiers"); + modifiers.setAccessible(true); + modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, value); + } +} diff --git a/liteloader/src/client/resources/mixins.liteloader.client.json b/liteloader/src/client/resources/mixins.liteloader.client.json new file mode 100644 index 00000000..bed9cc0b --- /dev/null +++ b/liteloader/src/client/resources/mixins.liteloader.client.json @@ -0,0 +1,23 @@ +{ + "required": true, + "minVersion": "0.4.10", + "package": "com.mumfrey.liteloader.client.mixin", + "refmap": "mixins.liteloader.client.refmap.json", + "mixins": [ + "MixinMinecraft", + "MixinSession", + "MixinEntityRenderer", + "MixinRenderManager", + "MixinGuiIngame", + "MixinEntityPlayerSP", + "MixinFramebuffer", + "MixinIntegratedServer", + "MixinScreenShotHelper", + "MixinRealmsMainScreen", + "MixinNetHandlerLoginClient", + "MixinRegistrySimple", + "MixinRegistryNamespaced", + "MixinTileEntityRendererDispatcher", + "MixinSimpleReloadableResourceManager" + ] +} \ No newline at end of file diff --git a/liteloader/src/debug/java/com/mumfrey/liteloader/debug/LoginManager.java b/liteloader/src/debug/java/com/mumfrey/liteloader/debug/LoginManager.java new file mode 100644 index 00000000..7c56e93a --- /dev/null +++ b/liteloader/src/debug/java/com/mumfrey/liteloader/debug/LoginManager.java @@ -0,0 +1,458 @@ +package com.mumfrey.liteloader.debug; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import javax.swing.JOptionPane; + +import com.google.common.base.Strings; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.annotations.SerializedName; +import com.mojang.authlib.Agent; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.UserType; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.exceptions.InvalidCredentialsException; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; +import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Manages login requests against Yggdrasil for use in MCP + * + * @author Adam Mummery-Smith + */ +public class LoginManager +{ + /** + * Gson instance for serialising and deserialising the authentication data + */ + private static Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + /** + * Authentication service + */ + private YggdrasilAuthenticationService authService; + + /** + * Authentication agent + */ + private YggdrasilUserAuthentication authentication; + + /** + * JSON file to load/save auth data from + */ + private File jsonFile; + + /** + * Username read from the auth JSON file, we use this as the default in the + * login dialog in case login fails. This is stored in the JSON even if + * authentication is not successful so that we can display the same username + * next time. + */ + private String defaultUsername; + + /** + * Minecraft screen name read from the auth JSON file. Use this as default + * in case the login fails or is skipped (when offline) so that at least the + * Minecraft client has a sensible display name. + * + *

      Defaults to user.name when not specified

      + */ + private String defaultDisplayName = System.getProperty("user.name"); + + /** + * True if login should not be attempted, skips the authentication attempt + * and the login dialog. + */ + private boolean offline = false; + + /** + * If authentication fails with token then the first attempt will be to use + * the user/pass specified on the command line (if any). This flag is set + * after that first attempt so that we know to display the login + * dialog anyway (eg. the login on the command line was bad). + */ + private boolean forceShowLoginDialog = false; + + /** + * ctor + * + * @param jsonFile + */ + public LoginManager(File jsonFile) + { + this.jsonFile = jsonFile; + + this.resetAuth(); + this.load(); + } + + /** + * When authenticaion fails, we regenerate the auth service and agent + * because trying again with the same client data will fail. + */ + public void resetAuth() + { + this.authService = new YggdrasilAuthenticationService(Proxy.NO_PROXY, UUID.randomUUID().toString()); + this.authentication = new YggdrasilUserAuthentication(this.authService, Agent.MINECRAFT); + } + + /** + * Load auth data from the json file + */ + private void load() + { + if (this.jsonFile != null && this.jsonFile.exists()) + { + FileReader fileReader = null; + + try + { + fileReader = new FileReader(this.jsonFile); + AuthData authData = LoginManager.gson.fromJson(fileReader, AuthData.class); + + if (authData != null && authData.validate()) + { + LiteLoaderLogger.info("Initialising Yggdrasil authentication service with client token: %s", authData.getClientToken()); + this.authService = new YggdrasilAuthenticationService(Proxy.NO_PROXY, authData.getClientToken()); + this.authentication = new YggdrasilUserAuthentication(this.authService, Agent.MINECRAFT); + authData.loadFromStorage(this.authentication); + this.offline = authData.workOffline(); + this.defaultUsername = authData.getUsername(); + this.defaultDisplayName = authData.getDisplayName(); + } + } + catch (IOException ex) {} + finally + { + try + { + if (fileReader != null) fileReader.close(); + } + catch (IOException ex) {} + } + } + } + + /** + * Save auth data to the JSON file + */ + private void save() + { + if (this.jsonFile != null) + { + FileWriter fileWriter = null; + + try + { + fileWriter = new FileWriter(this.jsonFile); + + AuthData authData = new AuthData(this.authService, this.authentication, this.offline, this.defaultUsername, this.defaultDisplayName); + LoginManager.gson.toJson(authData, fileWriter); + } + catch (IOException ex) + { + ex.printStackTrace(); + } + finally + { + try + { + if (fileWriter != null) fileWriter.close(); + } + catch (IOException ex) + { + ex.printStackTrace(); + } + } + } + } + + /** + * Attempt to login. If authentication data are found on disk then tries + * first to log in with the stored token. If the token login fails then + * attempts to log in with the username and password specified. If no user + * or pass are specified or if they fail then displays a login dialog to + * allow the user to login. If login succeeds then the token is stored on + * disk and the method returns. + * + *

      If the user presses cancel in the login dialog then the method returns + * false.

      + * + * @param username User name to log in with if token login fails, if null + * displays the login dialog immediately + * @param password Password to log in with if token login fails, if null + * displays the login dialog immediately + * @param remainingTries Number of loops to go through before giving up, + * decremented for each try, specify -1 for unlimited + * @return false if the user presses cancel in the login dialog, otherwise + * returns true + */ + public boolean login(String username, String password, int remainingTries) + { + if (this.offline || remainingTries == 0) + { + LiteLoaderLogger.info("LoginManager is set to work offline, skipping login"); + return false; + } + + LiteLoaderLogger.info("Remaining login tries: %s", remainingTries > 0 ? remainingTries : "unlimited"); + + try + { + LiteLoaderLogger.info("Attempting login, contacting Mojang auth servers..."); + + this.authentication.logIn(); + + if (this.authentication.isLoggedIn()) + { + LiteLoaderLogger.info("LoginManager logged in successfully. Can play online = %s", this.authentication.canPlayOnline()); + this.save(); + return true; + } + + LiteLoaderLogger.info("LoginManager failed to log in, unspecified status."); + } + catch (InvalidCredentialsException ex) + { + LiteLoaderLogger.info("Authentication agent reported invalid credentials: %s", ex.getMessage()); + this.resetAuth(); + + if (remainingTries > 1) + { + if (username == null) + { + username = this.defaultUsername; + } + + if (this.forceShowLoginDialog || username == null || password == null) + { + LoginPanel loginPanel = LoginPanel.getLoginPanel(username, password, this.forceShowLoginDialog ? ex.getMessage() : null); + boolean dialogResult = loginPanel.showModalDialog(); + this.offline = loginPanel.workOffline(); + this.defaultUsername = loginPanel.getUsername(); + + if (!dialogResult) + { + LiteLoaderLogger.info("User cancelled login dialog"); + return false; + } + + if (this.offline) + { + if (JOptionPane.showConfirmDialog(null, "You have chosen to work offline. " + + "You will never be prompted to log in again.

      " + + "If you would like to re-enable login please delete the file .auth.json " + + "from the working dir
      " + + "or press Cancel to return to the login dialog.", + "Confirm work offline", + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.INFORMATION_MESSAGE) == JOptionPane.CANCEL_OPTION) + { + this.offline = false; + remainingTries = Math.max(remainingTries, 3); + } + } + + username = loginPanel.getUsername(); + password = loginPanel.getPassword(); + this.save(); + } + + if (!Strings.isNullOrEmpty(username) && !Strings.isNullOrEmpty(password)) + { + this.authentication.setUsername(username); + this.authentication.setPassword(password); + } + + this.forceShowLoginDialog = true; + this.login(username, password, --remainingTries); + } + } + catch (AuthenticationException ex) + { + ex.printStackTrace(); + } + + this.save(); + return false; + } + + /** + * Get whether user logged in + */ + public boolean isLoggedIn() + { + return this.authentication.isLoggedIn(); + } + + /** + * Get whether we are able to play online or not + */ + public boolean canPlayOnline() + { + return this.authentication.canPlayOnline(); + } + + /** + * Get the profile name (minecraft player name) from login + */ + public String getProfileName() + { + GameProfile selectedProfile = this.authentication.getSelectedProfile(); + return selectedProfile != null ? selectedProfile.getName() : this.defaultDisplayName; + } + + /** + * Get the profile name (minecraft player name) from login + */ + public String getUUID() + { + GameProfile selectedProfile = this.authentication.getSelectedProfile(); + return selectedProfile != null ? selectedProfile.getId().toString().replace("-", "") : this.defaultDisplayName; + } + + /** + * Get the session token + */ + public String getAuthenticatedToken() + { + String accessToken = this.authentication.getAuthenticatedToken(); + return accessToken != null ? accessToken : "-"; + } + + public String getUserType() + { + UserType userType = this.authentication.getUserType(); + return (userType != null ? userType : UserType.LEGACY).toString().toLowerCase(); + } + + public String getUserProperties() + { + PropertyMap userProperties = this.authentication.getUserProperties(); + return userProperties != null ? (new GsonBuilder()).registerTypeAdapter(PropertyMap.class, + new UserPropertiesSerializer()).create().toJson(userProperties) : "{}"; + } + + class UserPropertiesSerializer implements JsonSerializer + { + @Override + public JsonElement serialize(PropertyMap propertyMap, Type argType, JsonSerializationContext context) + { + JsonObject result = new JsonObject(); + + for (String key : propertyMap.keySet()) + { + JsonArray values = new JsonArray(); + for (Property property : propertyMap.get(key)) + { + values.add(new JsonPrimitive(property.getValue())); + } + + result.add(key, values); + } + + return result; + } + } + + + /** + * Struct for Gson serialisation of authenticaion settings + * + * @author Adam Mummery-Smith + */ + class AuthData + { + @SerializedName("clientToken") + private String clientToken; + + @SerializedName("workOffline") + private boolean workOffline; + + @SerializedName("authData") + private Map credentials; + + public AuthData() + { + // default ctor for Gson + } + + public AuthData(YggdrasilAuthenticationService authService, YggdrasilUserAuthentication authentication, boolean workOffline, + String defaultUserName, String defaultDisplayName) + { + this.clientToken = authService.getClientToken(); + this.credentials = authentication.saveForStorage(); + this.workOffline = workOffline; + + if (defaultUserName != null && !this.credentials.containsKey("username")) + { + this.credentials.put("username", defaultUserName); + } + + if (defaultDisplayName != null && !this.credentials.containsKey("displayName")) + { + this.credentials.put("displayName", defaultDisplayName); + } + } + + /** + * Called after Gson deserialisation to check that deserialisation was + * successful. + */ + public boolean validate() + { + if (this.clientToken == null) this.clientToken = UUID.randomUUID().toString(); + if (this.credentials == null) this.credentials = new HashMap(); + return true; + } + + public String getClientToken() + { + return this.clientToken; + } + + public void setClientToken(String clientToken) + { + this.clientToken = clientToken; + } + + public void loadFromStorage(YggdrasilUserAuthentication authentication) + { + authentication.loadFromStorage(this.credentials); + } + + public boolean workOffline() + { + return this.workOffline; + } + + public String getUsername() + { + return this.credentials != null ? this.credentials.get("username").toString() : null; + } + + public String getDisplayName() + { + return this.credentials != null && this.credentials.containsKey("displayName") + ? this.credentials.get("displayName").toString() : System.getProperty("user.name"); + } + } +} diff --git a/liteloader/src/debug/java/com/mumfrey/liteloader/debug/LoginPanel.java b/liteloader/src/debug/java/com/mumfrey/liteloader/debug/LoginPanel.java new file mode 100644 index 00000000..5614e8ba --- /dev/null +++ b/liteloader/src/debug/java/com/mumfrey/liteloader/debug/LoginPanel.java @@ -0,0 +1,366 @@ +package com.mumfrey.liteloader.debug; + +import static javax.swing.WindowConstants.*; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; + +/** + * JPanel displayed in a JDialog to prompt the user for login credentials for + * minecraft. + * + * @author Adam Mummery-Smith + */ +public class LoginPanel extends JPanel +{ + private static final long serialVersionUID = 1L; + + private GridBagLayout panelLoginLayout; + + private JPanel panelTitle; + private JPanel panelCentre; + private JPanel panelPadding; + private JPanel panelBottom; + private JLabel lblTitle; + private JLabel lblSubTitle; + private JLabel lblMessage; + private JLabel lblUserName; + private JLabel lblPassword; + private TextField txtUsername; + private TextField txtPassword; + private JButton btnLogin; + private JButton btnCancel; + private JCheckBox chkOffline; + + private JDialog dialog; + + private ListFocusTraversal tabOrder = new ListFocusTraversal(); + + private boolean dialogResult = false; + + public LoginPanel(String username, String password, String error) + { + Color backColour = new Color(102, 118, 144); + + this.setFocusable(false); + this.setPreferredSize(new Dimension(400, 260)); + this.setBackground(new Color(105, 105, 105)); + this.setLayout(new BorderLayout(0, 0)); + + this.panelTitle = new JPanel(); + this.panelTitle.setBackground(backColour); + this.panelTitle.setPreferredSize(new Dimension(400, 64)); + this.panelTitle.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5)); + + this.panelBottom = new JPanel(); + this.panelBottom.setBackground(backColour); + this.panelBottom.setPreferredSize(new Dimension(400, 32)); + this.panelBottom.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5)); + + this.panelPadding = new JPanel(); + this.panelPadding.setBorder(new EmptyBorder(4, 8, 8, 8)); + this.panelPadding.setOpaque(false); + this.panelPadding.setLayout(new BorderLayout(0, 0)); + + this.panelCentre = new JPanel(); + this.panelCentre.setOpaque(false); + this.panelCentre.setBorder(new TitledBorder(UIManager.getBorder("TitledBorder.border"), "Yggdrasil Login", + TitledBorder.LEADING, TitledBorder.TOP, null, Color.WHITE)); + this.panelLoginLayout = new GridBagLayout(); + this.panelLoginLayout.columnWidths = new int[] {30, 80, 120, 120, 30}; + this.panelLoginLayout.rowHeights = new int[] {24, 32, 32, 32}; + this.panelLoginLayout.columnWeights = new double[]{0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE}; + this.panelLoginLayout.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0}; + this.panelCentre.setLayout(this.panelLoginLayout); + + this.lblTitle = new JLabel("Log in to minecraft.net"); + this.lblTitle.setBorder(new EmptyBorder(4, 16, 0, 16)); + this.lblTitle.setFont(new Font("Tahoma", Font.BOLD, 18)); + this.lblTitle.setForeground(Color.WHITE); + this.lblTitle.setPreferredSize(new Dimension(400, 26)); + + this.lblSubTitle = new JLabel("Your password will not be stored, logging in with Yggdrasil"); + this.lblSubTitle.setBorder(new EmptyBorder(0, 16, 0, 16)); + this.lblSubTitle.setForeground(Color.WHITE); + this.lblSubTitle.setPreferredSize(new Dimension(400, 16)); + + this.lblMessage = new JLabel("Enter your login details for minecraft.net"); + this.lblMessage.setForeground(Color.WHITE); + + this.lblUserName = new JLabel("User name"); + this.lblUserName.setForeground(Color.WHITE); + + this.lblPassword = new JLabel("Password"); + this.lblPassword.setForeground(Color.WHITE); + + this.txtUsername = new TextField(); + this.txtUsername.setPreferredSize(new Dimension(200, 22)); + this.txtUsername.setText(username); + + this.txtPassword = new TextField(); + this.txtPassword.setEchoChar('*'); + this.txtPassword.setPreferredSize(new Dimension(200, 22)); + this.txtPassword.setText(password); + + this.btnLogin = new JButton("Log in"); + this.btnLogin.addActionListener(new ActionListener() + { + @Override public void actionPerformed(ActionEvent e) + { + LoginPanel.this.onLoginClick(); + } + }); + + this.btnCancel = new JButton("Cancel"); + this.btnCancel.addActionListener(new ActionListener() + { + @Override public void actionPerformed(ActionEvent e) + { + LoginPanel.this.onCancelClick(); + } + }); + + this.chkOffline = new JCheckBox("Never ask me to log in (always run offline)"); + this.chkOffline.setPreferredSize(new Dimension(380, 23)); + this.chkOffline.setForeground(Color.WHITE); + this.chkOffline.setOpaque(false); + this.chkOffline.addActionListener(new ActionListener() + { + @Override public void actionPerformed(ActionEvent e) + { + LoginPanel.this.onOfflineCheckedChanged(); + } + }); + + GridBagConstraints lblMessageConstraints = new GridBagConstraints(); + lblMessageConstraints.anchor = GridBagConstraints.WEST; + lblMessageConstraints.gridwidth = 2; + lblMessageConstraints.insets = new Insets(0, 0, 5, 5); + lblMessageConstraints.gridx = 2; + lblMessageConstraints.gridy = 0; + + GridBagConstraints lblUserNameConstraints = new GridBagConstraints(); + lblUserNameConstraints.anchor = GridBagConstraints.WEST; + lblUserNameConstraints.fill = GridBagConstraints.VERTICAL; + lblUserNameConstraints.insets = new Insets(0, 0, 5, 5); + lblUserNameConstraints.gridx = 1; + lblUserNameConstraints.gridy = 1; + + GridBagConstraints lblPasswordConstraints = new GridBagConstraints(); + lblPasswordConstraints.anchor = GridBagConstraints.WEST; + lblPasswordConstraints.fill = GridBagConstraints.VERTICAL; + lblPasswordConstraints.insets = new Insets(0, 0, 5, 5); + lblPasswordConstraints.gridx = 1; + lblPasswordConstraints.gridy = 2; + + GridBagConstraints txtUsernameConstraints = new GridBagConstraints(); + txtUsernameConstraints.gridwidth = 2; + txtUsernameConstraints.fill = GridBagConstraints.HORIZONTAL; + txtUsernameConstraints.insets = new Insets(0, 0, 5, 0); + txtUsernameConstraints.gridx = 2; + txtUsernameConstraints.gridy = 1; + + GridBagConstraints txtPasswordConstraints = new GridBagConstraints(); + txtPasswordConstraints.gridwidth = 2; + txtPasswordConstraints.insets = new Insets(0, 0, 5, 0); + txtPasswordConstraints.fill = GridBagConstraints.HORIZONTAL; + txtPasswordConstraints.gridx = 2; + txtPasswordConstraints.gridy = 2; + + GridBagConstraints btnLoginConstraints = new GridBagConstraints(); + btnLoginConstraints.fill = GridBagConstraints.HORIZONTAL; + btnLoginConstraints.gridx = 3; + btnLoginConstraints.gridy = 3; + + GridBagConstraints btnCancelConstraints = new GridBagConstraints(); + btnCancelConstraints.anchor = GridBagConstraints.EAST; + btnCancelConstraints.insets = new Insets(0, 0, 0, 5); + btnCancelConstraints.gridx = 2; + btnCancelConstraints.gridy = 3; + + this.add(this.panelTitle, BorderLayout.NORTH); + this.add(this.panelPadding, BorderLayout.CENTER); + this.add(this.panelBottom, BorderLayout.SOUTH); + + this.panelPadding.add(this.panelCentre); + + this.panelTitle.add(this.lblTitle); + this.panelTitle.add(this.lblSubTitle); + + this.panelCentre.add(this.lblMessage, lblMessageConstraints); + this.panelCentre.add(this.lblUserName, lblUserNameConstraints); + this.panelCentre.add(this.lblPassword, lblPasswordConstraints); + this.panelCentre.add(this.txtUsername, txtUsernameConstraints); + this.panelCentre.add(this.txtPassword, txtPasswordConstraints); + this.panelCentre.add(this.btnLogin, btnLoginConstraints); + this.panelCentre.add(this.btnCancel, btnCancelConstraints); + + this.panelBottom.add(this.chkOffline); + + this.tabOrder.add(this.txtUsername); + this.tabOrder.add(this.txtPassword); + this.tabOrder.add(this.btnLogin); + this.tabOrder.add(this.btnCancel); + this.tabOrder.add(this.chkOffline); + + if (error != null) + { + this.lblMessage.setText(error); + this.lblMessage.setForeground(new Color(255, 180, 180)); + } + } + + protected void onShowDialog() + { + if (this.txtUsername.getText().length() > 0) + { + if (this.txtPassword.getText().length() > 0) + { + this.txtUsername.select(0, this.txtUsername.getText().length()); + } + else + { + this.txtPassword.requestFocusInWindow(); + } + } + } + + protected void onLoginClick() + { + this.dialogResult = true; + this.dialog.setVisible(false); + } + + protected void onCancelClick() + { + this.dialog.setVisible(false); + } + + protected void onOfflineCheckedChanged() + { + boolean selected = this.chkOffline.isSelected(); + this.btnLogin.setText(selected ? "Work Offline" : "Log In"); + this.txtUsername.setEnabled(!selected); + this.txtPassword.setEnabled(!selected); + } + + /** + * @param dialog + */ + public void setDialog(JDialog dialog) + { + this.dialog = dialog; + + this.dialog.addWindowListener(new WindowAdapter() + { + @Override + public void windowOpened(WindowEvent e) + { + LoginPanel.this.onShowDialog(); + } + }); + + this.dialog.getRootPane().setDefaultButton(this.btnLogin); + this.dialog.setFocusTraversalPolicy(this.tabOrder); + } + + public boolean showModalDialog() + { + this.dialog.setVisible(true); + this.dialog.dispose(); + return this.dialogResult; + } + + public String getUsername() + { + return this.txtUsername.getText(); + } + + public String getPassword() + { + return this.txtPassword.getText(); + } + + public boolean workOffline() + { + return this.chkOffline.isSelected(); + } + + public static LoginPanel getLoginPanel(String username, String password, String error) + { + if (username == null) username = ""; + if (password == null) password = ""; + + final JDialog dialog = new JDialog(); + final LoginPanel panel = new LoginPanel(username, password, error); + panel.setDialog(dialog); + + dialog.setContentPane(panel); + dialog.setTitle("Yggdrasil Login"); + dialog.setResizable(false); + dialog.pack(); + dialog.setDefaultCloseOperation(DISPOSE_ON_CLOSE); + dialog.setLocationRelativeTo(null); + dialog.setModal(true); + + return panel; + } + + class ListFocusTraversal extends FocusTraversalPolicy + { + private final List components = new ArrayList(); + + void add(Component component) + { + this.components.add(component); + } + + @Override + public Component getComponentAfter(Container container, Component component) + { + int index = this.components.indexOf(component) + 1; + if (index >= this.components.size()) return this.components.get(0); + return this.components.get(index); + } + + @Override + public Component getComponentBefore(Container container, Component component) + { + int index = this.components.indexOf(component) - 1; + if (index < 0) return this.getLastComponent(container); + return this.components.get(index); + } + + @Override + public Component getFirstComponent(Container container) + { + return this.components.get(0); + } + + @Override + public Component getLastComponent(Container container) + { + return this.components.get(this.components.size() - 1); + } + + @Override + public Component getDefaultComponent(Container container) + { + return this.getFirstComponent(container); + } + } +} diff --git a/liteloader/src/debug/java/com/mumfrey/liteloader/debug/Start.java b/liteloader/src/debug/java/com/mumfrey/liteloader/debug/Start.java new file mode 100644 index 00000000..7e3240d3 --- /dev/null +++ b/liteloader/src/debug/java/com/mumfrey/liteloader/debug/Start.java @@ -0,0 +1,204 @@ +package com.mumfrey.liteloader.debug; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import net.minecraft.launchwrapper.Launch; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import com.mumfrey.liteloader.launch.LiteLoaderTweaker; +import com.mumfrey.liteloader.launch.LiteLoaderTweakerServer; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Wrapper class for LaunchWrapper Main class, which logs in using Yggdrasil + * first so that online shizzle can be tested. + * + * @author Adam Mummery-Smith + */ +public abstract class Start +{ + /** + * Number of times to retry Yggdrasil login + */ + private static final int LOGIN_RETRIES = 5; + + /** + * Arguments which are allowed to have multiple occurrences + */ + private static final Set MULTI_VALUE_ARGS = ImmutableSet.of( + "--tweakClass" + ); + + /** + * Entry point. + * + * @param args + */ + public static void main(String[] args) + { + System.setProperty("mcpenv", "true"); + Launch.main(Start.processArgs(args)); + } + + /** + * Process the launch-time args, since we may be being launched by + * GradleStart we need to parse out any values passed in and ensure we + * replace them with our own. + */ + private static String[] processArgs(String[] args) + { + List unqualifiedArgs = new ArrayList(); + Map> qualifiedArgs = new HashMap>(); + + Start.parseArgs(args, unqualifiedArgs, qualifiedArgs); + + if (Start.hasArg(unqualifiedArgs, "server")) + { + Start.addRequiredArgsServer(args, unqualifiedArgs, qualifiedArgs); + } + else + { + Start.addRequiredArgsClient(args, unqualifiedArgs, qualifiedArgs); + } + + args = Start.combineArgs(args, unqualifiedArgs, qualifiedArgs); + + return args; + } + + private static boolean hasArg(List args, String target) + { + for (String arg : args) + { + if (target.equalsIgnoreCase(arg)) + { + return true; + } + } + + return false; + } + + /** + * Read the args from the command line into the qualified and unqualified + * collections. + */ + private static void parseArgs(String[] args, List unqualifiedArgs, Map> qualifiedArgs) + { + String qualifier = null; + for (String arg : args) + { + boolean isQualifier = arg.startsWith("-"); + + if (isQualifier) + { + if (qualifier != null) unqualifiedArgs.add(qualifier); + qualifier = arg; + } + else if (qualifier != null) + { + Start.addArg(qualifiedArgs, qualifier, arg); + qualifier = null; + } + else + { + unqualifiedArgs.add(arg); + } + } + + if (qualifier != null) unqualifiedArgs.add(qualifier); + } + + private static void addRequiredArgsClient(String[] args, List unqualifiedArgs, Map> qualifiedArgs) + { + LoginManager loginManager = Start.doLogin(qualifiedArgs); + + File gameDir = new File(System.getProperty("user.dir")); + File assetsDir = new File(gameDir, "assets"); + + Start.addArg(qualifiedArgs, "--tweakClass", LiteLoaderTweaker.class.getName()); + Start.addArg(qualifiedArgs, "--username", loginManager.getProfileName()); + Start.addArg(qualifiedArgs, "--uuid", loginManager.getUUID()); + Start.addArg(qualifiedArgs, "--accessToken", loginManager.getAuthenticatedToken()); + Start.addArg(qualifiedArgs, "--userType", loginManager.getUserType()); + Start.addArg(qualifiedArgs, "--userProperties", loginManager.getUserProperties()); + Start.addArg(qualifiedArgs, "--version", "mcp"); + Start.addArg(qualifiedArgs, "--gameDir", gameDir.getAbsolutePath()); + Start.addArg(qualifiedArgs, "--assetIndex", LiteLoaderTweaker.VERSION); + Start.addArg(qualifiedArgs, "--assetsDir", assetsDir.getAbsolutePath()); + } + + private static void addRequiredArgsServer(String[] args, List unqualifiedArgs, Map> qualifiedArgs) + { + File gameDir = new File(System.getProperty("user.dir")); + + Start.addArg(qualifiedArgs, "--tweakClass", LiteLoaderTweakerServer.class.getName()); + Start.addArg(qualifiedArgs, "--version", "mcp"); + Start.addArg(qualifiedArgs, "--gameDir", gameDir.getAbsolutePath()); + } + + private static LoginManager doLogin(Map> qualifiedArgs) + { + File loginJson = new File(new File(System.getProperty("user.dir")), ".auth.json"); + LoginManager loginManager = new LoginManager(loginJson); + + String usernameFromCmdLine = Start.getArg(qualifiedArgs, "--username"); + String passwordFromCmdLine = Start.getArg(qualifiedArgs, "--password"); + + loginManager.login(usernameFromCmdLine, passwordFromCmdLine, Start.LOGIN_RETRIES); + + LiteLoaderLogger.info("Launching game as %s", loginManager.getProfileName()); + + return loginManager; + } + + private static void addArg(Map> qualifiedArgs, String qualifier, String arg) + { + Set args = qualifiedArgs.get(qualifier); + + if (args == null) + { + args = new HashSet(); + qualifiedArgs.put(qualifier, args); + } + + if (!Start.MULTI_VALUE_ARGS.contains(qualifier)) + { + args.clear(); + } + + args.add(arg); + } + + private static String getArg(Map> qualifiedArgs, String arg) + { + if (qualifiedArgs.containsKey(arg)) + { + return qualifiedArgs.get(arg).iterator().next(); + } + + return null; + } + + private static String[] combineArgs(String[] args, List unqualifiedArgs, Map> qualifiedArgs) + { + for (Entry> qualifiedArg : qualifiedArgs.entrySet()) + { + for (String argValue : qualifiedArg.getValue()) + { + unqualifiedArgs.add(qualifiedArg.getKey()); + if (!Strings.isNullOrEmpty(argValue)) unqualifiedArgs.add(argValue); + } + } + + return unqualifiedArgs.toArray(args); + } +} diff --git a/liteloader/src/debug/resources/obfuscation.properties b/liteloader/src/debug/resources/obfuscation.properties new file mode 100644 index 00000000..e5d91592 --- /dev/null +++ b/liteloader/src/debug/resources/obfuscation.properties @@ -0,0 +1,69 @@ +field_71424_I=mcProfiler +field_78729_o=entityRenderMap +field_110546_b=reloadListeners +field_147393_d=networkManager +field_82596_a=registryObjects +field_148759_a=underlyingIntegerMap +field_148749_a=identityMap +field_148748_b=objectList +field_147559_m=mapSpecialRenderers +field_145855_i=nameToClassMap +field_145853_j=classToNameMap +field_71428_T=timer +field_71424_I=mcProfiler +field_71425_J=running +field_110449_ao=defaultResourcePacks +field_71475_ae=serverName +field_71477_af=serverPort +field_147712_ad=shaderResourceLocations +field_147713_ae=shaderIndex +field_175083_ad=useShader +field_149528_b=view +field_70163_u=posY +field_148919_a=chatComponent +func_148833_a=processPacket +func_71411_J=runGameLoop +func_71407_l=runTick +func_78480_b=updateCameraAndRender +func_78471_a=renderWorld +func_175180_a=renderGameOverlay +func_76320_a=startSection +func_76319_b=endSection +func_76318_c=endStartSection +func_148545_a=createPlayerForUser +func_72368_a=recreatePlayerEntity +func_72355_a=initializeConnectionToPlayer +func_72377_c=playerLoggedIn +func_72367_e=playerLoggedOut +func_71384_a=startGame +func_71197_b=startServer +func_71256_s=startServerThread +func_71165_d=sendChatMessage +func_147119_ah=updateFramebufferSize +func_147615_c=framebufferRender +func_178038_a=framebufferRenderExt +func_147612_c=bindFramebufferTexture +func_146230_a=drawChat +func_179086_m=clear +func_175068_a=renderWorldPass +func_148256_e=getProfile +func_148260_a=saveScreenshot +func_148822_b=isFramebufferEnabled +func_147939_a=doRenderEntity +func_76986_a=doRender +func_71370_a=resize +func_175069_a=loadShader +func_78481_a=getFOVModifier +func_78479_a=setupCameraTransform +func_147693_a=loadSoundResource +func_180784_a=onBlockClicked +func_180236_a=activateBlockOrUseItem +func_147346_a=processPlayerBlockPlacement +func_175087_a=handleAnimation +func_147345_a=processPlayerDigging +func_71190_q=updateTimeLightAndEntities +func_180031_a=checkThreadAndEnqueue +func_147347_a=processPlayer +func_174976_a=renderSky +func_180437_a=renderCloudsCheck +func_78468_a=setupFog \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/Configurable.java b/liteloader/src/main/java/com/mumfrey/liteloader/Configurable.java new file mode 100644 index 00000000..f73ac2de --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/Configurable.java @@ -0,0 +1,20 @@ +package com.mumfrey.liteloader; + +import com.mumfrey.liteloader.modconfig.ConfigPanel; + +/** + * Interface for mods which want to provide a configuration panel inside the + * "mod info" screen. + * + * @author Adam Mummery-Smith + */ +public interface Configurable +{ + /** + * Get the class of the configuration panel to use, the returned class must + * have a default (no-arg) constructor + * + * @return configuration panel class + */ + public abstract Class getConfigPanelClass(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/LiteMod.java b/liteloader/src/main/java/com/mumfrey/liteloader/LiteMod.java new file mode 100644 index 00000000..4df09ec4 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/LiteMod.java @@ -0,0 +1,40 @@ +package com.mumfrey.liteloader; + +import java.io.File; + +import com.mumfrey.liteloader.api.Listener; +import com.mumfrey.liteloader.modconfig.Exposable; + +/** + * Base interface for mods + * + * @author Adam Mummery-Smith + */ +public interface LiteMod extends Exposable, Listener +{ + /** + * Get the mod version string + * + * @return the mod version as a string + */ + public abstract String getVersion(); + + /** + * Do startup stuff here, minecraft is not fully initialised when this + * function is called so mods must not interact with minecraft in any + * way here. + * + * @param configPath Configuration path to use + */ + public abstract void init(File configPath); + + /** + * Called when the loader detects that a version change has happened since + * this mod was last loaded. + * + * @param version new version + * @param configPath Path for the new version-specific config + * @param oldConfigPath Path for the old version-specific config + */ + public abstract void upgradeSettings(String version, File configPath, File oldConfigPath); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/PacketHandler.java b/liteloader/src/main/java/com/mumfrey/liteloader/PacketHandler.java new file mode 100644 index 00000000..6c4ada21 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/PacketHandler.java @@ -0,0 +1,32 @@ +package com.mumfrey.liteloader; + +import java.util.List; + +import net.minecraft.network.INetHandler; +import net.minecraft.network.Packet; + +/** + * Interface for mods which want to handle raw packets + * + * @author Adam Mummery-Smith + */ +public interface PacketHandler extends LiteMod +{ + /** + * Get list of packets to handle + */ + public List> getHandledPackets(); + + /** + * @param netHandler The vanilla nethandler which will handle this packet if + * not cancelled + * @param packet Incoming packet + * @return True to allow further processing of this packet, including other + * PacketHandlers and eventually the vanilla netHandler, to inhibit + * further processing return false. You may choose to return false and + * then invoke the vanilla handler method on the supplied INetHandler + * if you wish to inhibit later PacketHandlers but preserve vanilla + * behaviour. + */ + public abstract boolean handlePacket(INetHandler netHandler, Packet packet); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/Permissible.java b/liteloader/src/main/java/com/mumfrey/liteloader/Permissible.java new file mode 100644 index 00000000..5b8a7cad --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/Permissible.java @@ -0,0 +1,55 @@ +package com.mumfrey.liteloader; + +import com.mumfrey.liteloader.permissions.PermissionsManager; +import com.mumfrey.liteloader.permissions.PermissionsManagerClient; + +/** + * Interface for mods which use the ClientPermissions system + * + * @author Adam Mummery-Smith + */ +public interface Permissible extends LiteMod +{ + /** + * Returns the node name of the mod, replicated permissions will be of the + * form mod..permission.node so this method must return a valid name + * for use in permission nodes. This method must also return the same value + * every time it is called since permissible names are not necessarily + * cached. + * + * @return Permissible name + */ + public abstract String getPermissibleModName(); + + /** + * The mod version to replicate to the server + * + * @return Mod version as a float + */ + public abstract float getPermissibleModVersion(); + + /** + * Called by the permissions manager at initialisation to instruct the mod + * to populate the list of permissions it supports. This method should call + * back against the supplied permissions manager to register the permissions + * to be sent to the server when connecting. + * + * @param permissionsManager Client permissions manager + */ + public abstract void registerPermissions(PermissionsManagerClient permissionsManager); + + /** + * Called when the permissions set is cleared + * + * @param manager + */ + public abstract void onPermissionsCleared(PermissionsManager manager); + + /** + * Called when the permissions are changed (eg. when new permissions are + * received from the server) + * + * @param manager + */ + public abstract void onPermissionsChanged(PermissionsManager manager); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/PlayerInteractionListener.java b/liteloader/src/main/java/com/mumfrey/liteloader/PlayerInteractionListener.java new file mode 100644 index 00000000..a63a1b4a --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/PlayerInteractionListener.java @@ -0,0 +1,58 @@ +package com.mumfrey.liteloader; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.util.BlockPos; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.MovingObjectPosition.MovingObjectType; + +/** + * Interface for mods which want to observe the player's "interaction" status + * (player mouse clicks), allows block interaction events to be cancelled. + * + * @author Adam Mummery-Smith + */ +public interface PlayerInteractionListener extends LiteMod +{ + /** + * Mouse buttons + */ + public static enum MouseButton + { + LEFT, + RIGHT + } + + /** + * Called when the player clicks but does not "hit" a block, the trace + * position is raytraced to the player's current view distance and + * represents the block which the player is "looking at". This method is + * not called when the player right clicks with an empty hand. + * + * @param player Player + * @param button Mouse button the user clicked + * @param tracePos Raytraced location of the block which was hit + * @param traceSideHit Raytraced side hit + * @param traceHitType Type of hit, will be MISS if the trace expired + * without hitting anything (eg. the player clicked the sky) + */ + public abstract void onPlayerClickedAir(EntityPlayerMP player, MouseButton button, BlockPos tracePos, EnumFacing traceSideHit, + MovingObjectType traceHitType); + + /** + * Calls when the player clicks and hits a block, usually indicates that the + * player is digging or placing a block, although a block placement does not + * necessarily succeed. Return true from this callback to allow the action + * to proceed, or false to cancel the action. Cancelling the action does not + * prevent further handlers from receiving the event. + * + * @param player Player + * @param button Mouse button that was pressed, left = dig, right = interact + * @param hitPos Block which was *hit*. Note that block placement will + * normally be at hitPos.offset(sideHit) + * @param sideHit Side of the block which was hit + * @return true to allow the action to be processed (another listener may + * still inhibit the action), return false to cancel the action (other + * listeners will still be notified) + */ + public abstract boolean onPlayerClickedBlock(EntityPlayerMP player, MouseButton button, BlockPos hitPos, EnumFacing sideHit); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/PlayerMoveListener.java b/liteloader/src/main/java/com/mumfrey/liteloader/PlayerMoveListener.java new file mode 100644 index 00000000..2ca8447e --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/PlayerMoveListener.java @@ -0,0 +1,27 @@ +package com.mumfrey.liteloader; + +import net.minecraft.entity.player.EntityPlayerMP; + +import com.mumfrey.liteloader.core.LiteLoaderEventBroker.ReturnValue; +import com.mumfrey.liteloader.util.Position; + +/** + * Interface for mods which want to monitor or control player movements + * + * @author Adam Mummery-Smith + */ +public interface PlayerMoveListener extends LiteMod +{ + /** + * Called when a movement/look packet is received from the client. + * + * @param playerMP Player moving + * @param from Player's previous recorded position + * @param to Position the player is attempting to move to + * @param newPos Set this position to teleport the player to newPos instead + * of processing the original move + * @return false to cancel the event or true to allow the movement to be + * processed as normal or newPos to be applied + */ + public abstract boolean onPlayerMove(EntityPlayerMP playerMP, Position from, Position to, ReturnValue newPos); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/PluginChannelListener.java b/liteloader/src/main/java/com/mumfrey/liteloader/PluginChannelListener.java new file mode 100644 index 00000000..1ddda3ba --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/PluginChannelListener.java @@ -0,0 +1,22 @@ +package com.mumfrey.liteloader; + +import net.minecraft.network.PacketBuffer; + +import com.mumfrey.liteloader.core.CommonPluginChannelListener; + +/** + * Interface for mods which want to use plugin channels + * + * @author Adam Mummery-Smith + */ +public interface PluginChannelListener extends LiteMod, CommonPluginChannelListener +{ + /** + * Called when a custom payload packet arrives on a channel this mod has + * registered. + * + * @param channel Channel on which the custom payload was received + * @param data Custom payload data + */ + public abstract void onCustomPayload(String channel, PacketBuffer data); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/PreJoinGameListener.java b/liteloader/src/main/java/com/mumfrey/liteloader/PreJoinGameListener.java new file mode 100644 index 00000000..54dec8cb --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/PreJoinGameListener.java @@ -0,0 +1,24 @@ +package com.mumfrey.liteloader; + +import net.minecraft.network.INetHandler; +import net.minecraft.network.play.server.S01PacketJoinGame; + + +/** + * Interface for mods which wish to be notified when the player connects to a + * server (or local game). + * + * @author Adam Mummery-Smith + */ +public interface PreJoinGameListener extends LiteMod +{ + /** + * Called before login. NOTICE: as of 1.8 the return value of this method + * has a different meaning! + * + * @param netHandler Net handler + * @param joinGamePacket Join game packet + * @return true to allow login to continue, false to cancel login + */ + public abstract boolean onPreJoinGame(INetHandler netHandler, S01PacketJoinGame joinGamePacket); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/Priority.java b/liteloader/src/main/java/com/mumfrey/liteloader/Priority.java new file mode 100644 index 00000000..5a0b24ac --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/Priority.java @@ -0,0 +1,22 @@ +package com.mumfrey.liteloader; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Priority declaration for LiteMods, used when sorting listener lists. Default + * value if no Priority annotation is specified is 1000. + * + * @author Adam Mummery-Smith + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Priority +{ + /** + * Priority value, default priority is 1000 + */ + public int value(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/ServerChatFilter.java b/liteloader/src/main/java/com/mumfrey/liteloader/ServerChatFilter.java new file mode 100644 index 00000000..15ea6676 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/ServerChatFilter.java @@ -0,0 +1,22 @@ +package com.mumfrey.liteloader; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.play.client.C01PacketChatMessage; + +/** + * Interface for mods which can filter inbound chat + * + * @author Adam Mummery-Smith + */ +public interface ServerChatFilter extends LiteMod +{ + /** + * Chat filter function, return false to filter this packet, true to pass + * the packet. + * + * @param chatPacket Chat packet to examine + * @param message Chat message + * @return True to keep the packet, false to discard + */ + public abstract boolean onChat(EntityPlayerMP player, C01PacketChatMessage chatPacket, String message); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/ServerCommandProvider.java b/liteloader/src/main/java/com/mumfrey/liteloader/ServerCommandProvider.java new file mode 100644 index 00000000..36a3c24e --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/ServerCommandProvider.java @@ -0,0 +1,21 @@ +package com.mumfrey.liteloader; + +import net.minecraft.command.ServerCommandManager; + + +/** + * Interface for mods which provide commands to the local integrated server + * + * @author Adam Mummery-Smith + */ +public interface ServerCommandProvider extends LiteMod +{ + /** + * Allows the mod to provide commands to the server command manager by + * invoking commandManager.registerCommand() to provide new commands for + * single player and lan worlds + * + * @param commandManager + */ + public abstract void provideCommands(ServerCommandManager commandManager); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/ServerPlayerListener.java b/liteloader/src/main/java/com/mumfrey/liteloader/ServerPlayerListener.java new file mode 100644 index 00000000..3ff7df24 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/ServerPlayerListener.java @@ -0,0 +1,51 @@ +package com.mumfrey.liteloader; + +import net.minecraft.entity.player.EntityPlayerMP; + +import com.mojang.authlib.GameProfile; + +/** + * Interface for mods which want to handle players joining and leaving a LAN + * game (or single player game) + * + * @author Adam Mummery-Smith + */ +public interface ServerPlayerListener extends LiteMod +{ + /** + * Called when a player connects to the server and the EntityPlayerMP + * instance is created, the player has not logged in at this point and may + * be disconnected if login fails. + * + * @param player Player attempting to connect + * @param profile Player's GameProfile from the authentication service + */ + public abstract void onPlayerConnect(EntityPlayerMP player, GameProfile profile); + + /** + * Called once the player has successfully logged in and all player + * variables are initialised and replicated. + * + * @param player Player connected + */ + public abstract void onPlayerLoggedIn(EntityPlayerMP player); + + /** + * Called when a player respawns. This event is raised when a player + * respawns after dying or conquers the end. + * + * @param player New player instance + * @param oldPlayer Old player instance being discarded + * @param newDimension Dimension the player is respawning in + * @param playerWonTheGame True if the player conquered the end (this + * respawn is NOT as the result of a death) + */ + public abstract void onPlayerRespawn(EntityPlayerMP player, EntityPlayerMP oldPlayer, int newDimension, boolean playerWonTheGame); + + /** + * Called when a player disconnects from the game + * + * @param player Player disconnecting + */ + public abstract void onPlayerLogout(EntityPlayerMP player); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/ServerPluginChannelListener.java b/liteloader/src/main/java/com/mumfrey/liteloader/ServerPluginChannelListener.java new file mode 100644 index 00000000..9ca14d47 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/ServerPluginChannelListener.java @@ -0,0 +1,25 @@ +package com.mumfrey.liteloader; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.PacketBuffer; + +import com.mumfrey.liteloader.core.CommonPluginChannelListener; + +/** + * Interface for mods which want to use plugin channels on the (integrated) + * server side. + * + * @author Adam Mummery-Smith + */ +public interface ServerPluginChannelListener extends CommonPluginChannelListener +{ + /** + * Called when a custom payload packet arrives on a channel this mod has + * registered. + * + * @param sender Player object which is the source of this message + * @param channel Channel on which the custom payload was received + * @param data Custom payload data + */ + public abstract void onCustomPayload(EntityPlayerMP sender, String channel, PacketBuffer data); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/ServerTickable.java b/liteloader/src/main/java/com/mumfrey/liteloader/ServerTickable.java new file mode 100644 index 00000000..61b1eee3 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/ServerTickable.java @@ -0,0 +1,18 @@ +package com.mumfrey.liteloader; + +import net.minecraft.server.MinecraftServer; + +/** + * Interface for mods which want to be ticked on the server thread + * + * @author Adam Mummery-Smith + */ +public interface ServerTickable extends LiteMod +{ + /** + * Called at the start of every server update tick + * + * @param server + */ + public abstract void onTick(MinecraftServer server); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/ShutdownListener.java b/liteloader/src/main/java/com/mumfrey/liteloader/ShutdownListener.java new file mode 100644 index 00000000..12c3efb4 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/ShutdownListener.java @@ -0,0 +1,13 @@ +package com.mumfrey.liteloader; + +/** + * Interface for mods that want to receive an event when the game is shutting + * down due to a user request. They do not receive the callback when the VM is + * terminating for other reasons, use a regular VM shutdownhook for that. + * + * @author Adam Mummery-Smith + */ +public interface ShutdownListener extends LiteMod +{ + public abstract void onShutDown(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/BrandingProvider.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/BrandingProvider.java new file mode 100644 index 00000000..5f129c64 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/BrandingProvider.java @@ -0,0 +1,116 @@ +package com.mumfrey.liteloader.api; + +import java.net.URI; + +import net.minecraft.util.ResourceLocation; + +import com.mumfrey.liteloader.util.render.Icon; + +/** + * LiteLoader Extensible API - Branding Provider + * + *

      The Branding Provider manages loader branding alterations for a particular + * API. This is an optional API component which allows an API to specify + * customisations and additions to the loader environment in order to provide a + * more comfortable integration for the API.

      + * + *

      Since some branding options simply stack (like the API copyright notices + * for example) all APIs will be allowed to supply this information, however + * other options (like the main logo image) can only be set by one API. To + * determine which API should be used to set this information, the getPriority() + * method will be called and used to sort branding providers by priority, with + * highest priority winning.

      + * + *

      All branding options may return a null/not set value, allowing a branding + * provider to only override the branding features it wishes. Some options + * require a non-null value to be returned from a set of methods in order to + * take effect. eg. the logo option requires non-null return values from BOTH + * the getLogoResource() and getLogoCoords() methods.

      + * + * @author Adam Mummery-Smith + */ +public interface BrandingProvider extends CustomisationProvider +{ + /** + * Get the priority of this provider, higher numbers take precedence. Some + * brandings can only be set by one provider (eg. the main "about" logo) so + * the branding provider with the highest priority will be the one which + * gets control of that feature. + */ + public abstract int getPriority(); + + /** + * Get the primary branding colour for this API, the branding provider + * should return 0 if it does not wish to override the branding colour. The + * branding colour is used for the mod list entries and hyper-links within + * the about GUI panels, the colour returned should be fully opaque. + */ + public abstract int getBrandingColour(); + + /** + * Get the resource to use for the main logo, the API with the highest + * priority gets to define the logo, this method can return null if this API + * does not want to override the logo. + */ + public abstract ResourceLocation getLogoResource(); + + /** + * Gets the coordinates of the logo as an IIcon instance, only called if + * getLogoResource() returns a non-null value and the logo will only be used + * if BOTH methods return a valid object. + */ + public abstract Icon getLogoCoords(); + + /** + * Get the resource to use for the icon logo (the chicken in the default + * setup), the API with the highest priority gets to define the icon logo, + * this method can return null if this API does not want to override the + * icon. + */ + public abstract ResourceLocation getIconResource(); + + /** + * Gets the coordinates of the icon logo as an IIcon instance, only called + * if getIconResource() returns a non-null value and the icon will only be + * used if BOTH methods return a valid object. + */ + public abstract Icon getIconCoords(); + + /** + * Get the display name for this API, used on the "about" screen, must not + * return null. + */ + public abstract String getDisplayName(); + + /** + * Get the copyright text for this API, used on the "about" screen, must not + * return null. + */ + public abstract String getCopyrightText(); + + /** + * Get the main home page URL for this API, used on the "about" screen, must + * not return null. + */ + public abstract URI getHomepage(); + + /** + * If you wish to display a clickable twitter icon next to the API + * information in the "about" panel then you must return values from this + * method as well as getTwitterAvatarResource() and + * getTwitterAvatarCoords(). Return the twitter user name here. + */ + public abstract String getTwitterUserName(); + + /** + * If you wish to display a clickable twitter icon next to the API + * information, return the icon resource here. + */ + public abstract ResourceLocation getTwitterAvatarResource(); + + /** + * If you wish to display a clickable twitter icon next to the API + * information, return the icon coordinates here. + */ + public abstract Icon getTwitterAvatarCoords(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/ContainerRegistry.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/ContainerRegistry.java new file mode 100644 index 00000000..fcd66eec --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/ContainerRegistry.java @@ -0,0 +1,98 @@ +package com.mumfrey.liteloader.api; + +import java.io.File; +import java.util.Collection; + +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.interfaces.Loadable; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.interfaces.TweakContainer; + +/** + * Registry for enabled, disabled, injected and bad containers + * + * @author Adam Mummery-Smith + */ +public interface ContainerRegistry +{ + public enum DisabledReason + { + UNKNOWN("Container %s is could not be loaded for UNKNOWN reason"), + USER_DISABLED("Container %s is disabled"), + MISSING_DEPENDENCY("Container %s is missing one or more dependencies"), + MISSING_API("Container %s is missing one or more required APIs"); + + private final String message; + + private DisabledReason(String message) + { + this.message = message; + } + + public String getMessage(LoadableMod container) + { + return String.format(this.message, container); + } + } + + /** + * Register an enabled container, removes the container from the disabled + * containers list if present. + */ + public abstract void registerEnabledContainer(LoadableMod container); + + /** + * Get all enabled containers + */ + public abstract Collection> getEnabledContainers(); + + /** + * Get a specific enabled container by id + */ + public abstract LoadableMod getEnabledContainer(String identifier); + + /** + * Register a disabled container + */ + public abstract void registerDisabledContainer(LoadableMod container, DisabledReason reason); + + /** + * Get all disabled containers + */ + public abstract Collection>> getDisabledContainers(); + + /** + * Check whether a specific container is registered as disabled + */ + public abstract boolean isDisabledContainer(LoadableMod container); + + /** + * Register a bad container + */ + public abstract void registerBadContainer(Loadable container, String reason); + + /** + * Get all bad containers + */ + public abstract Collection>> getBadContainers(); + + /** + * Register a candidate tweak container + */ + public abstract void registerTweakContainer(TweakContainer container); + + /** + * Get all registered tweak containers + */ + public abstract Collection> getTweakContainers(); + + /** + * Register an injected tweak container + */ + public abstract void registerInjectedTweak(TweakContainer container); + + /** + * Get all injected tweak containers + */ + public abstract Collection>> getInjectedTweaks(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/CoreProvider.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/CoreProvider.java new file mode 100644 index 00000000..f9feefb6 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/CoreProvider.java @@ -0,0 +1,63 @@ +package com.mumfrey.liteloader.api; + +import net.minecraft.network.INetHandler; +import net.minecraft.network.play.server.S01PacketJoinGame; + +import com.mumfrey.liteloader.common.GameEngine; +import com.mumfrey.liteloader.core.LiteLoaderMods; + +/** + * LiteLoader Extensible API - API Core Provider + * + * Core Providers are objects whose lifecycle is equivalent to the run time of + * game and thus the entire lifecycle of your API, they are instanced early in + * the loader startup process. CoreProviders can implement any Observer + * interface as appropriate and are automatically considered when allocating + * Observers to callback lists. + * + * @author Adam Mummery-Smith + */ +public interface CoreProvider extends TickObserver, WorldObserver, ShutdownObserver, PostRenderObserver +{ + public abstract void onInit(); + + /** + * During the postInit phase, the mods which were discovered during preInit + * phase are initialised and the interfaces are allocated. This callback is + * invoked at the very start of the postInit phase, before mods are + * initialised but after the point at which it is safe to assume it's ok to + * access game classes. This is the first point at which the Minecraft game + * instance should be referenced. Be aware that certain game classes (such + * as the EntityRenderer) are NOT initialised at this point. + * + * @param engine + */ + public abstract void onPostInit(GameEngine engine); + + /** + * Once the mods are initialised and the interfaces have been allocated, + * this callback is invoked to allow the CoreProvider to perform any tasks + * which should be performed in the postInit phase but after mods have been + * initialised. + * + * @param mods + */ + public abstract void onPostInitComplete(LiteLoaderMods mods); + + /** + * Called once startup is complete and the game loop begins running. This + * callback is invoked immediately prior to the first "tick" event and + * immediately after the the "late init" phase for mods + * (InitCompleteListener). + */ + public abstract void onStartupComplete(); + + /** + * Called immediately on joining a single or multi-player world when the + * JoinGame packet is received. Only called on the client. + * + * @param netHandler + * @param loginPacket + */ + public abstract void onJoinGame(INetHandler netHandler, S01PacketJoinGame loginPacket); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/CustomisationProvider.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/CustomisationProvider.java new file mode 100644 index 00000000..23ab0437 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/CustomisationProvider.java @@ -0,0 +1,11 @@ +package com.mumfrey.liteloader.api; + +/** + * Base interface for loader customisation providers, has to be here so that we + * don't load branding provider classes too soon. + * + * @author Adam Mummery-Smith + */ +public interface CustomisationProvider +{ +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/EnumerationObserver.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/EnumerationObserver.java new file mode 100644 index 00000000..ebd8b9fa --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/EnumerationObserver.java @@ -0,0 +1,64 @@ +package com.mumfrey.liteloader.api; + +import java.io.File; + +import com.mumfrey.liteloader.api.ContainerRegistry.DisabledReason; +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.interfaces.LoaderEnumerator; +import com.mumfrey.liteloader.interfaces.TweakContainer; + +/** + * LiteLoader Extensible API - Enumeration observer + * + * EnumerationObserver receive callbacks when mod containers are enumerated. + * Instances of this class must be returned from getPreInitObservers in + * order to work. + * + * @author Adam Mummery-Smith + */ +public interface EnumerationObserver extends Observer +{ + /** + * Called upon registration for every discovered container which is enabled + * + * @param enumerator + * @param container + */ + public abstract void onRegisterEnabledContainer(LoaderEnumerator enumerator, LoadableMod container); + + /** + * Called upon registration for every discovered container which is + * currently disabled, either because + * + * @param enumerator + * @param container + * @param reason + */ + public abstract void onRegisterDisabledContainer(LoaderEnumerator enumerator, LoadableMod container, DisabledReason reason); + + /** + * Called AFTER registration of an ENABLED container (eg. + * onRegisterEnabledContainer will be called first) if that container also + * contains tweaks. + * + * @param enumerator + * @param container + */ + public abstract void onRegisterTweakContainer(LoaderEnumerator enumerator, TweakContainer container); + + /** + * Called when a mod container is added to the pending mods list. This does + * not mean that the specific mod will actually be instanced since the mod + * can still be disabled via the exclusion list (this is to allow entire + * containers to be disabled or just individual mods) and so if you wish to + * observe actual mod instantiation you should still provide a + * {@link com.mumfrey.liteloader.client.ResourceObserver}. However this + * event expresses a declaration by the enumerator of an intention to load + * the specified mod. + * + * @param enumerator + * @param mod + */ + public abstract void onModAdded(LoaderEnumerator enumerator, ModInfo> mod); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/EnumeratorModule.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/EnumeratorModule.java new file mode 100644 index 00000000..2fc1cdea --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/EnumeratorModule.java @@ -0,0 +1,68 @@ +package com.mumfrey.liteloader.api; + +import com.mumfrey.liteloader.interfaces.ModularEnumerator; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; + +import net.minecraft.launchwrapper.LaunchClassLoader; + +/** + * LiteLoader Extensible API - Interface for objects which can enumerate mods in + * places. + * + *

      EnumeratorModules plug into the LoaderEnumerator and are used to discover + * mod containers in various locations, for example searching in a specific + * folder for particular files.

      + * + * @author Adam Mummery-Smith + */ +public interface EnumeratorModule +{ + /** + * @param environment Loader environment + * @param properties Loader properties + */ + public abstract void init(LoaderEnvironment environment, LoaderProperties properties); + + /** + * @param environment Loader environment + * @param properties Loader properties + */ + public abstract void writeSettings(LoaderEnvironment environment, LoaderProperties properties); + + /** + * Find loadable mods in this enumerator's domain, the enumerator module + * should call back against the enumerator itself to register containers it + * discovers using the registerModContainer() and registerTweakContainer() + * callbacks. + * + *

      This method is called during loader PREINIT phase so do not use any + * game classes here!

      + * + * @param enumerator + * @param profile + */ + public abstract void enumerate(ModularEnumerator enumerator, String profile); + + /** + * The enumerator module should inject (as required) any discovered + * containers into the classpath. + * + *

      This method is called during the loader INIT phase.

      + * + * @param enumerator + * @param classLoader + */ + public abstract void injectIntoClassLoader(ModularEnumerator enumerator, LaunchClassLoader classLoader); + + /** + * The enumerator module should callback against the enumerator using the + * registerModsFrom() callback to register mods from discovered containers. + * + *

      This method is called during the loader INIT phase

      + * + * @param enumerator + * @param classLoader + */ + public abstract void registerMods(ModularEnumerator enumerator, LaunchClassLoader classLoader); +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/EnumeratorPlugin.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/EnumeratorPlugin.java new file mode 100644 index 00000000..e7b113ba --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/EnumeratorPlugin.java @@ -0,0 +1,37 @@ +package com.mumfrey.liteloader.api; + +import java.util.List; + +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; + +/** + * LiteLoader Extensible API - Interface for objects which can interact with the + * enumeration process, not yet available to APIs. + * + * @author Adam Mummery-Smith + */ +public interface EnumeratorPlugin +{ + /** + * Initialise this plugin + */ + public abstract void init(LoaderEnvironment environment, LoaderProperties properties); + + /** + * Get classes in the supplied container + * + * @param container Container to inspect + * @param classloader ClassLoader for this container + * @param validator Mod class validator + * @return list of classes in the container + */ + public abstract List> getClasses(LoadableMod container, ClassLoader classloader, ModClassValidator validator); + + public abstract boolean checkEnabled(ContainerRegistry containers, LoadableMod container); + + public abstract boolean checkDependencies(ContainerRegistry containers, LoadableMod base); + + public abstract boolean checkAPIRequirements(ContainerRegistry containers, LoadableMod container); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/GenericObserver.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/GenericObserver.java new file mode 100644 index 00000000..98389292 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/GenericObserver.java @@ -0,0 +1,13 @@ +package com.mumfrey.liteloader.api; + +/** + * Generic Observer class, for Intra-API Observer inking + * + * @author Adam Mummery-Smith + * + * @param Argument type for observable events + */ +public interface GenericObserver extends Observer +{ + public abstract void onObservableEvent(String eventName, T... eventArgs) throws ClassCastException; +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/InterfaceObserver.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/InterfaceObserver.java new file mode 100644 index 00000000..58137627 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/InterfaceObserver.java @@ -0,0 +1,11 @@ +package com.mumfrey.liteloader.api; + +/** + * Observer for interface binding events + * + * @author Adam Mummery-Smith + */ +public interface InterfaceObserver extends Observer +{ + public void onRegisterListener(InterfaceProvider provider, Class interfaceType, Listener listener); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/InterfaceProvider.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/InterfaceProvider.java new file mode 100644 index 00000000..a6432eea --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/InterfaceProvider.java @@ -0,0 +1,32 @@ +package com.mumfrey.liteloader.api; + +import com.mumfrey.liteloader.core.InterfaceRegistrationDelegate; + +/** + * LiteLoader Extensible API - Interface Provider + * + * InterfaceProviders are able to advertise and provide Listener interfaces + * which can be implemented by mods or other Listener-derived classes. + * + * @author Adam Mummery-Smith + */ +public interface InterfaceProvider +{ + /** + * Base type of Listeners which can consume events provided by this provider + */ + public abstract Class getListenerBaseType(); + + /** + * The provider should call back against the supplied delegate in order to + * advertise the interfaces it provides. + * + * @param delegate + */ + public abstract void registerInterfaces(InterfaceRegistrationDelegate delegate); + + /** + * Initialise this provider, called AFTER enumeration but before binding + */ + public abstract void initProvider(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/Listener.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/Listener.java new file mode 100644 index 00000000..65044ae8 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/Listener.java @@ -0,0 +1,22 @@ +package com.mumfrey.liteloader.api; + +/** + * LiteLoader Extensible API - Listener is the base interface for + * (counter-intuitively) consumable Listener interfaces, in that derived + * interfaces are consumable (from the point of view of the providers) but + * actual implementors consume the events thus advertised by implementing those + * interfaces, making them themselves the consumers. Okay so that's probably + * pretty confusing but I can't think of any better terminology so it's + * staying :) + * + * @author Adam Mummery-Smith + */ +public interface Listener +{ + /** + * Get the display name + * + * @return display name + */ + public abstract String getName(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/LiteAPI.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/LiteAPI.java new file mode 100644 index 00000000..ef5c5d59 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/LiteAPI.java @@ -0,0 +1,120 @@ +package com.mumfrey.liteloader.api; + +import java.util.List; + +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; + +/** + * LiteLoader Extensible API - main Mod API + * + *

      Implementors of this class don't really do anything except provide + * instances of other classes which make up the API proper. Where possible, + * instance things as late as possible (eg. do not instance your + * CoreProviders in the constructor or init()) because it's possible to screw up + * the game startup if things get loaded out of order, in general it's best to + * instance things only at the earliest point in time at which they are needed. + *

      + * + * @author Adam Mummery-Smith + */ +public interface LiteAPI +{ + /** + * Initialise this API, the API should do as little processing as possible + * here, but should also cache the supplied environment and properties + * instances for later use. + * + * @param environment + * @param properties + */ + public abstract void init(LoaderEnvironment environment, LoaderProperties properties); + + /** + * Get the identifier for this API, the identifier is used to retrieve the + * API and match it against specified mod API dependencies. + */ + public abstract String getIdentifier(); + + /** + * Get the friendly name of this API + */ + public abstract String getName(); + + /** + * Get the human-readable version of the API, can be any value + */ + public abstract String getVersion(); + + /** + * Get the revision number of this API. Unlike the version number, the + * revision number should only change when an incompatible change is made to + * the APIs interfaces, it is also used when a mod specifies an API + * dependency using the api@revision syntax. + */ + public abstract int getRevision(); + + /** + * Get mixin environment configuration provider for this API, can return + * null. + */ + public abstract MixinConfigProvider getMixins(); + + /** + * Should return an array of required transformer names, these transformers + * will be injected UPSTREAM. Can return null. + */ + public abstract String[] getRequiredTransformers(); + + /** + * Should return an array of required transformer names, these transformers + * will be injected DOWNSTREAM. Can return null. + */ + public abstract String[] getRequiredDownstreamTransformers(); + + /** + * Return a mod class prefix supported by this API, can return null if an + * API just wants to use "LiteMod" as a standard class name prefix + */ + public abstract String getModClassPrefix(); + + /** + * Should return a list of Enumerator modules to be injected, can return + * null if the API doesn't want to inject any additonal modules + */ + public abstract List getEnumeratorModules(); + + /** + * Should return a list of CoreProviders for this API, can return null if + * the API doesn't have any CoreProviders, (almost) guaranteed to only be + * called once. + */ + public abstract List getCoreProviders(); + + /** + * Should return a list of InterfaceProviders for this API, can return null + * if the API doesn't have any InterfaceProviders, (almost) guaranteed to + * only be called once + */ + public abstract List getInterfaceProviders(); + + /** + * Should return a list of Observers which are safe to instantiate during + * pre-init, for example EnumerationObservers. Can return null if the API + * doesn't have any Observers. + */ + public abstract List getPreInitObservers(); + + /** + * Should return a list of Observers for this API, can return null if the + * API doesn't have any Observers, (almost) guaranteed to only be called + * once. This list may include Observers returned by getPreInitObservers if + * the observers are still required. + */ + public abstract List getObservers(); + + /** + * Get the customisation providers for this API, can return null + */ + public abstract List getCustomisationProviders(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/MixinConfigProvider.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/MixinConfigProvider.java new file mode 100644 index 00000000..097f34f2 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/MixinConfigProvider.java @@ -0,0 +1,27 @@ +package com.mumfrey.liteloader.api; + +import org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel; + +/** + * Container for all of an API's mixin environment configuration + */ +public interface MixinConfigProvider +{ + /** + * Get the minimum required mixin operating compatibility level for this + * API, can return null. + */ + public abstract CompatibilityLevel getCompatibilityLevel(); + + /** + * Get mixin configuration files for this API, all returned configs will be + * added to the DEFAULT environment. Can return null. + */ + public abstract String[] getMixinConfigs(); + + /** + * Get mixin error handler classes to register for this API. Can return + * null. + */ + public abstract String[] getErrorHandlers(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/ModClassValidator.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/ModClassValidator.java new file mode 100644 index 00000000..3c3c85eb --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/ModClassValidator.java @@ -0,0 +1,14 @@ +package com.mumfrey.liteloader.api; + +/** + * Interface for object which validates whether a supplied mod class can be + * loaded. + * + * @author Adam Mummery-Smith + */ +public interface ModClassValidator +{ + public abstract boolean validateName(String className); + + public abstract boolean validateClass(ClassLoader classLoader, Class candidateClass); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/ModInfoDecorator.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/ModInfoDecorator.java new file mode 100644 index 00000000..d176c491 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/ModInfoDecorator.java @@ -0,0 +1,53 @@ +package com.mumfrey.liteloader.api; + +import java.util.List; + +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.util.render.IconTextured; + +/** + * LiteLoader Extensible API - Branding Provider + * + * Decorator for ModInfo classes, to alter the appearance of ModInfo entries in + * the mod list. + * + * @author Adam Mummery-Smith + */ +public interface ModInfoDecorator extends CustomisationProvider +{ + /** + * Add icons to the mod list entry for this mod + * + * @param mod + * @param icons + */ + public abstract void addIcons(ModInfo mod, List icons); + + /** + * Allows this decorator to modify the status text for the specified mod, + * return null if no modification required. + * + * @param statusText + * @return new status text or NULL to indicate the text should remain + * default + */ + public abstract String modifyStatusText(ModInfo mod, String statusText); + + /** + * Allow decorators to draw custom content on the mod list entries + * + * @param mouseX Mouse X position + * @param mouseY Mouse Y position + * @param partialTicks + * @param xPosition Panel X position + * @param yPosition Panel Y position + * @param width Panel width + * @param selected Panel height + * @param mod ModInfo + * @param gradientColour + * @param titleColour + * @param statusColour + */ + public abstract void onDrawListEntry(int mouseX, int mouseY, float partialTicks, int xPosition, int yPosition, int width, int height, + boolean selected, ModInfo mod, int gradientColour, int titleColour, int statusColour); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/ModLoadObserver.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/ModLoadObserver.java new file mode 100644 index 00000000..82b9804e --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/ModLoadObserver.java @@ -0,0 +1,65 @@ +package com.mumfrey.liteloader.api; + +import java.io.File; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.interfaces.LoadableMod; + +/** + * LiteLoader Extensible API - Mod Load Observer + * + * ModLoadObservers receive callbacks when mod loading events are occurring, + * prior to init and other loader-managed processes. + * + * @author Adam Mummery-Smith + */ +public interface ModLoadObserver extends Observer +{ + /** + * Called immediately after a mod instance is created, throw an exception + * from this method in order to prevent further initialisation. + */ + public abstract void onModLoaded(LiteMod mod); + + /** + * Called after a mod is instanced and has been successfully added to the + * active mods list. + * + * @param handle Mod handle + */ + public abstract void onPostModLoaded(ModInfo> handle); + + /** + * Called if mod loading fails + * + * @param container + * @param identifier + * @param reason + * @param th + */ + public abstract void onModLoadFailed(LoadableMod container, String identifier, String reason, Throwable th); + + /** + * Called before a mod's init() method is called + * + * @param mod + */ + public abstract void onPreInitMod(LiteMod mod); + + /** + * Called after a mod's init() method is called + * + * @param mod + */ + public abstract void onPostInitMod(LiteMod mod); + + /** + * Called when migrating mod config from version to version + * + * @param mod + * @param newConfigPath + * @param oldConfigPath + */ + public abstract void onMigrateModConfig(LiteMod mod, File newConfigPath, File oldConfigPath); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/Observer.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/Observer.java new file mode 100644 index 00000000..ffbbe86c --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/Observer.java @@ -0,0 +1,15 @@ +package com.mumfrey.liteloader.api; + +/** + * LiteLoader Extensible API - Observer base interface + * + *

      Observers are essentially "core listeners" which are objects which make up + * the core a of a particular API implementation and sink events generated by + * the Loader or by other observable components. See the derived interfaces for + * more detail.

      + * + * @author Adam Mummery-Smith + */ +public interface Observer +{ +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/PostRenderObserver.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/PostRenderObserver.java new file mode 100644 index 00000000..2d733b2b --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/PostRenderObserver.java @@ -0,0 +1,14 @@ +package com.mumfrey.liteloader.api; + +/** + * LiteLoader Extensible API - Post-render Observers + * + *

      PostRenderObservers receive the onPostRender event every frame, allowing + * "draw-on-top" behaviour for API components.

      + * + * @author Adam Mummery-Smith + */ +public interface PostRenderObserver extends Observer +{ + public abstract void onPostRender(int mouseX, int mouseY, float partialTicks); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/ShutdownObserver.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/ShutdownObserver.java new file mode 100644 index 00000000..dced2a89 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/ShutdownObserver.java @@ -0,0 +1,15 @@ +package com.mumfrey.liteloader.api; + +/** + * LiteLoader Extensible API - ShutDownObserver + * + * ShutDownObservers receive an event when the game is shutting down due to a + * user request. They do NOT receive the callback when the VM is terminating, + * use a regular VM shutdownhook for that. + * + * @author Adam Mummery-Smith + */ +public interface ShutdownObserver extends Observer +{ + public abstract void onShutDown(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/TickObserver.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/TickObserver.java new file mode 100644 index 00000000..a2fc4e9d --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/TickObserver.java @@ -0,0 +1,13 @@ +package com.mumfrey.liteloader.api; + +/** + * LiteLoader Extensible API - TickObserver + * + * Received a callback every tick (duh) PRIOR to the mod tick event + * + * @author Adam Mummery-Smith + */ +public interface TickObserver extends Observer +{ + public abstract void onTick(boolean clock, float partialTicks, boolean inGame); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/TranslationProvider.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/TranslationProvider.java new file mode 100644 index 00000000..6f945f79 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/TranslationProvider.java @@ -0,0 +1,21 @@ +package com.mumfrey.liteloader.api; + +/** + * Interface for providers which can handle translation requests + * + * @author Adam Mummery-Smith + */ +public interface TranslationProvider extends CustomisationProvider +{ + /** + * Translate the supplied key or return NULL if the provider has no + * translation for the specified key + */ + public abstract String translate(String key, Object... args); + + /** + * Translate the supplied key to the specified locale, or return NULL if the + * provider has no translation for this key + */ + public abstract String translate(String locale, String key, Object... args); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/WorldObserver.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/WorldObserver.java new file mode 100644 index 00000000..4b662b9f --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/WorldObserver.java @@ -0,0 +1,16 @@ +package com.mumfrey.liteloader.api; + +import net.minecraft.world.World; + +/** + * LiteLoader Extensible API - WorldObserver + * + *

      WorldObservers receive a callback when the Minecraft.theWorld reference + * changes, beware the value is allowed to be null.

      + * + * @author Adam Mummery-Smith + */ +public interface WorldObserver extends Observer +{ + public abstract void onWorldChanged(World world); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/APIException.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/APIException.java new file mode 100644 index 00000000..f2e9d1d7 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/APIException.java @@ -0,0 +1,27 @@ +package com.mumfrey.liteloader.api.exceptions; + +public abstract class APIException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public APIException() + { + super(); + } + + public APIException(String message) + { + super(message); + } + + public APIException(Throwable cause) + { + super(cause); + } + + public APIException(String message, Throwable cause) + { + super(message, cause); + } + +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidAPIException.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidAPIException.java new file mode 100644 index 00000000..8615f121 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidAPIException.java @@ -0,0 +1,16 @@ +package com.mumfrey.liteloader.api.exceptions; + +public class InvalidAPIException extends APIException +{ + private static final long serialVersionUID = 1L; + + public InvalidAPIException(String message, Throwable cause) + { + super(message, cause); + } + + public InvalidAPIException(String message) + { + super(message); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidAPIStateException.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidAPIStateException.java new file mode 100644 index 00000000..57de5e7f --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidAPIStateException.java @@ -0,0 +1,11 @@ +package com.mumfrey.liteloader.api.exceptions; + +public class InvalidAPIStateException extends APIException +{ + private static final long serialVersionUID = 1L; + + public InvalidAPIStateException(String message) + { + super(message); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidProviderException.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidProviderException.java new file mode 100644 index 00000000..a361b9df --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/exceptions/InvalidProviderException.java @@ -0,0 +1,16 @@ +package com.mumfrey.liteloader.api.exceptions; + +public class InvalidProviderException extends APIException +{ + private static final long serialVersionUID = 1L; + + public InvalidProviderException(String message, Throwable cause) + { + super(message, cause); + } + + public InvalidProviderException(String message) + { + super(message); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIAdapter.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIAdapter.java new file mode 100644 index 00000000..53583fc1 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIAdapter.java @@ -0,0 +1,86 @@ +package com.mumfrey.liteloader.api.manager; + +import java.util.List; + +import com.mumfrey.liteloader.api.CoreProvider; +import com.mumfrey.liteloader.api.LiteAPI; +import com.mumfrey.liteloader.api.Observer; +import com.mumfrey.liteloader.interfaces.InterfaceRegistry; + +/** + * API Adapter provides convenience methods for invoking actions on ALL + * registered APIs + * + * @author Adam Mummery-Smith + */ +public interface APIAdapter +{ + /** + * APIs should register their mixin configs and set up the mixin environment + * here. + */ + public abstract void initMixins(); + + /** + * Aggregate and return required transformers from all registered APIs + */ + public abstract List getRequiredTransformers(); + + /** + * Aggregate and return required downstream transformers from all registered + * APIs + */ + public abstract List getRequiredDownstreamTransformers(); + + /** + * Register interfaces from all registered APIs with the specified registry + */ + public abstract void registerInterfaces(InterfaceRegistry interfaceManager); + + /** + * Get the CoreProviders for the specified API. Consuming classes should + * call this method instead of calling API::getCoreProviders() directly + * since getCoreProviders() should only be invoked once and the resulting + * collection is cached by the API Adapter + */ + public abstract List getCoreProviders(); + + /** + * Get the observers for the specified API. Consuming classes should call + * this method instead of calling API::getObservers() directly since + * getObservers() should only be invoked once and the resulting list is + * cached by the API Adapter + * + * @param api API to get observers for + */ + public abstract List getObservers(LiteAPI api); + + /** + * Get the observers for the specified API which implement the specified + * Observer interface. Always returns a valid list (even if empty) and + * doesn't return null. + * + * @param api API to get observers for + * @param observerType type of observer to search for + */ + public abstract List getObservers(LiteAPI api, Class observerType); + + /** + * Get the observers for all registered APIs which implement the specified + * Observer interface. Always returns a valid list (even if empty) and + * doesn't return null. Also includes core providers + * + * @param observerType type of observer to search for + */ + public abstract List getAllObservers(Class observerType); + + /** + * @param api + */ + public abstract List getPreInitObservers(LiteAPI api); + + /** + * @param observerType + */ + public abstract List getPreInitObservers(Class observerType); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIProvider.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIProvider.java new file mode 100644 index 00000000..8669b192 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIProvider.java @@ -0,0 +1,50 @@ +package com.mumfrey.liteloader.api.manager; + +import java.util.regex.Pattern; + +import com.mumfrey.liteloader.api.LiteAPI; + +/** + * Interface for the API Provider, which manages API instances and lifecycle + * + * @author Adam Mummery-Smith + */ +public interface APIProvider +{ + public static final Pattern idAndRevisionPattern = Pattern.compile("^([^@]+)@([0-9]{1,5})$"); + + /** + * Get all available API instances in an array + */ + public abstract LiteAPI[] getAPIs(); + + /** + * Returns true if the specified API is available + * + * @param identifier API identifier (case sensitive) or API + * identifier-plus-minrevision in the form "identifier@minver" + */ + public abstract boolean isAPIAvailable(String identifier); + + /** + * Returns true if the specified API is available + * + * @param identifier API identifier (case sensitive) + * @param minRevision minimum required revision + */ + public abstract boolean isAPIAvailable(String identifier, int minRevision); + + /** + * Gets a specific API by identifier + * + * @param identifier API identifier (case sensitive) + */ + public abstract LiteAPI getAPI(String identifier); + + /** + * Gets a specific API by class + * + * @param apiClass + */ + public abstract T getAPI(Class apiClass); +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIProviderBasic.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIProviderBasic.java new file mode 100644 index 00000000..fc7466f3 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIProviderBasic.java @@ -0,0 +1,340 @@ +package com.mumfrey.liteloader.api.manager; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; + +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel; + +import com.mumfrey.liteloader.api.CoreProvider; +import com.mumfrey.liteloader.api.LiteAPI; +import com.mumfrey.liteloader.api.MixinConfigProvider; +import com.mumfrey.liteloader.api.Observer; +import com.mumfrey.liteloader.interfaces.InterfaceRegistry; + +/** + * Basic implementation of APIProvider and APIAdapter + * + * @author Adam Mummery-Smith + */ +class APIProviderBasic implements APIProvider, APIAdapter +{ + /** + * API instances + */ + private final LiteAPI[] apis; + + /** + * Map of API identifiers to API instances + */ + private final Map apiMap = new HashMap(); + + /** + * Cached observer set + */ + private final Map> observers = new HashMap>(); + + /** + * Cached preinit observers + */ + private final Map> preInitiObservers = new HashMap>(); + + /** + * Cached CoreProvider set + */ + private List coreProviders; + + APIProviderBasic(LiteAPI[] apis) + { + this.apis = apis; + + for (LiteAPI api : this.apis) + { + this.apiMap.put(api.getIdentifier(), api); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIAdapter#initMixins() + */ + @Override + public void initMixins() + { + for (LiteAPI api : this.apis) + { + MixinConfigProvider mixins = api.getMixins(); + if (mixins != null) + { + CompatibilityLevel level = mixins.getCompatibilityLevel(); + if (level != null) + { + MixinEnvironment.setCompatibilityLevel(level); + } + + String[] configs = mixins.getMixinConfigs(); + if (configs != null) + { + for (String config : configs) + { + MixinEnvironment.getDefaultEnvironment().addConfiguration(config); + } + } + + String[] errorHandlers = mixins.getErrorHandlers(); + if (errorHandlers != null) + { + for (String handlerName : errorHandlers) + { + MixinEnvironment.getDefaultEnvironment().registerErrorHandlerClass(handlerName); + } + } + } + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIProvider + * #getRequiredTransformers() + */ + @Override + public List getRequiredTransformers() + { + List requiredTransformers = new ArrayList(); + + for (LiteAPI api : this.apis) + { + String[] apiTransformers = api.getRequiredTransformers(); + if (apiTransformers != null) + { + requiredTransformers.addAll(Arrays.asList(apiTransformers)); + } + } + + return requiredTransformers; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIProvider + * #getRequiredDownstreamTransformers() + */ + @Override + public List getRequiredDownstreamTransformers() + { + List requiredDownstreamTransformers = new ArrayList(); + + for (LiteAPI api : this.apis) + { + String[] apiTransformers = api.getRequiredDownstreamTransformers(); + if (apiTransformers != null) + { + requiredDownstreamTransformers.addAll(Arrays.asList(apiTransformers)); + } + } + + return requiredDownstreamTransformers; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIProvider + * #getObservers(com.mumfrey.liteloader.api.LiteAPI) + */ + @Override + public List getObservers(LiteAPI api) + { + if (!this.observers.containsKey(api)) + { + List apiObservers = api.getObservers(); + this.observers.put(api, Collections.unmodifiableList(apiObservers != null ? apiObservers : new ArrayList())); + } + + return this.observers.get(api); + } + + @Override + public List getPreInitObservers(LiteAPI api) + { + if (!this.preInitiObservers.containsKey(api)) + { + List apiObservers = api.getPreInitObservers(); + this.preInitiObservers.put(api, Collections.unmodifiableList(apiObservers != null ? apiObservers : new ArrayList())); + } + + return this.preInitiObservers.get(api); + } + + @SuppressWarnings("unchecked") + @Override + public List getObservers(LiteAPI api, Class observerType) + { + List matchingObservers = new ArrayList(); + + for (Observer observer : this.getObservers(api)) + { + if (observerType.isAssignableFrom(observer.getClass()) && !matchingObservers.contains(observer)) + { + matchingObservers.add((T)observer); + } + } + + return matchingObservers; + } + + @SuppressWarnings("unchecked") + @Override + public List getAllObservers(Class observerType) + { + List matchingObservers = new ArrayList(); + for (LiteAPI api : this.apis) + { + matchingObservers.addAll(this.getObservers(api, observerType)); + } + + for (CoreProvider coreProvider : this.getCoreProviders()) + { + if (observerType.isAssignableFrom(coreProvider.getClass()) && !matchingObservers.contains(coreProvider)) + { + matchingObservers.add((T)coreProvider); + } + } + + return matchingObservers; + } + + @SuppressWarnings("unchecked") + @Override + public List getPreInitObservers(Class observerType) + { + List matchingObservers = new ArrayList(); + for (LiteAPI api : this.apis) + { + for (Observer observer : this.getPreInitObservers(api)) + { + if (observerType.isAssignableFrom(observer.getClass()) && !matchingObservers.contains(observer)) + { + matchingObservers.add((T)observer); + } + } + } + + return matchingObservers; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIProvider + * #registerInterfaceProviders( + * com.mumfrey.liteloader.core.InterfaceManager) + */ + @Override + public void registerInterfaces(InterfaceRegistry interfaceManager) + { + for (LiteAPI api : this.apis) + { + interfaceManager.registerAPI(api); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIAdapter#getCoreProviders() + */ + @Override + public List getCoreProviders() + { + if (this.coreProviders == null) + { + List coreProviders = new ArrayList(); + + for (LiteAPI api : this.apis) + { + List apiCoreProviders = api.getCoreProviders(); + if (apiCoreProviders != null) + { + coreProviders.addAll(apiCoreProviders); + } + } + + this.coreProviders = Collections.unmodifiableList(coreProviders); + } + + return this.coreProviders; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIProvider#getAPIs() + */ + @Override + public LiteAPI[] getAPIs() + { + return this.apis; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIProvider + * #isAPIAvailable(java.lang.String) + */ + @Override + public boolean isAPIAvailable(String identifier) + { + if (identifier != null && identifier.contains("@")) + { + Matcher idAndRevisionPatternMatcher = APIProvider.idAndRevisionPattern.matcher(identifier); + if (idAndRevisionPatternMatcher.matches()) + { + return this.isAPIAvailable(idAndRevisionPatternMatcher.group(1), Integer.parseInt(idAndRevisionPatternMatcher.group(2))); + } + } + + return this.apiMap.containsKey(identifier); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIProvider + * #isAPIAvailable(java.lang.String, int) + */ + @Override + public boolean isAPIAvailable(String identifier, int minRevision) + { + LiteAPI api = this.apiMap.get(identifier); + if (api == null) return false; + + return api.getRevision() >= minRevision; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIProvider + * #getAPI(java.lang.String) + */ + @Override + public LiteAPI getAPI(String identifier) + { + return this.apiMap.get(identifier); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.manager.APIProvider + * #getAPI(java.lang.Class) + */ + @SuppressWarnings("unchecked") + @Override + public T getAPI(Class apiClass) + { + try + { + for (LiteAPI api : this.apis) + { + if (apiClass.isAssignableFrom(api.getClass())) + { + return (T)api; + } + } + } + catch (NullPointerException ex1) {} + catch (ClassCastException ex2) {} + + return null; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIRegistry.java b/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIRegistry.java new file mode 100644 index 00000000..e921b37a --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/api/manager/APIRegistry.java @@ -0,0 +1,182 @@ +package com.mumfrey.liteloader.api.manager; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.minecraft.launchwrapper.Launch; + +import com.mumfrey.liteloader.api.LiteAPI; +import com.mumfrey.liteloader.api.exceptions.InvalidAPIStateException; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +/** + * This is where we register API classes during early startup before baking the + * registered list into an APIProvider instance + * + * @author Adam Mummery-Smith + */ +public final class APIRegistry +{ + private Set registeredAPIClasses = new LinkedHashSet(); + private Map instances = new LinkedHashMap(); + + private final LoaderEnvironment environment; + + private final LoaderProperties properties; + + private APIProviderBasic baked; + + /** + * @param environment + * @param properties + */ + public APIRegistry(LoaderEnvironment environment, LoaderProperties properties) + { + this.environment = environment; + this.properties = properties; + } + + /** + * Register an API class, throws an exception if the API list has already + * been baked. + * + * @param apiClass + */ + public void registerAPI(String apiClass) throws InvalidAPIStateException + { + if (this.baked != null) + { + throw new InvalidAPIStateException("Unable to register API provider '" + apiClass + + "' because the API state is now frozen, this probably means you are registering an API too late in the initialisation process"); + } + + if (!this.registeredAPIClasses.contains(apiClass)) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Registering API provider class %s", apiClass); + this.registeredAPIClasses.add(apiClass); + } + } + + /** + * Get all currently registered API classes + */ + public String[] getRegisteredAPIs() + { + return this.registeredAPIClasses.toArray(new String[0]); + } + + /** + * @param apiClassName + */ + private LiteAPI spawnAPI(String apiClassName) + { + try + { + LiteLoaderLogger.info("Spawning API provider class '%s' ...", apiClassName); + + @SuppressWarnings("unchecked") + Class apiClass = (Class)Class.forName(apiClassName, true, Launch.classLoader); + + LiteAPI api = apiClass.newInstance(); + String identifier = api.getIdentifier(); + + if (!this.instances.containsKey(identifier)) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "API provider class '%s' provides API '%s'", apiClassName, identifier); + this.instances.put(identifier, api); + return api; + } + + Class conflictingAPIClass = this.instances.get(identifier).getClass(); + LiteLoaderLogger.severe("API identifier clash while registering '%s', identifier '%s' clashes with '%s'", apiClassName, + identifier, conflictingAPIClass); + } + catch (ClassNotFoundException ex) + { + LiteLoaderLogger.severe("API class '%s' could not be created, the specified class could not be loaded", apiClassName); + } + catch (Exception ex) + { + LiteLoaderLogger.severe(ex, "Error while instancing API class '%s'", apiClassName); + } + + return null; + } + + /** + * Populate and return the API instance array + */ + private LiteAPI[] getAllAPIs() + { + List allAPIs = new ArrayList(); + + for (String apiClass : this.registeredAPIClasses) + { + LiteAPI api = this.spawnAPI(apiClass); + if (api != null) + { + allAPIs.add(api); + } + } + + for (LiteAPI api : allAPIs) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Initialising API '%s' ...", api.getIdentifier()); + api.init(this.environment, this.properties); + } + + return allAPIs.toArray(new LiteAPI[allAPIs.size()]); + } + + /** + * Bakes all currently registered API classes to a new APIProvider + * containing the API instances. + * + * @throws InvalidAPIStateException if the API list was already baked + */ + public APIProvider bake() throws InvalidAPIStateException + { + if (this.baked != null) + { + throw new InvalidAPIStateException("Unable to bake the API provider list because the API list is already baked"); + } + + LiteAPI[] apis = this.getAllAPIs(); + return this.baked = new APIProviderBasic(apis); + } + + /** + * Gets the current APIProvider instance + * @throws InvalidAPIStateException if the provider list was not yet baked + */ + public APIProvider getProvider() throws InvalidAPIStateException + { + if (this.baked == null) + { + throw new InvalidAPIStateException("Call to APIRegistry.getProvider() failed because the provider list has not been baked"); + } + + return this.baked; + } + + /** + * Gets the current APIAdapter instance + * @throws InvalidAPIStateException if the provider list was not yet baked + */ + public APIAdapter getAdapter() throws InvalidAPIStateException + { + if (this.baked == null) + { + throw new InvalidAPIStateException("Call to APIRegistry.APIAdapter() failed because the provider list has not been baked"); + } + + return this.baked; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/GameEngine.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/GameEngine.java new file mode 100644 index 00000000..34d92fe6 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/GameEngine.java @@ -0,0 +1,78 @@ +package com.mumfrey.liteloader.common; + +import java.util.List; + +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.profiler.Profiler; +import net.minecraft.server.MinecraftServer; + +/** + * @author Adam Mummery-Smith + * + * @param Type of the client runtime, "Minecraft" on client and null + * on the server + * @param Type of the server runtime, "IntegratedServer" on the + * client, "MinecraftServer" on the server + */ +public interface GameEngine +{ + /** + * True if the environment is a client environment + */ + public abstract boolean isClient(); + + /** + * True if the current environment is a server environment, always true on + * dedicated and true in single player. + */ + public abstract boolean isServer(); + + /** + * True if the client is "in game", always true on server + */ + public abstract boolean isInGame(); + + /** + * True if the game loop's "isRunning" flag is true + */ + public abstract boolean isRunning(); + + /** + * True if the current world is single player, always false on the server + */ + public abstract boolean isSinglePlayer(); + + /** + * Get the underlying client instance, returns a dummy on the server + */ + public abstract TClient getClient(); + + /** + * Get the underlying server instance + */ + public abstract TServer getServer(); + + /** + * Get the resources manager + */ + public abstract Resources getResources(); + + /** + * Get the profiler instance + */ + public abstract Profiler getProfiler(); + + /** + * Get the keybinding list, only supported on client will throw an exception + * on the server. + */ + public abstract List getKeyBindings(); + + /** + * Set the keybinding list, only supported on client will throw an exception + * on the server. + * + * @param keyBindings + */ + public abstract void setKeyBindings(List keyBindings); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/LoadingProgress.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/LoadingProgress.java new file mode 100644 index 00000000..aba6b538 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/LoadingProgress.java @@ -0,0 +1,66 @@ +package com.mumfrey.liteloader.common; + +/** + * @author Adam Mummery-Smith + */ +public abstract class LoadingProgress +{ + private static LoadingProgress instance; + + protected LoadingProgress() + { + LoadingProgress.instance = this; + } + + public static void setEnabled(boolean enabled) + { + if (LoadingProgress.instance != null) LoadingProgress.instance._setEnabled(enabled); + } + + public static void dispose() + { + if (LoadingProgress.instance != null) LoadingProgress.instance._dispose(); + } + + public static void incLiteLoaderProgress() + { + if (LoadingProgress.instance != null) LoadingProgress.instance._incLiteLoaderProgress(); + } + + public static void setMessage(String format, String... args) + { + if (LoadingProgress.instance != null) LoadingProgress.instance._setMessage(String.format(format, (Object[])args)); + } + + public static void setMessage(String message) + { + if (LoadingProgress.instance != null) LoadingProgress.instance._setMessage(message); + } + + public static void incLiteLoaderProgress(String format, String... args) + { + if (LoadingProgress.instance != null) LoadingProgress.instance._incLiteLoaderProgress(String.format(format, (Object[])args)); + } + + public static void incLiteLoaderProgress(String message) + { + if (LoadingProgress.instance != null) LoadingProgress.instance._incLiteLoaderProgress(message); + } + + public static void incTotalLiteLoaderProgress(int by) + { + if (LoadingProgress.instance != null) LoadingProgress.instance._incTotalLiteLoaderProgress(by); + } + + protected abstract void _setEnabled(boolean enabled); + + protected abstract void _dispose(); + + protected abstract void _incLiteLoaderProgress(); + + protected abstract void _setMessage(String message); + + protected abstract void _incLiteLoaderProgress(String message); + + protected abstract void _incTotalLiteLoaderProgress(int by); +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/Resources.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/Resources.java new file mode 100644 index 00000000..4b616ca2 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/Resources.java @@ -0,0 +1,28 @@ +package com.mumfrey.liteloader.common; + +public interface Resources +{ + /** + * Refresh resource pack list + * + * @param force + */ + public abstract void refreshResources(boolean force); + + /** + * Get the resource manager for the current environment, returns the + * SimpleReloadableResourceManager on client and ModResourceManager on the + * server. + */ + public abstract TResourceManager getResourceManager(); + + /** + * @param resourcePack + */ + public abstract boolean registerResourcePack(TResourcePack resourcePack); + + /** + * @param resourcePack + */ + public abstract boolean unRegisterResourcePack(TResourcePack resourcePack); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/ducks/IChatPacket.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/ducks/IChatPacket.java new file mode 100644 index 00000000..e58fa85e --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/ducks/IChatPacket.java @@ -0,0 +1,10 @@ +package com.mumfrey.liteloader.common.ducks; + +import net.minecraft.util.IChatComponent; + +public interface IChatPacket +{ + public abstract IChatComponent getChatComponent(); + + public abstract void setChatComponent(IChatComponent chatComponent); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/ducks/IPacketClientSettings.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/ducks/IPacketClientSettings.java new file mode 100644 index 00000000..9795a5f7 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/ducks/IPacketClientSettings.java @@ -0,0 +1,6 @@ +package com.mumfrey.liteloader.common.ducks; + +public interface IPacketClientSettings +{ + public abstract int getViewDistance(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinC15PacketClientSettings.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinC15PacketClientSettings.java new file mode 100644 index 00000000..b5c14f79 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinC15PacketClientSettings.java @@ -0,0 +1,20 @@ +package com.mumfrey.liteloader.common.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.mumfrey.liteloader.common.ducks.IPacketClientSettings; + +import net.minecraft.network.play.client.C15PacketClientSettings; + +@Mixin(C15PacketClientSettings.class) +public abstract class MixinC15PacketClientSettings implements IPacketClientSettings +{ + @Shadow private int view; + + @Override + public int getViewDistance() + { + return this.view; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinItemInWorldManager.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinItemInWorldManager.java new file mode 100644 index 00000000..b37ae3fb --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinItemInWorldManager.java @@ -0,0 +1,42 @@ +package com.mumfrey.liteloader.common.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.mumfrey.liteloader.core.Proxy; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.server.management.ItemInWorldManager; +import net.minecraft.util.BlockPos; +import net.minecraft.util.EnumFacing; +import net.minecraft.world.World; + +@Mixin(ItemInWorldManager.class) +public abstract class MixinItemInWorldManager +{ + @Inject( + method = "onBlockClicked(Lnet/minecraft/util/BlockPos;Lnet/minecraft/util/EnumFacing;)V", + cancellable = true, + at = @At("HEAD") + ) + private void onBlockClicked(BlockPos pos, EnumFacing side, CallbackInfo ci) + { + Proxy.onBlockClicked(ci, (ItemInWorldManager)(Object)this, pos, side); + } + + @Inject( + method = "activateBlockOrUseItem(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/world/World;Lnet/minecraft/item/ItemStack;" + + "Lnet/minecraft/util/BlockPos;Lnet/minecraft/util/EnumFacing;FFF)Z", + cancellable = true, + at = @At("HEAD") + ) + private void onUseItem(EntityPlayer player, World worldIn, ItemStack stack, BlockPos pos, EnumFacing side, float offsetX, float offsetY, + float offsetZ, CallbackInfoReturnable cir) + { + Proxy.onUseItem(cir, player, worldIn, stack, pos, side, offsetX, offsetY, offsetZ); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinMinecraftServer.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinMinecraftServer.java new file mode 100644 index 00000000..1ffa13fd --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinMinecraftServer.java @@ -0,0 +1,20 @@ +package com.mumfrey.liteloader.common.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.mumfrey.liteloader.core.Proxy; + +import net.minecraft.server.MinecraftServer; + +@Mixin(MinecraftServer.class) +public abstract class MixinMinecraftServer +{ + @Inject(method = "updateTimeLightAndEntities()V", at = @At("HEAD")) + private void onServerTick(CallbackInfo ci) + { + Proxy.onServerTick((MinecraftServer)(Object)this); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinNetHandlerPlayServer.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinNetHandlerPlayServer.java new file mode 100644 index 00000000..054ef435 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinNetHandlerPlayServer.java @@ -0,0 +1,91 @@ +package com.mumfrey.liteloader.common.mixin; + +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.At.Shift; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Surrogate; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import com.mumfrey.liteloader.core.Proxy; + +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.play.client.C03PacketPlayer; +import net.minecraft.network.play.client.C07PacketPlayerDigging; +import net.minecraft.network.play.client.C08PacketPlayerBlockPlacement; +import net.minecraft.network.play.client.C0APacketAnimation; +import net.minecraft.world.WorldServer; + +@Mixin(NetHandlerPlayServer.class) +public abstract class MixinNetHandlerPlayServer +{ + @Inject( + method = "processPlayerBlockPlacement(Lnet/minecraft/network/play/client/C08PacketPlayerBlockPlacement;)V", + cancellable = true, + at = @At( + value = "INVOKE", + shift = Shift.AFTER, + target = "Lnet/minecraft/network/PacketThreadUtil;checkThreadAndEnqueue" + + "(Lnet/minecraft/network/Packet;Lnet/minecraft/network/INetHandler;Lnet/minecraft/util/IThreadListener;)V" + ) + ) + private void onPlaceBlock(C08PacketPlayerBlockPlacement packetIn, CallbackInfo ci) + { + Proxy.onPlaceBlock(ci, (NetHandlerPlayServer)(Object)this, packetIn); + } + + @Inject( + method = "handleAnimation(Lnet/minecraft/network/play/client/C0APacketAnimation;)V", + cancellable = true, + at = @At( + value = "INVOKE", + shift = Shift.AFTER, + target = "Lnet/minecraft/network/PacketThreadUtil;checkThreadAndEnqueue" + + "(Lnet/minecraft/network/Packet;Lnet/minecraft/network/INetHandler;Lnet/minecraft/util/IThreadListener;)V" + ) + ) + private void onClickedAir(C0APacketAnimation packetIn, CallbackInfo ci) + { + Proxy.onClickedAir(ci, (NetHandlerPlayServer)(Object)this, packetIn); + } + + @Inject( + method = "processPlayerDigging(Lnet/minecraft/network/play/client/C07PacketPlayerDigging;)V", + cancellable = true, + at = @At( + value = "INVOKE", + shift = Shift.AFTER, + target = "Lnet/minecraft/network/PacketThreadUtil;checkThreadAndEnqueue" + + "(Lnet/minecraft/network/Packet;Lnet/minecraft/network/INetHandler;Lnet/minecraft/util/IThreadListener;)V" + ) + ) + private void onPlayerDigging(C07PacketPlayerDigging packetIn, CallbackInfo ci) + { + Proxy.onPlayerDigging(ci, (NetHandlerPlayServer)(Object)this, packetIn); + } + + @Inject( + method = "processPlayer(Lnet/minecraft/network/play/client/C03PacketPlayer;)V", + cancellable = true, + locals = LocalCapture.CAPTURE_FAILHARD, + at = @At( + value = "FIELD", + opcode = Opcodes.GETFIELD, + target = "Lnet/minecraft/entity/Entity;posY:D", + ordinal = 4 + ) + ) + private void onPlayerMoved(C03PacketPlayer packetIn, CallbackInfo ci, WorldServer world, double oldPosX, double oldPosY, double oldPosZ, + double deltaMoveSq, double deltaX, double deltaY, double deltaZ) + { + Proxy.onPlayerMoved(ci, (NetHandlerPlayServer)(Object)this, packetIn, world, oldPosX, oldPosY, oldPosZ, deltaMoveSq, deltaX, deltaY, deltaZ); + } + + @Surrogate + private void onPlayerMoved(C03PacketPlayer packetIn, CallbackInfo ci, WorldServer world, double oldPosX, double oldPosY, double oldPosZ) + { + Proxy.onPlayerMoved(ci, (NetHandlerPlayServer)(Object)this, packetIn, world, oldPosX, oldPosY, oldPosZ); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinS02PacketChat.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinS02PacketChat.java new file mode 100644 index 00000000..3a913c67 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinS02PacketChat.java @@ -0,0 +1,27 @@ +package com.mumfrey.liteloader.common.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.mumfrey.liteloader.common.ducks.IChatPacket; + +import net.minecraft.network.play.server.S02PacketChat; +import net.minecraft.util.IChatComponent; + +@Mixin(S02PacketChat.class) +public abstract class MixinS02PacketChat implements IChatPacket +{ + @Shadow private IChatComponent chatComponent; + + @Override + public IChatComponent getChatComponent() + { + return this.chatComponent; + } + + @Override + public void setChatComponent(IChatComponent chatComponent) + { + this.chatComponent = chatComponent; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinServerConfigurationManager.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinServerConfigurationManager.java new file mode 100644 index 00000000..52b9db40 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/mixin/MixinServerConfigurationManager.java @@ -0,0 +1,68 @@ +package com.mumfrey.liteloader.common.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Surrogate; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.mojang.authlib.GameProfile; +import com.mumfrey.liteloader.core.Proxy; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.NetworkManager; +import net.minecraft.server.management.ServerConfigurationManager; + +@Mixin(ServerConfigurationManager.class) +public abstract class MixinServerConfigurationManager +{ + @Inject( + method = "initializeConnectionToPlayer(Lnet/minecraft/network/NetworkManager;Lnet/minecraft/entity/player/EntityPlayerMP;)V", + at = @At("RETURN") + ) + private void onInitializePlayerConnection(NetworkManager netManager, EntityPlayerMP player, CallbackInfo ci) + { + Proxy.onInitializePlayerConnection((ServerConfigurationManager)(Object)this, netManager, player); + } + + // Because, forge + @Surrogate + private void onInitializePlayerConnection(NetworkManager netManager, EntityPlayerMP player, NetHandlerPlayServer nhps, CallbackInfo ci) + { + Proxy.onInitializePlayerConnection((ServerConfigurationManager)(Object)this, netManager, player); + } + + @Inject(method = "playerLoggedIn(Lnet/minecraft/entity/player/EntityPlayerMP;)V", at = @At("RETURN")) + private void onPlayerLogin(EntityPlayerMP player, CallbackInfo ci) + { + Proxy.onPlayerLogin((ServerConfigurationManager)(Object)this, player); + } + + @Inject(method = "playerLoggedOut(Lnet/minecraft/entity/player/EntityPlayerMP;)V", at = @At("RETURN")) + private void onPlayerLogout(EntityPlayerMP player, CallbackInfo ci) + { + Proxy.onPlayerLogout((ServerConfigurationManager)(Object)this, player); + } + + @Inject( + method = "createPlayerForUser(Lcom/mojang/authlib/GameProfile;)Lnet/minecraft/entity/player/EntityPlayerMP;", + cancellable = true, + at = @At("RETURN") + ) + private void onSpawnPlayer(GameProfile profile, CallbackInfoReturnable cir) + { + Proxy.onSpawnPlayer(cir, (ServerConfigurationManager)(Object)this, profile); + } + + @Inject( + method = "recreatePlayerEntity(Lnet/minecraft/entity/player/EntityPlayerMP;IZ)Lnet/minecraft/entity/player/EntityPlayerMP;", + cancellable = true, + at = @At("RETURN") + ) + private void onRespawnPlayer(EntityPlayerMP player, int dimension, boolean conqueredEnd, CallbackInfoReturnable cir) + { + Proxy.onRespawnPlayer(cir, (ServerConfigurationManager)(Object)this, player, dimension, conqueredEnd); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/LiteLoaderPacketTransformer.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/LiteLoaderPacketTransformer.java new file mode 100644 index 00000000..1ac2dbc9 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/LiteLoaderPacketTransformer.java @@ -0,0 +1,24 @@ +package com.mumfrey.liteloader.common.transformers; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.core.runtime.Packets; +import com.mumfrey.liteloader.transformers.event.EventInjectionTransformer; +import com.mumfrey.liteloader.transformers.event.InjectionPoint; +import com.mumfrey.liteloader.transformers.event.MethodInfo; +import com.mumfrey.liteloader.transformers.event.inject.MethodHead; + +public class LiteLoaderPacketTransformer extends EventInjectionTransformer +{ + @Override + protected void addEvents() + { + InjectionPoint methodHead = new MethodHead(); + MethodInfo handlePacket = new MethodInfo(Obf.PacketEvents, "handlePacket"); + + for (Packets packet : Packets.packets) + { + MethodInfo processPacket = new MethodInfo(packet, Obf.processPacket, Void.TYPE, Obf.INetHandler); + this.addEvent(new PacketEvent(packet), processPacket, methodHead).addListener(handlePacket); + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/PacketEvent.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/PacketEvent.java new file mode 100644 index 00000000..0faf231d --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/PacketEvent.java @@ -0,0 +1,65 @@ +package com.mumfrey.liteloader.common.transformers; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.VarInsnNode; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.core.runtime.Packets; +import com.mumfrey.liteloader.transformers.event.Event; +import com.mumfrey.liteloader.transformers.event.EventInfo; + +/** + * Special event used to hook all packets + * + * @author Adam Mummery-Smith + */ +public class PacketEvent extends Event +{ + /** + * Soft index for this packet, used as a lookup for speed when determining + * handlers. + */ + private int packetIndex; + + PacketEvent(Packets packet) + { + super("on" + packet.getShortName(), true, 1000); + this.packetIndex = packet.getIndex(); + this.verbose = false; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.transformers.event.Event + * #getEventInfoClassName() + */ + @Override + public String getEventInfoClassName() + { + return "com/mumfrey/liteloader/common/transformers/PacketEventInfo"; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.transformers.event.Event + * #invokeEventInfoConstructor(org.objectweb.asm.tree.InsnList, + * boolean) + */ + @Override + protected int invokeEventInfoConstructor(InsnList insns, boolean cancellable, boolean pushReturnValue, int marshallVar) + { + int ctorMAXS = 0; + + insns.add(new LdcInsnNode(this.name)); ctorMAXS++; + insns.add(this.methodIsStatic ? new InsnNode(Opcodes.ACONST_NULL) : new VarInsnNode(Opcodes.ALOAD, 0)); ctorMAXS++; + insns.add(new InsnNode(cancellable ? Opcodes.ICONST_1 : Opcodes.ICONST_0)); ctorMAXS++; + insns.add(new IntInsnNode(Opcodes.BIPUSH, this.packetIndex)); + insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, this.eventInfoClass, Obf.constructor.name, + EventInfo.getConstructorDescriptor().replace(")", "I)"), false)); + + return ctorMAXS; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/PacketEventInfo.java b/liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/PacketEventInfo.java new file mode 100644 index 00000000..d498dc41 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/common/transformers/PacketEventInfo.java @@ -0,0 +1,23 @@ +package com.mumfrey.liteloader.common.transformers; + +import com.mumfrey.liteloader.transformers.event.EventInfo; + +import net.minecraft.network.Packet; + +public class PacketEventInfo extends EventInfo +{ + private final int packetId; + + @SuppressWarnings("unchecked") + public PacketEventInfo(String name, Object source, boolean cancellable, int packetId) + { + super(name, (S)source, cancellable); + + this.packetId = packetId; + } + + public int getPacketId() + { + return this.packetId; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/BadContainerInfo.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/BadContainerInfo.java new file mode 100644 index 00000000..8841b084 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/BadContainerInfo.java @@ -0,0 +1,64 @@ +package com.mumfrey.liteloader.core; + +import com.mumfrey.liteloader.core.api.LoadableModFile; +import com.mumfrey.liteloader.interfaces.Loadable; + +/** + * ModInfo for invalid containers + * + * @author Adam Mummery-Smith + */ +public class BadContainerInfo extends NonMod +{ + /** + * Reason the container could not be loaded + */ + private final String reason; + + public BadContainerInfo(Loadable container, String reason) + { + super(container, false); + this.reason = reason; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#isToggleable() + */ + @Override + public boolean isToggleable() + { + return false; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#isValid() + */ + @Override + public boolean isValid() + { + return false; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getDescription() + */ + @Override + public String getDescription() + { + return "\247c" + this.reason; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getVersion() + */ + @Override + public String getVersion() + { + if (this.container instanceof LoadableModFile) + { + return "supported: \247c" + ((LoadableModFile)this.container).getTargetVersion() + "\247r"; + } + + return "supported: \247cUnknown"; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/ClientPluginChannels.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/ClientPluginChannels.java new file mode 100644 index 00000000..a450a897 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/ClientPluginChannels.java @@ -0,0 +1,196 @@ +package com.mumfrey.liteloader.core; + +import net.minecraft.network.INetHandler; +import net.minecraft.network.PacketBuffer; +import net.minecraft.network.play.server.S3FPacketCustomPayload; + +import com.mumfrey.liteloader.PluginChannelListener; +import com.mumfrey.liteloader.api.Listener; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.interfaces.FastIterableDeque; +import com.mumfrey.liteloader.permissions.PermissionsManagerClient; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Handler for client plugin channels + * + * @author Adam Mummery-Smith + */ +public abstract class ClientPluginChannels extends PluginChannels +{ + private static ClientPluginChannels instance; + + protected ClientPluginChannels() + { + if (ClientPluginChannels.instance != null) throw new RuntimeException("Plugin Channels Startup Error", + new InstantiationException("Only a single instance of ClientPluginChannels is allowed")); + ClientPluginChannels.instance = this; + } + + @Override + protected FastIterableDeque createHandlerList() + { + return new HandlerList(PluginChannelListener.class); + } + + protected static ClientPluginChannels getInstance() + { + return ClientPluginChannels.instance; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#initProvider() + */ + @Override + public void initProvider() + { + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#getListenerBaseType() + */ + @Override + public Class getListenerBaseType() + { + return Listener.class; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider + * #registerInterfaces( + * com.mumfrey.liteloader.core.InterfaceRegistrationDelegate) + */ + @Override + public void registerInterfaces(InterfaceRegistrationDelegate delegate) + { + delegate.registerInterface(PluginChannelListener.class); + } + + void addClientPluginChannelListener(PluginChannelListener pluginChannelListener) + { + super.addPluginChannelListener(pluginChannelListener); + } + + /** + * Callback for the plugin channel hook + * + * @param customPayload + */ + public abstract void onPluginChannelMessage(S3FPacketCustomPayload customPayload); + + /** + * @param channel + * @param data + */ + protected void onPluginChannelMessage(String channel, PacketBuffer data) + { + if (PluginChannels.CHANNEL_REGISTER.equals(channel)) + { + this.onRegisterPacketReceived(data); + } + else if (this.pluginChannels.containsKey(channel)) + { + try + { + PermissionsManagerClient permissionsManager = LiteLoader.getClientPermissionsManager(); + if (permissionsManager != null) + { + permissionsManager.onCustomPayload(channel, data); + } + } + catch (Exception ex) {} + + this.onModPacketReceived(channel, data); + } + } + + /** + * @param channel + * @param data + */ + protected void onModPacketReceived(String channel, PacketBuffer data) + { + for (PluginChannelListener pluginChannelListener : this.pluginChannels.get(channel)) + { + try + { + pluginChannelListener.onCustomPayload(channel, data); + throw new RuntimeException(); + } + catch (Exception ex) + { + int failCount = 1; + if (this.faultingPluginChannelListeners.containsKey(pluginChannelListener)) + { + failCount = this.faultingPluginChannelListeners.get(pluginChannelListener).intValue() + 1; + } + + if (failCount >= PluginChannels.WARN_FAULT_THRESHOLD) + { + LiteLoaderLogger.warning("Plugin channel listener %s exceeded fault threshold on channel %s with %s", + pluginChannelListener.getName(), channel, ex.getClass().getSimpleName()); + this.faultingPluginChannelListeners.remove(pluginChannelListener); + } + else + { + this.faultingPluginChannelListeners.put(pluginChannelListener, Integer.valueOf(failCount)); + } + } + } + } + + protected void sendRegisteredPluginChannels(INetHandler netHandler) + { + // Add the permissions manager channels + this.addPluginChannelsFor(LiteLoader.getClientPermissionsManager()); + + try + { + // Enumerate mods for plugin channels + for (PluginChannelListener pluginChannelListener : this.pluginChannelListeners) + { + this.addPluginChannelsFor(pluginChannelListener); + } + + PacketBuffer registrationData = this.getRegistrationData(); + if (registrationData != null) + { + this.sendRegistrationData(netHandler, registrationData); + } + } + catch (Exception ex) + { + LiteLoaderLogger.warning(ex, "Error dispatching REGISTER packet to server %s", ex.getClass().getSimpleName()); + } + } + + /** + * @param netHandler + * @param registrationData + */ + protected abstract void sendRegistrationData(INetHandler netHandler, PacketBuffer registrationData); + + /** + * Send a message to the server on a plugin channel + * + * @param channel Channel to send, must not be a reserved channel name + * @param data + */ + public static boolean sendMessage(String channel, PacketBuffer data, ChannelPolicy policy) + { + if (ClientPluginChannels.instance != null) + { + return ClientPluginChannels.instance.send(channel, data, policy); + } + + return false; + } + + /** + * Send a message to the server on a plugin channel + * + * @param channel Channel to send, must not be a reserved channel name + * @param data + */ + protected abstract boolean send(String channel, PacketBuffer data, ChannelPolicy policy); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/CommonPluginChannelListener.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/CommonPluginChannelListener.java new file mode 100644 index 00000000..8efc5156 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/CommonPluginChannelListener.java @@ -0,0 +1,22 @@ +package com.mumfrey.liteloader.core; + +import java.util.List; + +import com.mumfrey.liteloader.api.Listener; + +/** + * Common interface for the client/server plugin channel listeners. Do not + * implement this interface directly, nothing will happen! + * + * @author Adam Mummery-Smith + */ +public interface CommonPluginChannelListener extends Listener +{ + /** + * Return a list of the plugin channels the mod wants to register. + * + * @return plugin channel names as a list, it is recommended to use + * {@link com.google.common.collect.ImmutableList#of} for this purpose + */ + public abstract List getChannels(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/Containers.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/Containers.java new file mode 100644 index 00000000..6bac1968 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/Containers.java @@ -0,0 +1,172 @@ +package com.mumfrey.liteloader.core; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.mumfrey.liteloader.api.ContainerRegistry; +import com.mumfrey.liteloader.interfaces.Loadable; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.interfaces.TweakContainer; + +/** + * Implementation of ContainerRegistry for LiteLoaderEnumerator + * + * @author Adam Mummery-Smith + */ +class Containers implements ContainerRegistry +{ + /** + * Mod containers which are disabled + */ + private final Map>> disabledContainers = new HashMap>>(); + + /** + * Mapping of identifiers to mod containers + */ + private final Map> enabledContainers = new HashMap>(); + + /** + * Map of containers which cannot be loaded to reasons + */ + private final Set>> badContainers = new HashSet>>(); + + /** + * Tweaks to inject + */ + private final List> tweakContainers = new ArrayList>(); + + /** + * Other tweak-containing jars which we have injected + */ + private final List>> injectedTweaks = new ArrayList>>(); + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry#getDisabledContainers() + */ + @Override + public Collection>> getDisabledContainers() + { + return this.disabledContainers.values(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry#getEnabledContainers() + */ + @Override + public Collection> getEnabledContainers() + { + return this.enabledContainers.values(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry#getBadContainers() + */ + @Override + public Collection>> getBadContainers() + { + return this.badContainers; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry#getTweakContainers() + */ + @Override + public List> getTweakContainers() + { + return this.tweakContainers; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry#getInjectedTweaks() + */ + @Override + public List>> getInjectedTweaks() + { + return this.injectedTweaks; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry + * #isDisabledContainer(com.mumfrey.liteloader.interfaces.LoadableMod) + */ + @Override + public boolean isDisabledContainer(LoadableMod container) + { + return this.disabledContainers.containsValue(container); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry + * #getEnabledContainer(java.lang.String) + */ + @Override + public LoadableMod getEnabledContainer(String identifier) + { + LoadableMod container = this.enabledContainers.get(identifier); + return container != null ? container : LoadableMod.NONE; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry + * #registerBadContainer(com.mumfrey.liteloader.interfaces.Loadable, + * java.lang.String) + */ + @Override + public void registerBadContainer(Loadable container, String reason) + { + this.badContainers.add(new BadContainerInfo(container, reason)); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry + * #registerEnabledContainer( + * com.mumfrey.liteloader.interfaces.LoadableMod) + */ + @Override + public void registerEnabledContainer(LoadableMod container) + { + this.disabledContainers.remove(container.getIdentifier()); + this.enabledContainers.put(container.getIdentifier(), container); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry + * #registerDisabledContainer( + * com.mumfrey.liteloader.interfaces.LoadableMod, + * com.mumfrey.liteloader.api.ContainerRegistry.DisabledReason) + */ + @Override + public void registerDisabledContainer(LoadableMod container, DisabledReason reason) + { + this.enabledContainers.remove(container.getIdentifier()); + this.disabledContainers.put(container.getIdentifier(), new NonMod(container, false)); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry + * #registerTweakContainer( + * com.mumfrey.liteloader.interfaces.TweakContainer) + */ + @Override + public void registerTweakContainer(TweakContainer container) + { + this.tweakContainers.add(container); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.ContainerRegistry + * #registerInjectedTweak( + * com.mumfrey.liteloader.interfaces.TweakContainer) + */ + @Override + public void registerInjectedTweak(TweakContainer container) + { + this.injectedTweaks.add(new NonMod(container, true)); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/EnabledModsList.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/EnabledModsList.java new file mode 100644 index 00000000..d50e1b56 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/EnabledModsList.java @@ -0,0 +1,252 @@ +package com.mumfrey.liteloader.core; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Serialisable (via GSON) object which stores list of enabled/disabled mods for + * each profile. + * + * @author Adam Mummery-Smith + */ +public final class EnabledModsList +{ + @SuppressWarnings("unused") + private static final transient long serialVersionUID = -6449451105617763769L; + + /** + * Gson object for serialisation/deserialisation + */ + private static transient Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + /** + * This is the node which gets serialised + */ + private TreeMap> mods; + + /** + * By default, when we discover a mod which is NOT in the list for the + * current profile, we will ENABLE the mod and add it to the list. However, + * when we receive a list of mods on the command line, we instead want to + * disable any additional unlisted mods, we also don't want to save + * the mods list because the command line is supposed to be an override + * rather than a new mask. These two values provide this behaviour. + */ + private transient Boolean defaultEnabledValue = Boolean.TRUE; + private transient boolean allowSave = true; + + /** + * JSON file containing the list of enabled/disabled mods by profile + */ + private transient File enabledModsFile = null; + + private EnabledModsList() + { + // Private because we are always instanced by the static createFrom() method below + } + + /** + * Check whether a particular mod is enabled + * + * @param profileName + * @param identifier + */ + public boolean isEnabled(String profileName, String identifier) + { + Map profile = this.getProfile(profileName); + identifier = identifier.toLowerCase().trim(); + + if (!profile.containsKey(identifier)) + { + profile.put(identifier, this.defaultEnabledValue); + } + + return profile.get(identifier); + } + + /** + * Set the enablement state of a mod in the specified profile + * + * @param profileName + * @param identifier + * @param enabled + */ + public void setEnabled(String profileName, String identifier, boolean enabled) + { + Map profile = this.getProfile(profileName); + profile.put(identifier.toLowerCase().trim(), Boolean.valueOf(enabled)); + + this.allowSave = true; + } + + /** + * Reads the mods list passed in on the command line + * + * @param profileName + * @param modNameFilter + */ + public void processModsList(String profileName, List modNameFilter) + { + Map profile = this.getProfile(profileName); + + try + { + if (modNameFilter != null) + { + for (String modName : profile.keySet()) + { + profile.put(modName, Boolean.FALSE); + } + + this.defaultEnabledValue = Boolean.FALSE; + this.allowSave = false; + + for (String filterEntry : modNameFilter) + { + profile.put(filterEntry.toLowerCase().trim(), Boolean.TRUE); + } + } + } + catch (Exception ex) + { + this.defaultEnabledValue = Boolean.TRUE; + this.allowSave = true; + } + } + + /** + * Internal method which returns the map for the specified profile + * + * @param profileName + */ + private Map getProfile(String profileName) + { + if (profileName == null) profileName = "default"; + if (this.mods == null) this.mods = new TreeMap>(); + + if (!this.mods.containsKey(profileName)) + { + this.mods.put(profileName, new TreeMap()); + } + + return this.mods.get(profileName); + } + + /** + * Factory method which tries to deserialise the enablement list from the + * file or if failing creates and returns a new instance. + * + * @param file JSON file to create the EnabledModsList from + * @return a new EnabledModsList instance + */ + public static EnabledModsList createFrom(File file) + { + if (file.exists()) + { + FileReader reader = null; + + try + { + reader = new FileReader(file); + EnabledModsList instance = gson.fromJson(reader, EnabledModsList.class); + instance.setEnabledModsFile(file); + return instance; + } + catch (Exception ex) + { + ex.printStackTrace(); + } + finally + { + try + { + if (reader != null) + { + reader.close(); + } + } + catch (IOException ex) + { + ex.printStackTrace(); + } + } + } + + EnabledModsList instance = new EnabledModsList(); + instance.setEnabledModsFile(file); + return instance; + } + + /** + * Save the enablement list to the specified file + * + * @param file + */ + public void saveTo(File file) + { + if (!this.allowSave) return; + + FileWriter writer = null; + + try + { + writer = new FileWriter(file); + gson.toJson(this, writer); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + finally + { + try + { + if (writer != null) + { + writer.close(); + } + } + catch (IOException ex) + { + ex.printStackTrace(); + } + } + } + + /** + * Save to the file we were loaded from + */ + public void save() + { + if (this.enabledModsFile != null) + { + this.saveTo(this.enabledModsFile); + } + } + + /** + * Get whether saving this list is allowed + */ + public boolean saveAllowed() + { + return this.allowSave; + } + + public File getEnabledModsFile() + { + return this.enabledModsFile; + } + + public void setEnabledModsFile(File enabledModsFile) + { + this.enabledModsFile = enabledModsFile; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/IEventState.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/IEventState.java new file mode 100644 index 00000000..a976adf7 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/IEventState.java @@ -0,0 +1,9 @@ +package com.mumfrey.liteloader.core; + +import net.minecraft.server.MinecraftServer; + + +public interface IEventState +{ + public abstract void onTick(MinecraftServer server); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/InterfaceRegistrationDelegate.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/InterfaceRegistrationDelegate.java new file mode 100644 index 00000000..47c167e8 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/InterfaceRegistrationDelegate.java @@ -0,0 +1,93 @@ +package com.mumfrey.liteloader.core; + +import java.util.ArrayList; +import java.util.List; + +import com.mumfrey.liteloader.api.Listener; +import com.mumfrey.liteloader.api.InterfaceProvider; +import com.mumfrey.liteloader.core.LiteLoaderInterfaceManager.InterfaceHandler; +import com.mumfrey.liteloader.interfaces.InterfaceRegistry; + +/** + * Delegate passed in to an InterfaceProvider's registerInterfaces method + + * @author Adam Mummery-Smith + */ +public class InterfaceRegistrationDelegate +{ + /** + * Registry which this delegate is delegating for + */ + private final InterfaceRegistry registry; + + /** + * InterfaceProvider being queried + */ + private final InterfaceProvider provider; + + /** + * The registry temporarily stores the list of handlers here + */ + private final List handlers = new ArrayList(); + + /** + * @param registry + * @param provider + */ + InterfaceRegistrationDelegate(InterfaceRegistry registry, InterfaceProvider provider) + { + this.registry = registry; + this.provider = provider; + } + + /** + * @param handler + */ + void addHandler(InterfaceHandler handler) + { + this.handlers.add(handler); + } + + /** + * + */ + List getHandlers() + { + return this.handlers; + } + + /** + * + */ + void registerInterfaces() + { + this.provider.registerInterfaces(this); + } + + /** + * @param interfaceType + */ + public void registerInterface(Class interfaceType) + { + this.registry.registerInterface(this.provider, interfaceType); + } + + /** + * @param interfaceType + * @param priority + */ + public void registerInterface(Class interfaceType, int priority) + { + this.registry.registerInterface(this.provider, interfaceType, priority); + } + + /** + * @param interfaceType + * @param priority + * @param exclusive + */ + public void registerInterface(Class interfaceType, int priority, boolean exclusive) + { + this.registry.registerInterface(this.provider, interfaceType, priority, exclusive); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoader.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoader.java new file mode 100644 index 00000000..088723f5 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoader.java @@ -0,0 +1,1023 @@ +package com.mumfrey.liteloader.core; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.activity.InvalidActivityException; + +import org.spongepowered.asm.mixin.MixinEnvironment; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.api.*; +import com.mumfrey.liteloader.api.manager.APIAdapter; +import com.mumfrey.liteloader.api.manager.APIProvider; +import com.mumfrey.liteloader.common.GameEngine; +import com.mumfrey.liteloader.common.LoadingProgress; +import com.mumfrey.liteloader.core.api.LiteLoaderCoreAPI; +import com.mumfrey.liteloader.core.event.EventProxy; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.crashreport.CallableLaunchWrapper; +import com.mumfrey.liteloader.crashreport.CallableLiteLoaderBrand; +import com.mumfrey.liteloader.crashreport.CallableLiteLoaderMods; +import com.mumfrey.liteloader.interfaces.*; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderEnvironment.EnvironmentType; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.messaging.MessageBus; +import com.mumfrey.liteloader.modconfig.ConfigManager; +import com.mumfrey.liteloader.modconfig.Exposable; +import com.mumfrey.liteloader.permissions.PermissionsManagerClient; +import com.mumfrey.liteloader.permissions.PermissionsManagerServer; +import com.mumfrey.liteloader.transformers.event.EventTransformer; +import com.mumfrey.liteloader.util.Input; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; +import net.minecraft.launchwrapper.LaunchClassLoader; +import net.minecraft.network.EnumConnectionState; +import net.minecraft.network.INetHandler; +import net.minecraft.network.play.server.S01PacketJoinGame; +import net.minecraft.profiler.Profiler; +import net.minecraft.world.World; + +/** + * LiteLoader is a simple loader which loads and provides useful callbacks to + * lightweight mods + * + * @author Adam Mummery-Smith + */ +public final class LiteLoader +{ + /** + * LiteLoader is a singleton, this is the singleton instance + */ + private static LiteLoader instance; + + /** + * Tweak system class loader + */ + private static LaunchClassLoader classLoader; + + /** + * Reference to the game engine instance + */ + private GameEngine engine; + + /** + * Minecraft Profiler + */ + private Profiler profiler; + + /** + * Loader environment instance + */ + private final LoaderEnvironment environment; + + /** + * Loader Properties adapter + */ + private final LoaderProperties properties; + + /** + * Mod enumerator instance + */ + private final LoaderEnumerator enumerator; + + /** + * Mods + */ + protected final LiteLoaderMods mods; + + /** + * API Provider instance + */ + private final APIProvider apiProvider; + + /** + * API Adapter instance + */ + private final APIAdapter apiAdapter; + + /** + * Our core API instance + */ + private final LiteLoaderCoreAPI api; + + /** + * Factory which can be used to instance main loader helper objects + */ + private final ObjectFactory objectFactory; + + /** + * Core providers + */ + private final FastIterableDeque coreProviders = new HandlerList(CoreProvider.class); + private final FastIterableDeque tickObservers = new HandlerList(TickObserver.class); + private final FastIterableDeque worldObservers = new HandlerList(WorldObserver.class); + private final FastIterableDeque shutdownObservers = new HandlerList(ShutdownObserver.class); + private final FastIterableDeque postRenderObservers = new HandlerList(PostRenderObserver.class); + + /** + * Mod panel manager, deliberately raw + */ + @SuppressWarnings("rawtypes") + private PanelManager panelManager; + + /** + * Interface Manager + */ + private LiteLoaderInterfaceManager interfaceManager; + + /** + * Event manager + */ + private LiteLoaderEventBroker events; + + /** + * Plugin channel manager + */ + private final ClientPluginChannels clientPluginChannels; + + /** + * Server channel manager + */ + private final ServerPluginChannels serverPluginChannels; + + /** + * Permission Manager + */ + private final PermissionsManagerClient permissionsManagerClient; + + private final PermissionsManagerServer permissionsManagerServer; + + /** + * Mod configuration manager + */ + private final ConfigManager configManager; + + /** + * Flag which keeps track of whether late initialisation has completed + */ + private boolean modInitComplete; + + /** + * + */ + private Input input; + + /** + * + */ + private final List translators = new ArrayList(); + + /** + * ctor + * + * @param environment + * @param properties + */ + private LiteLoader(LoaderEnvironment environment, LoaderProperties properties) + { + this.environment = environment; + this.properties = properties; + this.enumerator = environment.getEnumerator(); + + this.configManager = new ConfigManager(); + + this.mods = new LiteLoaderMods(this, environment, properties, this.configManager); + + this.apiProvider = environment.getAPIProvider(); + this.apiAdapter = environment.getAPIAdapter(); + + this.api = this.apiProvider.getAPI(LiteLoaderCoreAPI.class); + if (this.api == null) + { + throw new IllegalStateException("The core API was not registered. Startup halted"); + } + + this.objectFactory = this.api.getObjectFactory(); + + this.input = this.objectFactory.getInput(); + + this.clientPluginChannels = this.objectFactory.getClientPluginChannels(); + this.serverPluginChannels = this.objectFactory.getServerPluginChannels(); + + this.permissionsManagerClient = this.objectFactory.getClientPermissionManager(); + this.permissionsManagerServer = this.objectFactory.getServerPermissionManager(); + + this.initTranslators(); + } + + /** + * + */ + protected void initTranslators() + { + for (LiteAPI api : this.apiProvider.getAPIs()) + { + List customisationProviders = api.getCustomisationProviders(); + if (customisationProviders != null) + { + for (CustomisationProvider provider : customisationProviders) + { + if (provider instanceof TranslationProvider) + { + this.translators.add((TranslationProvider)provider); + } + } + } + } + } + + /** + * Set up reflection methods required by the loader + */ + private void onInit() + { + try + { + this.coreProviders.addAll(this.apiAdapter.getCoreProviders()); + this.tickObservers.addAll(this.apiAdapter.getAllObservers(TickObserver.class)); + this.worldObservers.addAll(this.apiAdapter.getAllObservers(WorldObserver.class)); + this.shutdownObservers.addAll(this.apiAdapter.getAllObservers(ShutdownObserver.class)); + this.postRenderObservers.addAll(this.apiAdapter.getAllObservers(PostRenderObserver.class)); + + this.coreProviders.all().onInit(); + + this.enumerator.onInit(); + this.mods.init(this.apiAdapter.getAllObservers(ModLoadObserver.class)); + } + catch (Throwable th) + { + LiteLoaderLogger.severe(th, "Error initialising LiteLoader", th); + } + } + + /** + * + */ + private void onPostInit() + { + LoadingProgress.setMessage("LiteLoader POSTINIT..."); + + this.initLifetimeObjects(); + + this.postInitCoreProviders(); + + // Spawn mod instances and initialise them + this.loadAndInitMods(); + + this.coreProviders.all().onPostInitComplete(this.mods); + + // Save stuff + this.properties.writeProperties(); + } + + /** + * Get the singleton instance of LiteLoader, initialises the loader if + * necessary. + * + * @return LiteLoader instance + */ + public static final LiteLoader getInstance() + { + return LiteLoader.instance; + } + + /** + * Get the tweak system classloader + */ + public static LaunchClassLoader getClassLoader() + { + return LiteLoader.classLoader; + } + + /** + * Get LiteLoader version + */ + public static final String getVersion() + { + return LiteLoaderVersion.CURRENT.getLoaderVersion(); + } + + /** + * Get LiteLoader version + */ + public static final String getVersionDisplayString() + { + return String.format("LiteLoader %s", LiteLoaderVersion.CURRENT.getLoaderVersion()); + } + + /** + * Get the loader revision + */ + public static final int getRevision() + { + return LiteLoaderVersion.CURRENT.getLoaderRevision(); + } + + /** + * Get all active API instances + */ + public static final LiteAPI[] getAPIs() + { + LiteAPI[] apis = LiteLoader.instance.apiProvider.getAPIs(); + LiteAPI[] apisCopy = new LiteAPI[apis.length]; + System.arraycopy(apis, 0, apisCopy, 0, apis.length); + return apisCopy; + } + + /** + * Get an API instance by identifier (returns null if no instance matching + * the supplied identifier exists). + * + * @param identifier + */ + public static final LiteAPI getAPI(String identifier) + { + return LiteLoader.instance.apiProvider.getAPI(identifier); + } + + /** + * @param identifier + */ + public static boolean isAPIAvailable(String identifier) + { + return LiteLoader.getAPI(identifier) != null; + } + + @SuppressWarnings("unchecked") + public static final C getCustomisationProvider(LiteAPI api, Class providerType) + { + List customisationProviders = api.getCustomisationProviders(); + if (customisationProviders != null) + { + for (CustomisationProvider provider : customisationProviders) + { + if (providerType.isAssignableFrom(provider.getClass())) return (C)provider; + } + } + + return null; + } + + /** + * Get the client-side permissions manager + */ + public static PermissionsManagerClient getClientPermissionsManager() + { + return LiteLoader.instance.permissionsManagerClient; + } + + /** + * Get the server-side permissions manager + */ + public static PermissionsManagerServer getServerPermissionsManager() + { + return LiteLoader.instance.permissionsManagerServer; + } + + /** + * Get the current game engine wrapper + */ + public static GameEngine getGameEngine() + { + return LiteLoader.instance.engine; + } + + /** + * Get the interface manager + */ + public static LiteLoaderInterfaceManager getInterfaceManager() + { + return LiteLoader.instance.interfaceManager; + } + + /** + * Get the client-side plugin channel manager + */ + public static ClientPluginChannels getClientPluginChannels() + { + return LiteLoader.instance.clientPluginChannels; + } + + /** + * Get the server-side plugin channel manager + */ + public static ServerPluginChannels getServerPluginChannels() + { + return LiteLoader.instance.serverPluginChannels; + } + + /** + * Get the input manager + */ + public static Input getInput() + { + return LiteLoader.instance.input; + } + + /** + * Get the mod panel manager + */ + @SuppressWarnings({ "cast", "unchecked" }) + public static PanelManager getModPanelManager() + { + return (PanelManager)LiteLoader.instance.panelManager; + } + + /** + * Get the "mods" folder + */ + public static File getModsFolder() + { + return LiteLoader.instance.environment.getModsFolder(); + } + + /** + * Get the common (version-independent) config folder + */ + public static File getCommonConfigFolder() + { + return LiteLoader.instance.environment.getCommonConfigFolder(); + } + + /** + * Get the config folder for this version + */ + public static File getConfigFolder() + { + return LiteLoader.instance.environment.getVersionedConfigFolder(); + } + + /** + * Get the game directory + */ + public static File getGameDirectory() + { + return LiteLoader.instance.environment.getGameDirectory(); + } + + /** + * Get the "assets" root directory + */ + public static File getAssetsDirectory() + { + return LiteLoader.instance.environment.getAssetsDirectory(); + } + + /** + * Get the name of the profile which launched the game + */ + public static String getProfile() + { + return LiteLoader.instance.environment.getProfile(); + } + + /** + * Get the type of environment (client or dedicated server) + */ + public static EnvironmentType getEnvironmentType() + { + return LiteLoader.instance.environment.getType(); + } + + /** + * Used to get the name of the modpack being used + * + * @return name of the modpack in use or null if no pack + */ + public static String getBranding() + { + return LiteLoader.instance.properties.getBranding(); + } + + /** + * Get whether the current environment is MCP + */ + public static boolean isDevelopmentEnvironment() + { + return "true".equals(System.getProperty("mcpenv")); + } + + /** + * Dump debugging information to the console + */ + public static void dumpDebugInfo() + { + if (LiteLoaderLogger.DEBUG) + { + EventTransformer.dumpInjectionState(); + MixinEnvironment.getCurrentEnvironment().audit(); + LiteLoaderLogger.info("Debug info dumped to console"); + } + else + { + LiteLoaderLogger.info("Debug dump not available, developer flag not enabled"); + } + } + + /** + * Used for crash reporting, returns a text list of all loaded mods + * + * @return List of loaded mods as a string + */ + public String getLoadedModsList() + { + return this.mods.getLoadedModsList(); + } + + /** + * Get a list containing all loaded mods + */ + public List getLoadedMods() + { + List loadedMods = new ArrayList(); + + for (ModInfo> loadedMod : this.mods.getLoadedMods()) + { + loadedMods.add(loadedMod.getMod()); + } + + return loadedMods; + } + + /** + * Get a list containing all mod files which were NOT loaded + */ + public List> getDisabledMods() + { + List> disabledMods = new ArrayList>(); + + for (ModInfo disabledMod : this.mods.getDisabledMods()) + { + disabledMods.add(disabledMod.getContainer()); + } + + return disabledMods; + } + + /** + * Get the list of injected tweak containers + */ + @SuppressWarnings("unchecked") + public Collection> getInjectedTweaks() + { + Collection> tweaks = new ArrayList>(); + + for (ModInfo> tweak : this.mods.getInjectedTweaks()) + { + tweaks.add((Loadable)tweak.getContainer()); + } + + return tweaks; + } + + /** + * Get a reference to a loaded mod, if the mod exists + * + * @param modName Mod's name, identifier or class name + * @throws InvalidActivityException + */ + public T getMod(String modName) throws InvalidActivityException, IllegalArgumentException + { + if (!this.modInitComplete) + { + throw new InvalidActivityException("Attempted to get a reference to a mod before loader startup is complete"); + } + + return this.mods.getMod(modName); + } + + /** + * Get a reference to a loaded mod, if the mod exists + * + * @param modClass Mod class + */ + public T getMod(Class modClass) + { + if (!this.modInitComplete) + { + throw new RuntimeException("Attempted to get a reference to a mod before loader startup is complete"); + } + + return this.mods.getMod(modClass); + } + + /** + * Get whether the specified mod is installed + * + * @param modName + */ + public boolean isModInstalled(String modName) + { + if (!this.modInitComplete || modName == null) return false; + + return this.mods.isModInstalled(modName); + } + + /** + * Get a metadata value for the specified mod + * + * @param modNameOrId + * @param metaDataKey + * @param defaultValue + * @throws IllegalArgumentException Thrown by getMod if argument is null + */ + public String getModMetaData(String modNameOrId, String metaDataKey, String defaultValue) throws IllegalArgumentException + { + return this.mods.getModMetaData(modNameOrId, metaDataKey, defaultValue); + } + + /** + * Get a metadata value for the specified mod + * + * @param mod + * @param metaDataKey + * @param defaultValue + */ + public String getModMetaData(LiteMod mod, String metaDataKey, String defaultValue) + { + return this.mods.getModMetaData(mod, metaDataKey, defaultValue); + } + + /** + * Get a metadata value for the specified mod + * + * @param modClass + * @param metaDataKey + * @param defaultValue + */ + public String getModMetaData(Class modClass, String metaDataKey, String defaultValue) + { + return this.mods.getModMetaData(modClass, metaDataKey, defaultValue); + } + + /** + * Get the mod identifier, this is used for versioning, exclusivity, and + * enablement checks. + * + * @param modClass + */ + public String getModIdentifier(Class modClass) + { + return this.mods.getModIdentifier(modClass); + } + + /** + * Get the mod identifier, this is used for versioning, exclusivity, and + * enablement checks. + * + * @param mod + */ + public String getModIdentifier(LiteMod mod) + { + return this.mods.getModIdentifier(mod); + } + + /** + * Get the container (mod file, classpath jar or folder) for the specified + * mod. + * + * @param modClass + */ + public LoadableMod getModContainer(Class modClass) + { + return this.mods.getModContainer(modClass); + } + + /** + * Get the container (mod file, classpath jar or folder) for the specified + * mod. + * + * @param mod + */ + public LoadableMod getModContainer(LiteMod mod) + { + return this.mods.getModContainer(mod); + } + + /** + * Get the mod which matches the specified identifier + * + * @param identifier + */ + public Class getModFromIdentifier(String identifier) + { + return this.mods.getModFromIdentifier(identifier); + } + + /** + * @param identifier Identifier of the mod to enable + */ + public void enableMod(String identifier) + { + this.mods.setModEnabled(identifier, true); + } + + /** + * @param identifier Identifier of the mod to disable + */ + public void disableMod(String identifier) + { + this.mods.setModEnabled(identifier, false); + } + + /** + * @param identifier Identifier of the mod to enable/disable + * @param enabled + */ + public void setModEnabled(String identifier, boolean enabled) + { + this.mods.setModEnabled(identifier, enabled); + } + + /** + * @param modName + */ + public boolean isModEnabled(String modName) + { + return this.mods.isModEnabled(modName); + } + + /** + * @param modName + */ + public boolean isModActive(String modName) + { + return this.mods.isModActive(modName); + } + + /** + * @param exposable + */ + public void writeConfig(Exposable exposable) + { + this.configManager.invalidateConfig(exposable); + } + + /** + * Register an arbitrary Exposable + * + * @param exposable Exposable object to register + * @param fileName Override config file name to use (leave null to use value + * from ExposableConfig specified value) + */ + public void registerExposable(Exposable exposable, String fileName) + { + this.configManager.registerExposable(exposable, fileName, true); + this.configManager.initConfig(exposable); + } + + /** + * Initialise lifetime objects like the game engine, event broker and + * interface manager. + */ + private void initLifetimeObjects() + { + // Cache game engine reference + this.engine = this.objectFactory.getGameEngine(); + + // Cache profiler instance + this.profiler = this.objectFactory.getGameEngine().getProfiler(); + + // Create the event broker + this.events = this.objectFactory.getEventBroker(); + if (this.events != null) + { + this.events.setMods(this.mods); + } + + // Get the mod panel manager + this.panelManager = this.objectFactory.getPanelManager(); + if (this.panelManager != null) + { + this.panelManager.init(this.mods, this.configManager); + } + + // Create the interface manager + this.interfaceManager = new LiteLoaderInterfaceManager(this.apiAdapter); + } + + /** + * + */ + private void postInitCoreProviders() + { + this.coreProviders.all().onPostInit(this.engine); + + this.interfaceManager.registerInterfaces(); + + for (CoreProvider provider : this.coreProviders) + { + if (provider instanceof Listener) + { + this.interfaceManager.registerListener((Listener)provider); + } + } + } + + private void loadAndInitMods() + { + int totalMods = this.enumerator.modsToLoadCount(); + int totalTweaks = this.enumerator.getInjectedTweaks().size(); + LiteLoaderLogger.info(Verbosity.REDUCED, "Discovered %d total mod(s), injected %d tweak(s)", totalMods, totalTweaks); + + if (totalMods > 0) + { + this.mods.loadMods(); + this.mods.initMods(); + } + else + { + LiteLoaderLogger.info(Verbosity.REDUCED, "No mod classes were found. Not loading any mods."); + } + + // Initialises the required hooks for loaded mods + this.interfaceManager.onPostInit(); + + this.modInitComplete = true; + this.mods.onPostInit(); + } + + void onPostInitMod(LiteMod mod) + { + // add mod to permissions manager if permissible + if (this.permissionsManagerClient != null) + { + this.permissionsManagerClient.registerMod(mod); + } + } + + /** + * Called after mod late init + */ + void onStartupComplete() + { + // Set the loader branding in ClientBrandRetriever using reflection + LiteLoaderBootstrap.setBranding("LiteLoader"); + + this.coreProviders.all().onStartupComplete(); + + if (this.panelManager != null) + { + this.panelManager.onStartupComplete(); + } + + MessageBus.getInstance().onStartupComplete(); + + // Force packet injections + EnumConnectionState.values(); + } + + /** + * Called on login + * + * @param netHandler + * @param loginPacket + */ + void onJoinGame(INetHandler netHandler, S01PacketJoinGame loginPacket) + { + if (this.permissionsManagerClient != null) + { + this.permissionsManagerClient.onJoinGame(netHandler, loginPacket); + } + + this.coreProviders.all().onJoinGame(netHandler, loginPacket); + } + + /** + * Called when the world reference is changed + * + * @param world + */ + void onWorldChanged(World world) + { + if (world != null && this.permissionsManagerClient != null) + { + // For bungeecord + this.permissionsManagerClient.scheduleRefresh(); + } + + this.worldObservers.all().onWorldChanged(world); + } + + /** + * @param mouseX + * @param mouseY + * @param partialTicks + */ + void onPostRender(int mouseX, int mouseY, float partialTicks) + { + this.profiler.startSection("core"); + this.postRenderObservers.all().onPostRender(mouseX, mouseY, partialTicks); + this.profiler.endSection(); + } + + /** + * @param clock + * @param partialTicks + * @param inGame + */ + void onTick(boolean clock, float partialTicks, boolean inGame) + { + if (clock) + { + // Tick the permissions manager + if (this.permissionsManagerClient != null) + { + this.profiler.startSection("permissionsmanager"); + this.permissionsManagerClient.onTick(this.engine, partialTicks, inGame); + this.profiler.endSection(); + } + + // Tick the config manager + this.profiler.startSection("configmanager"); + this.configManager.onTick(); + this.profiler.endSection(); + + if (!this.engine.isRunning()) + { + this.onShutDown(); + return; + } + } + + this.profiler.startSection("observers"); + + this.tickObservers.all().onTick(clock, partialTicks, inGame); + + this.profiler.endSection(); + } + + private void onShutDown() + { + LiteLoaderLogger.info(Verbosity.REDUCED, "LiteLoader is shutting down, shutting down core providers and syncing configuration"); + + this.shutdownObservers.all().onShutDown(); + + this.configManager.syncConfig(); + } + + public static String translate(String key, Object... args) + { + for (TranslationProvider translator : LiteLoader.instance.translators) + { + String translated = translator.translate(key, args); + if (translated != null) + { + return translated; + } + } + + return key; + } + + /** + * @param objCrashReport This is an object so that we don't need to + * transform the obfuscated name in the transformer + */ + public static void populateCrashReport(Object objCrashReport) + { + if (objCrashReport instanceof CrashReport) + { + EventProxy.populateCrashReport((CrashReport)objCrashReport); + LiteLoader.populateCrashReport((CrashReport)objCrashReport); + } + } + + private static void populateCrashReport(CrashReport crashReport) + { + CrashReportCategory category = crashReport.getCategory(); // crashReport.makeCategoryDepth("Mod System Details", 1); + category.addCrashSectionCallable("Mod Pack", new CallableLiteLoaderBrand(crashReport)); + category.addCrashSectionCallable("LiteLoader Mods", new CallableLiteLoaderMods(crashReport)); + category.addCrashSectionCallable("LaunchWrapper", new CallableLaunchWrapper(crashReport)); + } + + static final void createInstance(LoaderEnvironment environment, LoaderProperties properties, LaunchClassLoader classLoader) + { + if (LiteLoader.instance == null) + { + LiteLoader.classLoader = classLoader; + LiteLoader.instance = new LiteLoader(environment, properties); + } + } + + static final void invokeInit() + { + LiteLoaderLogger.info(Verbosity.REDUCED, "LiteLoader begin INIT..."); + + LiteLoader.instance.onInit(); + } + + static final void invokePostInit() + { + LiteLoaderLogger.info(Verbosity.REDUCED, "LiteLoader begin POSTINIT..."); + + LiteLoader.instance.onPostInit(); + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderBootstrap.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderBootstrap.java new file mode 100644 index 00000000..50fd7919 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderBootstrap.java @@ -0,0 +1,775 @@ +package com.mumfrey.liteloader.core; + +import java.io.*; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Properties; + +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.FileAppender; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.spongepowered.asm.mixin.MixinEnvironment; + +import com.mumfrey.liteloader.api.LiteAPI; +import com.mumfrey.liteloader.api.manager.APIAdapter; +import com.mumfrey.liteloader.api.manager.APIProvider; +import com.mumfrey.liteloader.api.manager.APIRegistry; +import com.mumfrey.liteloader.common.LoadingProgress; +import com.mumfrey.liteloader.core.api.LiteLoaderCoreAPI; +import com.mumfrey.liteloader.interfaces.LoaderEnumerator; +import com.mumfrey.liteloader.launch.*; +import com.mumfrey.liteloader.util.ObfuscationUtilities; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +import net.minecraft.launchwrapper.ITweaker; +import net.minecraft.launchwrapper.Launch; +import net.minecraft.launchwrapper.LaunchClassLoader; + +/** + * LiteLoaderBootstrap is responsible for managing the early part of the + * LiteLoader startup process, this is to ensure that NONE of the Minecraft + * classes which by necessity the Loader references get loaded before the + * PREINIT stage has completed. This allows us to load transforming tweakers in + * the PREINIT stage without all hell breaking loose because class names have + * changed between initialisation stages! + * + *

      This class handles setting up requisite resources like the logger, + * enumerator and plug-in API modules and passes init calls through to the + * LiteLoader instance at the appropriate points during startup. Because this + * class is the first part of the loader to get loaded, we also keep central + * references like the paths, version and loader properties in here.

      + * + * @author Adam Mummery-Smith + */ +class LiteLoaderBootstrap implements LoaderBootstrap, LoaderEnvironment, LoaderProperties +{ + /** + * Base game directory, passed in from the tweaker + */ + private final File gameDirectory; + + /** + * Assets directory, passed in from the tweaker + */ + private final File assetsDirectory; + + /** + * Active profile, passed in from the tweaker + */ + private final String profile; + + /** + * "Mods" folder to use + */ + private final File modsFolder; + + /** + * "Mods" folder to use + */ + private final File versionedModsFolder; + + /** + * Base "liteconfig" folder under which all other lite mod configs and + * liteloader configs are placed. + */ + private final File configBaseFolder; + + /** + * Folder containing version-independent configuration + */ + private final File commonConfigFolder; + + /** + * Folder containing version-specific configuration + */ + private final File versionConfigFolder; + + /** + * File to write log entries to + */ + private File logFile; + + /** + * File containing the properties + */ + private File propertiesFile; + + /** + * JSON file containing the list of enabled/disabled mods by profile + */ + private File enabledModsFile; + + /** + * Internal properties loaded from inside the jar + */ + private Properties internalProperties = new Properties(); + + /** + * LiteLoader properties + */ + private Properties localProperties = new Properties(); + + /** + * Pack brand from properties, used to put the modpack/compilation name in + * crash reports + */ + private String branding = null; + + private boolean loadTweaks = true; + + private LaunchClassLoader classLoader; + + private final ITweaker tweaker; + + private final APIRegistry apiRegistry; + + private final APIProvider apiProvider; + + private final APIAdapter apiAdapter; + + private final EnvironmentType environmentType; + + /** + * The mod enumerator instance + */ + private LiteLoaderEnumerator enumerator; + + /** + * List of mods passed into the command line + */ + private EnabledModsList enabledModsList; + + /** + * @param env + * @param tweaker + */ + public LiteLoaderBootstrap(StartupEnvironment env, ITweaker tweaker) + { + this.environmentType = EnvironmentType.values()[env.getEnvironmentTypeId()]; + this.tweaker = tweaker; + + this.apiRegistry = new APIRegistry(this.getEnvironment(), this.getProperties()); + + this.gameDirectory = env.getGameDirectory(); + this.assetsDirectory = env.getAssetsDirectory(); + this.profile = env.getProfile(); + this.modsFolder = env.getModsFolder(); + + this.versionedModsFolder = new File(this.modsFolder, LiteLoaderVersion.CURRENT.getMinecraftVersion()); + this.configBaseFolder = new File(this.gameDirectory, "liteconfig"); + this.logFile = new File(this.configBaseFolder, "liteloader.log"); + this.propertiesFile = new File(this.configBaseFolder, "liteloader.properties"); + this.enabledModsFile = new File(this.configBaseFolder, "liteloader.profiles.json"); + + this.commonConfigFolder = new File(this.configBaseFolder, "common"); + this.versionConfigFolder = this.inflectVersionedConfigPath(LiteLoaderVersion.CURRENT); + + if (!this.modsFolder.exists()) this.modsFolder.mkdirs(); + if (!this.versionedModsFolder.exists()) this.versionedModsFolder.mkdirs(); + if (!this.configBaseFolder.exists()) this.configBaseFolder.mkdirs(); + if (!this.commonConfigFolder.exists()) this.commonConfigFolder.mkdirs(); + if (!this.versionConfigFolder.exists()) this.versionConfigFolder.mkdirs(); + + this.initAPIs(env.getAPIsToLoad()); + this.apiProvider = this.apiRegistry.getProvider(); + this.apiAdapter = this.apiRegistry.getAdapter(); + } + + /** + * @param version + */ + @Override + public File inflectVersionedConfigPath(LiteLoaderVersion version) + { + if (version.equals(LiteLoaderVersion.LEGACY)) + { + return this.modsFolder; + } + + return new File(this.configBaseFolder, String.format("config.%s", version.getMinecraftVersion())); + } + + /** + * + */ + private void initAPIs(List apisToLoad) + { + if (apisToLoad != null) + { + for (String apiClassName : apisToLoad) + this.registerAPI(apiClassName); + } + + this.apiRegistry.bake(); + } + + /** + * @param apiClassName + */ + public void registerAPI(String apiClassName) + { + this.apiRegistry.registerAPI(apiClassName); + } + + @Override + public APIProvider getAPIProvider() + { + return this.apiProvider; + } + + @Override + public APIAdapter getAPIAdapter() + { + return this.apiAdapter; + } + + @Override + public EnabledModsList getEnabledModsList() + { + return this.enabledModsList; + } + + @Override + public LoaderEnumerator getEnumerator() + { + return this.enumerator; + } + + @Override + public EnvironmentType getType() + { + return this.environmentType; + } + + @Override + public LoaderEnvironment getEnvironment() + { + return this; + } + + @Override + public LoaderProperties getProperties() + { + return this; + } + + @Override + public boolean addCascadedTweaker(String tweakClass, int priority) + { + if (this.tweaker instanceof LiteLoaderTweaker) + { + return ((LiteLoaderTweaker)this.tweaker).addCascadedTweaker(tweakClass, priority); + } + + return false; + } + + @Override + public ClassTransformerManager getTransformerManager() + { + if (this.tweaker instanceof LiteLoaderTweaker) + { + return ((LiteLoaderTweaker)this.tweaker).getTransformerManager(); + } + + return null; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.launch.ILoaderBootstrap + * #preInit(net.minecraft.launchwrapper.LaunchClassLoader, boolean) + */ + @Override + public void preInit(LaunchClassLoader classLoader, boolean loadTweaks, List modsToLoad) + { + this.classLoader = classLoader; + this.loadTweaks = loadTweaks; + + LiteLoaderLogger.info(Verbosity.REDUCED, "LiteLoader begin PREINIT..."); + + // Set up the bootstrap + if (!this.prepare()) return; + + LiteLoaderLogger.info(Verbosity.REDUCED, "LiteLoader %s starting up...", LiteLoaderVersion.CURRENT.getLoaderVersion()); + + // Print the branding version if any was provided + if (this.branding != null) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Active Pack: %s", this.branding); + } + + LiteLoaderLogger.info(Verbosity.REDUCED, "Java reports OS=\"%s\"", System.getProperty("os.name").toLowerCase()); + + this.enabledModsList = EnabledModsList.createFrom(this.enabledModsFile); + this.enabledModsList.processModsList(this.profile, modsToLoad); + + this.enumerator = this.spawnEnumerator(classLoader); + this.enumerator.onPreInit(); + + this.initMixins(); + + LiteLoaderLogger.info(Verbosity.REDUCED, "LiteLoader PREINIT complete"); + } + + private void initMixins() + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Initialising LiteLoader Mixins"); + this.getAPIAdapter().initMixins(); + } + + /** + * @param classLoader + */ + protected LiteLoaderEnumerator spawnEnumerator(LaunchClassLoader classLoader) + { + return new LiteLoaderEnumerator(this, this, classLoader); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.launch.LoaderBootstrap#beginGame() + */ + @Override + public void preBeginGame() + { + LiteAPI api = this.getAPIProvider().getAPI("liteloader"); + if (api instanceof LiteLoaderCoreAPI) + { + ((LiteLoaderCoreAPI)api).getObjectFactory().preBeginGame(); + } + + LoadingProgress.setEnabled(this.getAndStoreBooleanProperty(LoaderProperties.OPTION_LOADING_BAR, true)); + + if (ObfuscationUtilities.fmlIsPresent()) + { + LiteLoaderLogger.info("FML detected, switching to searge mappings"); + MixinEnvironment.getDefaultEnvironment().setObfuscationContext("searge"); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.launch.ILoaderBootstrap + * #init(java.util.List, net.minecraft.launchwrapper.LaunchClassLoader) + */ + @Override + public void init() + { + // PreInit failed + if (this.enumerator == null) return; + + LiteLoader.createInstance(this.getEnvironment(), this.getProperties(), this.classLoader); + LiteLoader.invokeInit(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.launch.ILoaderBootstrap#postInit() + */ + @Override + public void postInit() + { + // PreInit failed + if (this.enumerator == null) return; + + LiteLoader.invokePostInit(); + } + + /** + * Set up reflection methods required by the loader + */ + private boolean prepare() + { + try + { + // Prepare the properties + this.prepareProperties(); + + // Prepare the log writer + this.prepareLogger(); + + this.prepareBranding(); + } + catch (Throwable th) + { + LiteLoaderLogger.severe(th, "Error initialising LiteLoader Bootstrap"); + return false; + } + + return true; + } + + /** + * @throws SecurityException + * @throws IOException + */ + private void prepareLogger() throws SecurityException + { + LiteLoaderLogger.info("Setting up logger..."); + + Logger logger = LiteLoaderLogger.getLogger(); + Layout layout = PatternLayout.createLayout("[%d{HH:mm:ss}] [%t/%level]: %msg%n", + logger.getContext().getConfiguration(), null, "UTF-8", "True"); + FileAppender fileAppender = FileAppender.createAppender(this.logFile.getAbsolutePath(), "False", "False", + "LiteLoader", "True", "True", "True", layout, null, "False", "", logger.getContext().getConfiguration()); + fileAppender.start(); + logger.addAppender(fileAppender); + } + + /** + * Prepare the loader properties + */ + private void prepareProperties() + { + LiteLoaderLogger.info("Initialising Loader properties..."); + + try + { + InputStream propertiesStream = LiteLoaderBootstrap.class.getResourceAsStream("/liteloader.properties"); + + if (propertiesStream != null) + { + this.internalProperties.load(propertiesStream); + propertiesStream.close(); + } + } + catch (Throwable th) + { + this.internalProperties = new Properties(); + } + + try + { + this.localProperties = new Properties(this.internalProperties); + InputStream localPropertiesStream = this.getLocalPropertiesStream(); + + if (localPropertiesStream != null) + { + this.localProperties.load(localPropertiesStream); + localPropertiesStream.close(); + } + } + catch (Throwable th) + { + this.localProperties = new Properties(this.internalProperties); + } + } + + /** + * Get the properties stream either from the jar or from the properties file + * in the minecraft folder + * + * @throws FileNotFoundException + */ + private InputStream getLocalPropertiesStream() throws FileNotFoundException + { + if (this.propertiesFile.exists()) + { + return new FileInputStream(this.propertiesFile); + } + + // Otherwise read settings from the config + return LiteLoaderBootstrap.class.getResourceAsStream("/liteloader.properties"); + } + + /** + * Write current properties to the properties file + */ + @Override + public void writeProperties() + { + try + { + this.localProperties.store(new FileWriter(this.propertiesFile), String.format("Properties for LiteLoader %s", LiteLoaderVersion.CURRENT)); + } + catch (Throwable th) + { + LiteLoaderLogger.warning(th, "Error writing liteloader properties"); + } + } + + /** + * + */ + private void prepareBranding() + { + this.branding = this.internalProperties.getProperty(LoaderProperties.OPTION_BRAND, null); + if (this.branding != null && this.branding.length() < 1) + { + this.branding = null; + } + + // Save appropriate branding in the local properties file + if (this.branding != null) + { + this.localProperties.setProperty(LoaderProperties.OPTION_BRAND, this.branding); + } + else + { + this.localProperties.remove(LoaderProperties.OPTION_BRAND); + } + } + + /** + * Get the game directory + */ + @Override + public File getGameDirectory() + { + return this.gameDirectory; + } + + /** + * Get the assets directory + */ + @Override + public File getAssetsDirectory() + { + return this.assetsDirectory; + } + + /** + * Get the profile directory + */ + @Override + public String getProfile() + { + return this.profile; + } + + /** + * Get the mods folder + */ + @Override + public File getModsFolder() + { + return this.modsFolder; + } + + /** + * Get the mods folder + */ + @Override + public File getVersionedModsFolder() + { + return this.versionedModsFolder; + } + + /** + * Get the base "liteconfig" folder + */ + @Override + public File getConfigBaseFolder() + { + return this.configBaseFolder; + } + + /** + * Get the common configuration folder + */ + @Override + public File getCommonConfigFolder() + { + return this.commonConfigFolder; + } + + /** + * Get the versioned configuration folder + */ + @Override + public File getVersionedConfigFolder() + { + return this.versionConfigFolder; + } + + /** + * Get a boolean propery from the properties file and also write the new + * value back to the properties file. + * + * @param propertyName + * @param defaultValue + */ + @Override + public boolean getAndStoreBooleanProperty(String propertyName, boolean defaultValue) + { + boolean result = this.localProperties.getProperty(propertyName, String.valueOf(defaultValue)).equalsIgnoreCase("true"); + this.localProperties.setProperty(propertyName, String.valueOf(result)); + return result; + } + + /** + * Get a boolean propery from the properties file and also write the new + * value back to the properties file. + * + * @param propertyName + */ + @Override + public boolean getBooleanProperty(String propertyName) + { + return this.localProperties.getProperty(propertyName, "false").equalsIgnoreCase("true"); + } + + /** + * Set a boolean property + * + * @param propertyName + * @param value + */ + @Override + public void setBooleanProperty(String propertyName, boolean value) + { + this.localProperties.setProperty(propertyName, String.valueOf(value)); + } + + @Override + public int getAndStoreIntegerProperty(String propertyName, int defaultValue) + { + int result = LiteLoaderBootstrap.tryParseInt(this.localProperties.getProperty(propertyName, String.valueOf(defaultValue)), defaultValue); + this.localProperties.setProperty(propertyName, String.valueOf(result)); + return result; + } + + @Override + public int getIntegerProperty(String propertyName) + { + return LiteLoaderBootstrap.tryParseInt(this.localProperties.getProperty(propertyName, "0"), 0); + } + + @Override + public void setIntegerProperty(String propertyName, int value) + { + this.localProperties.setProperty(propertyName, String.valueOf(value)); + } + + /** + * Store current revision for mod in the config file + * + * @param modKey + */ + @Override + public void storeLastKnownModRevision(String modKey) + { + if (this.localProperties != null) + { + this.localProperties.setProperty(modKey, String.valueOf(LiteLoaderVersion.CURRENT.getLoaderRevision())); + this.writeProperties(); + } + } + + /** + * Get last know revision for mod from the config file + * + * @param modKey + */ + @Override + public int getLastKnownModRevision(String modKey) + { + if (this.localProperties != null) + { + String storedRevision = this.localProperties.getProperty(modKey, "0"); + return Integer.parseInt(storedRevision); + } + + return 0; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.launch.LoaderEnvironment#loadTweaksEnabled() + */ + @Override + public boolean loadTweaksEnabled() + { + return this.loadTweaks; + } + + /** + * Used to get the name of the modpack being used + * + * @return name of the modpack in use or null if no pack + */ + @Override + public String getBranding() + { + return this.branding; + } + + /** + * Set the brand in ClientBrandRetriever to the specified brand + * + * @param brand + */ + static void setBranding(String brand) + { + try + { + Method mGetClientModName; + + try + { + Class cbrClass = Class.forName("net.minecraft.client.ClientBrandRetriever", false, Launch.classLoader); + mGetClientModName = cbrClass.getDeclaredMethod("getClientModName"); + } + catch (ClassNotFoundException ex) + { + return; + } + + String oldBrand = (String)mGetClientModName.invoke(null); + + if ("vanilla".equals(oldBrand)) + { + char[] newValue = brand.toCharArray(); + + Field stringValue = String.class.getDeclaredField("value"); + stringValue.setAccessible(true); + stringValue.set(oldBrand, newValue); + + try + { + Field stringCount = String.class.getDeclaredField("count"); + stringCount.setAccessible(true); + stringCount.set(oldBrand, newValue.length); + } + catch (NoSuchFieldException ex) {} // java 1.7 doesn't have this member + } + } + catch (Throwable th) + { + LiteLoaderLogger.warning(th, "Setting branding failed"); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.launch.LoaderBootstrap + * #getRequiredTransformers() + */ + @Override + public List getRequiredTransformers() + { + return this.getAPIAdapter().getRequiredTransformers(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.launch.LoaderBootstrap + * #getRequiredDownstreamTransformers() + */ + @Override + public List getRequiredDownstreamTransformers() + { + List requiredDownstreamTransformers = this.getAPIAdapter().getRequiredDownstreamTransformers(); + requiredDownstreamTransformers.add(0, "com.mumfrey.liteloader.transformers.event.EventTransformer"); + return requiredDownstreamTransformers; + } + + private static int tryParseInt(String string, int defaultValue) + { + try + { + return Integer.parseInt(string); + } + catch (NumberFormatException ex) + { + return defaultValue; + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderEnumerator.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderEnumerator.java new file mode 100644 index 00000000..d94a0172 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderEnumerator.java @@ -0,0 +1,832 @@ +package com.mumfrey.liteloader.core; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.MixinEnvironment.Phase; + +import com.google.common.base.Throwables; +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.api.ContainerRegistry; +import com.mumfrey.liteloader.api.ContainerRegistry.DisabledReason; +import com.mumfrey.liteloader.api.EnumerationObserver; +import com.mumfrey.liteloader.api.EnumeratorModule; +import com.mumfrey.liteloader.api.EnumeratorPlugin; +import com.mumfrey.liteloader.api.LiteAPI; +import com.mumfrey.liteloader.api.ModClassValidator; +import com.mumfrey.liteloader.core.api.DefaultClassValidator; +import com.mumfrey.liteloader.core.api.DefaultEnumeratorPlugin; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.interfaces.FastIterableDeque; +import com.mumfrey.liteloader.interfaces.Injectable; +import com.mumfrey.liteloader.interfaces.Loadable; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.interfaces.LoaderEnumerator; +import com.mumfrey.liteloader.interfaces.MixinContainer; +import com.mumfrey.liteloader.interfaces.TweakContainer; +import com.mumfrey.liteloader.launch.ClassTransformerManager; +import com.mumfrey.liteloader.launch.LiteLoaderTweaker; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +import net.minecraft.launchwrapper.Launch; +import net.minecraft.launchwrapper.LaunchClassLoader; + +/** + * The enumerator performs all mod discovery functions for LiteLoader, this + * includes locating mod files to load as well as searching for mod classes + * within the class path and discovered mod files. + * + * @author Adam Mummery-Smith + */ +public class LiteLoaderEnumerator implements LoaderEnumerator +{ + public enum EnumeratorState + { + INIT(null), + DISCOVER(INIT), + INJECT(DISCOVER), + REGISTER(INJECT), + FINALISED(REGISTER); + + private final EnumeratorState previousState; + + private EnumeratorState(EnumeratorState previousState) + { + this.previousState = previousState; + } + + public boolean checkGotoState(EnumeratorState fromState) + { + if (fromState != this && fromState != this.previousState) + { + throw new IllegalStateException("Attempted to move to an invalid enumerator state " + this + ", expected to be in state " + + this.previousState + " but current state is " + fromState); + } + + return true; + } + } + + private final LoaderEnvironment environment; + + private final LoaderProperties properties; + + /** + * Reference to the launch classloader + */ + private final LaunchClassLoader classLoader; + + /** + * + */ + private final List modules = new ArrayList(); + + private final List plugins = new ArrayList(); + + private final ContainerRegistry containers = new Containers(); + + /** + * Containers which have already been checked for potential mod candidates + */ + private final Set> enumeratedContainers = new HashSet>(); + + /** + * Classes to load, mapped by class name + */ + private final Set>> modsToLoad = new LinkedHashSet>>(); + + private final ModClassValidator validator; + + private final FastIterableDeque observers = new HandlerList(EnumerationObserver.class); + + protected EnumeratorState state = EnumeratorState.INIT; + + /** + * @param environment + * @param properties + * @param classLoader + */ + public LiteLoaderEnumerator(LoaderEnvironment environment, LoaderProperties properties, LaunchClassLoader classLoader) + { + this.environment = environment; + this.properties = properties; + this.classLoader = classLoader; + this.validator = this.getValidator(environment); + + this.initModules(environment); + this.registerPlugin(new DefaultEnumeratorPlugin()); + + // Initialise observers + this.observers.addAll(environment.getAPIAdapter().getPreInitObservers(EnumerationObserver.class)); + + // Initialise the shared mod list if we haven't already + this.getSharedModList(); + } + + /** + * @param environment + */ + private ModClassValidator getValidator(LoaderEnvironment environment) + { + List prefixes = new ArrayList(); + + for (LiteAPI api : environment.getAPIProvider().getAPIs()) + { + String prefix = api.getModClassPrefix(); + if (prefix != null) + { + LiteLoaderLogger.info("Adding supported mod class prefix '%s'", prefix); + prefixes.add(prefix); + } + } + + return new DefaultClassValidator(LiteMod.class, prefixes); + } + + /** + * @param environment + */ + private void initModules(LoaderEnvironment environment) + { + for (LiteAPI api : environment.getAPIProvider().getAPIs()) + { + List apiModules = api.getEnumeratorModules(); + + if (apiModules != null) + { + for (EnumeratorModule module : apiModules) + { + this.registerModule(module); + } + } + } + } + + private void checkState(EnumeratorState state, String action) + { + if (this.state != state) + { + throw new IllegalStateException("Illegal enumerator state whilst performing " + action + ", expecting " + state + " but current state is " + + this.state); + } + } + + private void gotoState(EnumeratorState state) + { + if (state.checkGotoState(this.state)) + { + this.state = state; + } + } + + /** + * Get the loader environment + */ + public LoaderEnvironment getEnvironment() + { + return this.environment; + } + + /** + * Initialise the "shared" mod list if it's not already been created + */ + @Override + public Map> getSharedModList() + { + try + { + @SuppressWarnings("unchecked") + Map> sharedModList = (Map>) Launch.blackboard.get("modList"); + + if (sharedModList == null) + { + sharedModList = new HashMap>(); + Launch.blackboard.put("modList", sharedModList); + } + + return sharedModList; + } + catch (Exception ex) + { + LiteLoaderLogger.warning("Shared mod list was invalid or not accessible, this isn't especially bad but something isn't quite right"); + return null; + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.PluggableEnumerator + * #registerModule(com.mumfrey.liteloader.core.EnumeratorModule) + */ + @Override + public void registerModule(EnumeratorModule module) + { + this.checkState(EnumeratorState.INIT, "registerModule"); + + if (module != null && !this.modules.contains(module)) + { + LiteLoaderLogger.info("Registering discovery module %s: [%s]", module.getClass().getSimpleName(), module); + this.modules.add(module); + module.init(this.environment, this.properties); + } + } + + @Override + public void registerPlugin(EnumeratorPlugin plugin) + { + this.checkState(EnumeratorState.INIT, "registerPlugin"); + + if (plugin != null && !this.plugins.contains(plugin)) + { + LiteLoaderLogger.info("Registering enumerator plugin %s: [%s]", plugin.getClass().getSimpleName(), plugin); + this.plugins.add(plugin); + plugin.init(this.environment, this.properties); + } + } + + /** + * Get the list of all enumerated mod classes to load + */ + @Override + public Collection>> getModsToLoad() + { + this.checkState(EnumeratorState.FINALISED, "getModsToLoad"); + return Collections.unmodifiableSet(this.modsToLoad); + } + + /** + * Get the set of disabled containers + */ + @Override + public Collection>> getDisabledContainers() + { + this.checkState(EnumeratorState.FINALISED, "getDisabledContainers"); + return this.containers.getDisabledContainers(); + } + + @Override + public Collection>> getBadContainers() + { + this.checkState(EnumeratorState.FINALISED, "getBadContainers"); + return this.containers.getBadContainers(); + } + + /** + * Get the list of injected tweak containers + */ + @Override + public Collection>> getInjectedTweaks() + { + this.checkState(EnumeratorState.FINALISED, "getInjectedTweaks"); + return this.containers.getInjectedTweaks(); + } + + /** + * Get the number of mods to load + */ + @Override + public int modsToLoadCount() + { + return this.modsToLoad.size(); + } + + /** + * Get a metadata value for the specified mod + * + * @param modClass + * @param metaDataKey + * @param defaultValue + */ + @Override + public String getModMetaData(Class modClass, String metaDataKey, String defaultValue) + { + this.checkState(EnumeratorState.FINALISED, "getModMetaData"); + return this.getContainerForMod(modClass).getMetaValue(metaDataKey, defaultValue); + } + + /** + * @param identifier + */ + @Override + public LoadableMod getContainer(String identifier) + { + this.checkState(EnumeratorState.FINALISED, "getContainer"); + return this.containers.getEnabledContainer(identifier); + } + + /** + * @param modClass + */ + @Override + public LoadableMod getContainer(Class modClass) + { + this.checkState(EnumeratorState.FINALISED, "getContainer"); + return this.getContainerForMod(modClass); + } + + /** + * @param modClass + */ + private LoadableMod getContainerForMod(Class modClass) + { + for (ModInfo> mod : this.modsToLoad) + { + if (modClass.equals(mod.getModClass())) + { + return mod.getContainer(); + } + } + + return LoadableMod.NONE; + } + + /** + * Get the mod identifier (metadata key), this is used for versioning, + * exclusivity, and enablement checks. + * + * @param modClass + */ + @Override + public String getIdentifier(Class modClass) + { + String modClassName = modClass.getSimpleName(); + + for (ModInfo> mod : this.modsToLoad) + { + if (modClassName.equals(mod.getModClassSimpleName())) + { + return mod.getIdentifier(); + } + } + + return LiteLoaderEnumerator.getModClassName(modClass); + } + + @Override + public void onPreInit() + { + this.discoverContainers(); + this.injectDiscoveredTweaks(); + } + + /** + * Call enumerator modules in order to find mod containers + */ + private void discoverContainers() + { + this.gotoState(EnumeratorState.DISCOVER); + + for (EnumeratorModule module : this.modules) + { + try + { + module.enumerate(this, this.environment.getProfile()); + } + catch (Throwable th) + { + LiteLoaderLogger.warning(th, "Enumerator Module %s encountered an error whilst enumerating", module.getClass().getName()); + } + } + + this.checkDependencies(); + } + + private void injectDiscoveredTweaks() + { + this.gotoState(EnumeratorState.INJECT); + + for (TweakContainer tweakContainer : this.containers.getTweakContainers()) + { + this.addTweaksFrom(tweakContainer); + } + } + + /** + * Enumerate class path and discovered mod files to find mod classes + */ + @Override + public void onInit() + { + try + { + this.gotoState(EnumeratorState.INJECT); + this.injectIntoClassLoader(); + + this.gotoState(EnumeratorState.REGISTER); + this.registerMods(); + + this.gotoState(EnumeratorState.FINALISED); + LiteLoaderLogger.info("Mod class discovery completed"); + } + catch (IllegalStateException ex) // wut? + { + Throwables.propagate(ex); + } + catch (Throwable th) + { + LiteLoaderLogger.warning(th, "Mod class discovery failed"); + } + } + + /** + * + */ + private void injectIntoClassLoader() + { + for (EnumeratorModule module : this.modules) + { + try + { + module.injectIntoClassLoader(this, this.classLoader); + } + catch (Throwable th) + { + LiteLoaderLogger.warning(th, "Enumerator Module %s encountered an error whilst injecting", module.getClass().getName()); + } + } + } + + /** + * + */ + private void registerMods() + { + for (EnumeratorModule module : this.modules) + { + try + { + module.registerMods(this, this.classLoader); + } + catch (Throwable th) + { + LiteLoaderLogger.warning(th, "Enumerator Module %s encountered an error whilst registering mods", module.getClass().getName()); + } + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.interfaces.ModularEnumerator + * #registerModContainer(com.mumfrey.liteloader.interfaces.LoadableMod) + */ + @Override + public final boolean registerModContainer(LoadableMod container) + { + this.checkState(EnumeratorState.DISCOVER, "registerModContainer"); + + if (container == null) + { + return true; + } + + if (!this.checkEnabled(container)) + { + this.registerDisabledContainer(container, DisabledReason.USER_DISABLED); + return false; + } + + if (!this.checkAPIRequirements(container)) + { + this.registerDisabledContainer(container, DisabledReason.MISSING_API); + return false; + } + + this.registerEnabledContainer(container); + return true; + } + + @Override + public void registerBadContainer(Loadable container, String reason) + { + this.checkState(EnumeratorState.DISCOVER, "registerBadContainer"); + this.containers.registerBadContainer(container, reason); + } + + /** + * @param container + */ + protected void registerEnabledContainer(LoadableMod container) + { + this.checkState(EnumeratorState.DISCOVER, "registerEnabledContainer"); + this.containers.registerEnabledContainer(container); + this.observers.all().onRegisterEnabledContainer(this, container); + } + + /** + * @param container + */ + protected void registerDisabledContainer(LoadableMod container, DisabledReason reason) + { + this.checkState(EnumeratorState.DISCOVER, "registerDisabledContainer"); + + LiteLoaderLogger.info(Verbosity.REDUCED, reason.getMessage(container)); + this.containers.registerDisabledContainer(container, reason); + this.observers.all().onRegisterDisabledContainer(this, container, reason); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.PluggableEnumerator#addTweaksFrom( + * com.mumfrey.liteloader.core.TweakContainer) + */ + @Override + public boolean registerTweakContainer(TweakContainer container) + { + this.checkState(EnumeratorState.DISCOVER, "registerTweakContainer"); + + if (!container.isEnabled(this.environment)) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Mod %s is disabled for profile %s, not injecting tranformers", + container.getIdentifier(), this.environment.getProfile()); + return false; + } + + this.containers.registerTweakContainer(container); + this.observers.all().onRegisterTweakContainer(this, container); + return true; + } + + /** + * @param tweakContainer + */ + private void addTweaksFrom(TweakContainer tweakContainer) + { + this.checkState(EnumeratorState.INJECT, "addTweaksFrom"); + + if (this.checkDependencies(tweakContainer)) + { + if (tweakContainer.hasTweakClass()) + { + this.addTweakFrom(tweakContainer); + } + + if (tweakContainer.hasClassTransformers()) + { + this.addClassTransformersFrom(tweakContainer); + } + + if (tweakContainer.hasMixins()) + { + this.addMixinsFrom(tweakContainer); + } + } + } + + private void addTweakFrom(TweakContainer container) + { + try + { + String tweakClass = container.getTweakClassName(); + int tweakPriority = container.getTweakPriority(); + LiteLoaderLogger.info(Verbosity.REDUCED, "Mod file '%s' provides tweakClass '%s', adding to Launch queue with priority %d", + container.getName(), tweakClass, tweakPriority); + if (this.environment.addCascadedTweaker(tweakClass, tweakPriority)) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "tweakClass '%s' was successfully added", tweakClass); + container.injectIntoClassPath(this.classLoader, true); + + if (container.isExternalJar()) + { + this.containers.registerInjectedTweak(container); + } + + String[] classPathEntries = container.getClassPathEntries(); + if (classPathEntries != null) + { + for (String classPathEntry : classPathEntries) + { + try + { + File classPathJar = new File(this.environment.getGameDirectory(), classPathEntry); + URL classPathJarUrl = classPathJar.toURI().toURL(); + + LiteLoaderLogger.info("Adding Class-Path entry: %s", classPathEntry); + LiteLoaderTweaker.addURLToParentClassLoader(classPathJarUrl); + this.classLoader.addURL(classPathJarUrl); + } + catch (MalformedURLException ex) {} + } + } + } + } + catch (MalformedURLException ex) + { + } + } + + private void addClassTransformersFrom(TweakContainer container) + { + try + { + for (String classTransformerClass : container.getClassTransformerClassNames()) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Mod file '%s' provides classTransformer '%s', adding to class loader", + container.getName(), classTransformerClass); + ClassTransformerManager transformerManager = this.environment.getTransformerManager(); + if (transformerManager != null && transformerManager.injectTransformer(classTransformerClass)) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "classTransformer '%s' was successfully added", classTransformerClass); + this.injectContainerRecursive(container); + } + } + } + catch (MalformedURLException ex) + { + } + } + + private void addMixinsFrom(MixinContainer container) + { + for (String config : container.getMixinConfigs()) + { + if (config.endsWith(".json")) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Registering mixin config %s for %s", config, container.getName()); + MixinEnvironment.getDefaultEnvironment().addConfiguration(config); + } + else if (config.contains(".json@")) + { + int pos = config.indexOf(".json@"); + String phaseName = config.substring(pos + 6); + config = config.substring(0, pos + 5); + Phase phase = Phase.forName(phaseName); + if (phase != null) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Registering mixin config %s for %s", config, container.getName()); + MixinEnvironment.getEnvironment(phase).addConfiguration(config); + } + } + } + } + + /** + * @param container + */ + private void injectContainerRecursive(Injectable container) throws MalformedURLException + { + if (container.injectIntoClassPath(this.classLoader, true) && container instanceof LoadableMod) + { + LoadableMod file = (LoadableMod)container; + for (String dependency : file.getDependencies()) + { + LoadableMod dependencyContainer = this.containers.getEnabledContainer(dependency); + if (dependencyContainer != null) + { + this.injectContainerRecursive(dependencyContainer); + } + } + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.PluggableEnumerator#registerMods( + * com.mumfrey.liteloader.core.LoadableMod, boolean) + */ + @Override + public void registerModsFrom(LoadableMod container, boolean registerContainer) + { + this.checkState(EnumeratorState.REGISTER, "registerModsFrom"); + + if (this.containers.isDisabledContainer(container)) + { + throw new IllegalArgumentException("Attempted to register mods from a disabled container '" + container.getName() + "'"); + } + + if (this.enumeratedContainers.contains(container)) + { + // already handled this container + return; + } + + this.enumeratedContainers.add(container); + + List> modClasses = new ArrayList>(); + + for (EnumeratorPlugin plugin : this.plugins) + { + List> classes = plugin.getClasses(container, this.classLoader, this.validator); + LiteLoaderLogger.debug("Plugin %s returned %d classes for %s", plugin.getClass(), classes.size(), container.getDisplayName()); + modClasses.addAll(classes); + } + + for (Class modClass : modClasses) + { + Mod mod = new Mod(container, modClass); + this.registerMod(mod); + } + + if (modClasses.size() > 0) + { + LiteLoaderLogger.info("Found %d potential matches", modClasses.size()); + this.containers.registerEnabledContainer(container); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.interfaces.ModularEnumerator#registerMod( + * com.mumfrey.liteloader.interfaces.ModInfo) + */ + @Override + public void registerMod(ModInfo> mod) + { + this.checkState(EnumeratorState.REGISTER, "registerMod"); + + if (this.modsToLoad.contains(mod)) + { + LiteLoaderLogger.warning("Mod name collision for mod with class '%s', maybe you have more than one copy?", mod.getModClassSimpleName()); + } + + this.modsToLoad.add(mod); + + this.observers.all().onModAdded(this, mod); + } + + private boolean checkEnabled(LoadableMod container) + { + for (EnumeratorPlugin plugin : this.plugins) + { + if (!plugin.checkEnabled(this.containers, container)) return false; + } + + return true; + } + + @Override + public boolean checkAPIRequirements(LoadableMod container) + { + for (EnumeratorPlugin plugin : this.plugins) + { + if (!plugin.checkAPIRequirements(this.containers, container)) return false; + } + + return true; + } + + /** + * Check dependencies of enabled containers + */ + private void checkDependencies() + { + Collection> enabledContainers = this.containers.getEnabledContainers(); + Deque> containers = new LinkedList>(enabledContainers); + + while (containers.size() > 0) + { + LoadableMod container = containers.pop(); + if (!this.checkDependencies(container)) + { + this.registerDisabledContainer(container, DisabledReason.MISSING_DEPENDENCY); + + // Iterate so that a container disabled by a failed dependency check will also + // disable any containers which depend upon it + containers.clear(); + containers.addAll(enabledContainers); + } + } + } + + @Override + public boolean checkDependencies(LoadableMod container) + { + for (EnumeratorPlugin plugin : this.plugins) + { + if (!plugin.checkDependencies(this.containers, container)) return false; + } + + return true; + } + + @SuppressWarnings("unchecked") + public boolean checkDependencies(TweakContainer tweakContainer) + { + if (tweakContainer instanceof LoadableMod) + { + return this.checkDependencies((LoadableMod)tweakContainer); + } + + return true; + } + + public static String getModClassName(LiteMod mod) + { + return LiteLoaderEnumerator.getModClassName(mod.getClass()); + } + + public static String getModClassName(Class mod) + { + return mod.getSimpleName().substring(7); + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderEventBroker.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderEventBroker.java new file mode 100644 index 00000000..46eb1051 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderEventBroker.java @@ -0,0 +1,548 @@ +package com.mumfrey.liteloader.core; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import com.mojang.authlib.GameProfile; +import com.mumfrey.liteloader.*; +import com.mumfrey.liteloader.PlayerInteractionListener.MouseButton; +import com.mumfrey.liteloader.api.InterfaceProvider; +import com.mumfrey.liteloader.api.Listener; +import com.mumfrey.liteloader.api.ShutdownObserver; +import com.mumfrey.liteloader.common.GameEngine; +import com.mumfrey.liteloader.common.LoadingProgress; +import com.mumfrey.liteloader.common.ducks.IPacketClientSettings; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.core.event.HandlerList.ReturnLogicOp; +import com.mumfrey.liteloader.interfaces.FastIterable; +import com.mumfrey.liteloader.interfaces.FastIterableDeque; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.util.Position; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +import net.minecraft.command.ICommandManager; +import net.minecraft.command.ServerCommandManager; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.play.client.C03PacketPlayer; +import net.minecraft.network.play.client.C15PacketClientSettings; +import net.minecraft.network.play.server.S08PacketPlayerPosLook; +import net.minecraft.network.play.server.S23PacketBlockChange; +import net.minecraft.profiler.Profiler; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.management.ItemInWorldManager; +import net.minecraft.server.management.ServerConfigurationManager; +import net.minecraft.util.BlockPos; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.MovingObjectPosition.MovingObjectType; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; +import net.minecraft.world.WorldSettings; + +/** + * @author Adam Mummery-Smith + * + * @param Type of the client runtime, "Minecraft" on client and null + * on the server + * @param Type of the server runtime, "IntegratedServer" on the client + * "MinecraftServer" on the server + */ +public abstract class LiteLoaderEventBroker implements InterfaceProvider, ShutdownObserver +{ + /** + * @author Adam Mummery-Smith + * + * @param + */ + public static class ReturnValue + { + private T value; + private boolean isSet; + + public ReturnValue(T value) + { + this.value = value; + } + + public ReturnValue() + { + } + + public boolean isSet() + { + return this.isSet; + } + + public T get() + { + return this.value; + } + + public void set(T value) + { + this.isSet = true; + this.value = value; + } + } + + public static enum InteractType + { + RIGHT_CLICK, + LEFT_CLICK, + LEFT_CLICK_BLOCK, + PLACE_BLOCK_MAYBE, + DIG_BLOCK_MAYBE + } + + /** + * Singleton + */ + static LiteLoaderEventBroker broker; + + /** + * Reference to the loader instance + */ + protected final LiteLoader loader; + + /** + * Reference to the game + */ + protected final GameEngine engine; + + /** + * Profiler + */ + protected final Profiler profiler; + + protected LiteLoaderMods mods; + + private Map playerStates = new HashMap(); + private FastIterableDeque playerStateList = new HandlerList(IEventState.class); + + /** + * List of mods which provide server commands + */ + private FastIterable serverCommandProviders + = new HandlerList(ServerCommandProvider.class); + + /** + * List of mods which monitor server player events + */ + private FastIterable serverPlayerListeners + = new HandlerList(ServerPlayerListener.class); + + /** + * List of mods which handle player interaction events + */ + private FastIterable playerInteractionListeners + = new HandlerList(PlayerInteractionListener.class, ReturnLogicOp.AND); + + /** + * List of mods which handle player movement events + */ + private FastIterable playerMoveListeners + = new HandlerList(PlayerMoveListener.class, ReturnLogicOp.AND_BREAK_ON_FALSE); + + /** + * List of mods which monitor server ticks + */ + private FastIterable serverTickListeners + = new HandlerList(ServerTickable.class); + + /** + * List of mods which want to be notified when the game is shutting down + */ + private FastIterable shutdownListeners + = new HandlerList(ShutdownListener.class); + + /** + * ctor + * + * @param loader + * @param engine + * @param properties + */ + public LiteLoaderEventBroker(LiteLoader loader, GameEngine engine, LoaderProperties properties) + { + this.loader = loader; + this.engine = engine; + this.profiler = engine.getProfiler(); + + LiteLoaderEventBroker.broker = this; + } + + /** + * @param mods + */ + void setMods(LiteLoaderMods mods) + { + this.mods = mods; + } + + /** + * + */ + protected void onStartupComplete() + { + LoadingProgress.setMessage("Checking mods..."); + this.mods.onStartupComplete(); + + LoadingProgress.setMessage("Initialising CoreProviders..."); + this.loader.onStartupComplete(); + + LoadingProgress.setMessage("Starting Game..."); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#getListenerBaseType() + */ + @Override + public Class getListenerBaseType() + { + return LiteMod.class; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#registerInterfaces( + * com.mumfrey.liteloader.core.InterfaceRegistrationDelegate) + */ + @Override + public void registerInterfaces(InterfaceRegistrationDelegate delegate) + { + delegate.registerInterface(ServerCommandProvider.class); + delegate.registerInterface(ServerPlayerListener.class); + delegate.registerInterface(PlayerInteractionListener.class); + delegate.registerInterface(PlayerMoveListener.class); + delegate.registerInterface(CommonPluginChannelListener.class); + delegate.registerInterface(ServerTickable.class); + delegate.registerInterface(ShutdownListener.class); + } + + /** + * Add a listener to the relevant listener lists + * + * @param listener + */ + public void addCommonPluginChannelListener(CommonPluginChannelListener listener) + { + if (!(listener instanceof PluginChannelListener) && !(listener instanceof ServerPluginChannelListener)) + { + LiteLoaderLogger.warning("Interface error for mod '%1s'. Implementing CommonPluginChannelListener has no effect! " + + "Use PluginChannelListener or ServerPluginChannelListener instead", listener.getName()); + } + } + + /** + * @param serverCommandProvider + */ + public void addServerCommandProvider(ServerCommandProvider serverCommandProvider) + { + this.serverCommandProviders.add(serverCommandProvider); + } + + /** + * @param serverPlayerListener + */ + public void addServerPlayerListener(ServerPlayerListener serverPlayerListener) + { + this.serverPlayerListeners.add(serverPlayerListener); + } + + /** + * @param playerInteractionListener + */ + public void addPlayerInteractionListener(PlayerInteractionListener playerInteractionListener) + { + this.playerInteractionListeners.add(playerInteractionListener); + } + + /** + * @param playerMoveListener + */ + public void addPlayerMoveListener(PlayerMoveListener playerMoveListener) + { + this.playerMoveListeners.add(playerMoveListener); + } + + /** + * @param serverTickable + */ + public void addServerTickable(ServerTickable serverTickable) + { + this.serverTickListeners.add(serverTickable); + } + + /** + * @param shutdownListener + */ + public void addShutdownListener(ShutdownListener shutdownListener) + { + this.shutdownListeners.add(shutdownListener); + } + + /** + * @param instance + * @param folderName + * @param worldName + * @param worldSettings + */ + public void onStartServer(MinecraftServer instance, String folderName, String worldName, WorldSettings worldSettings) + { + ICommandManager commandManager = instance.getCommandManager(); + + if (commandManager instanceof ServerCommandManager) + { + ServerCommandManager serverCommandManager = (ServerCommandManager)commandManager; + this.serverCommandProviders.all().provideCommands(serverCommandManager); + } + + LiteLoader.getServerPluginChannels().onServerStartup(); + + this.playerStates.clear(); + } + + /** + * @param scm + * @param player + * @param profile + */ + public void onSpawnPlayer(ServerConfigurationManager scm, EntityPlayerMP player, GameProfile profile) + { + this.serverPlayerListeners.all().onPlayerConnect(player, profile); + PlayerEventState playerState = this.getPlayerState(player); + playerState.onSpawned(); + } + + /** + * @param scm + * @param player + */ + public void onPlayerLogin(ServerConfigurationManager scm, EntityPlayerMP player) + { + LiteLoader.getServerPluginChannels().onPlayerJoined(player); + } + + /** + * @param scm + * @param netManager + * @param player + */ + public void onInitializePlayerConnection(ServerConfigurationManager scm, NetworkManager netManager, EntityPlayerMP player) + { + this.serverPlayerListeners.all().onPlayerLoggedIn(player); + } + + /** + * @param scm + * @param player + * @param oldPlayer + * @param dimension + * @param won + */ + public void onRespawnPlayer(ServerConfigurationManager scm, EntityPlayerMP player, EntityPlayerMP oldPlayer, int dimension, boolean won) + { + this.serverPlayerListeners.all().onPlayerRespawn(player, oldPlayer, dimension, won); + } + + /** + * @param scm + * @param player + */ + public void onPlayerLogout(ServerConfigurationManager scm, EntityPlayerMP player) + { + this.serverPlayerListeners.all().onPlayerLogout(player); + this.removePlayer(player); + } + + /** + * @param clock + * @param partialTicks + * @param inGame + */ + protected void onTick(boolean clock, float partialTicks, boolean inGame) + { + this.loader.onTick(clock, partialTicks, inGame); + } + + /** + * @param mouseX + * @param mouseY + * @param partialTicks + */ + protected void onPostRender(int mouseX, int mouseY, float partialTicks) + { + this.loader.onPostRender(mouseX, mouseY, partialTicks); + } + + protected void onWorldChanged(World world) + { + this.loader.onWorldChanged(world); + } + + public void onServerTick(MinecraftServer server) + { + this.playerStateList.all().onTick(server); + this.serverTickListeners.all().onTick(server); + } + + public boolean onPlaceBlock(NetHandlerPlayServer netHandler, EntityPlayerMP playerMP, BlockPos pos, EnumFacing facing) + { + if (!this.onPlayerInteract(InteractType.PLACE_BLOCK_MAYBE, playerMP, pos, facing)) + { + S23PacketBlockChange cancellation = new S23PacketBlockChange(playerMP.worldObj, pos.offset(facing)); + netHandler.playerEntity.playerNetServerHandler.sendPacket(cancellation); + playerMP.sendContainerToPlayer(playerMP.inventoryContainer); + return false; + } + + return true; + } + + public boolean onClickedAir(NetHandlerPlayServer netHandler) + { + return this.onPlayerInteract(InteractType.LEFT_CLICK, netHandler.playerEntity, null, EnumFacing.SOUTH); + } + + public boolean onPlayerDigging(NetHandlerPlayServer netHandler, BlockPos pos, EntityPlayerMP playerMP) + { + if (!this.onPlayerInteract(InteractType.DIG_BLOCK_MAYBE, playerMP, pos, EnumFacing.SOUTH)) + { + S23PacketBlockChange cancellation = new S23PacketBlockChange(playerMP.worldObj, pos); + netHandler.playerEntity.playerNetServerHandler.sendPacket(cancellation); + return false; + } + + return true; + } + + public boolean onUseItem(BlockPos pos, EnumFacing side, EntityPlayerMP playerMP) + { + if (!this.onPlayerInteract(InteractType.PLACE_BLOCK_MAYBE, playerMP, pos, side)) + { + S23PacketBlockChange cancellation = new S23PacketBlockChange(playerMP.worldObj, pos); + playerMP.playerNetServerHandler.sendPacket(cancellation); + return false; + } + + return true; + } + + public boolean onBlockClicked(BlockPos pos, EnumFacing side, ItemInWorldManager manager) + { + if (!this.onPlayerInteract(InteractType.LEFT_CLICK_BLOCK, manager.thisPlayerMP, pos, side)) + { + S23PacketBlockChange cancellation = new S23PacketBlockChange(manager.theWorld, pos); + manager.thisPlayerMP.playerNetServerHandler.sendPacket(cancellation); + return false; + } + + return true; + } + + public boolean onPlayerInteract(InteractType action, EntityPlayerMP player, BlockPos position, EnumFacing side) + { + PlayerEventState eventState = this.getPlayerState(player); + return eventState.onPlayerInteract(action, player, position, side); + } + + void onPlayerClickedAir(EntityPlayerMP player, MouseButton button, BlockPos tracePos, EnumFacing traceSideHit, MovingObjectType traceHitType) + { + this.playerInteractionListeners.all().onPlayerClickedAir(player, button, tracePos, traceSideHit, traceHitType); + } + + boolean onPlayerClickedBlock(EntityPlayerMP player, MouseButton button, BlockPos hitPos, EnumFacing sideHit) + { + return this.playerInteractionListeners.all().onPlayerClickedBlock(player, button, hitPos, sideHit); + } + + public boolean onPlayerMove(NetHandlerPlayServer netHandler, C03PacketPlayer packet, EntityPlayerMP playerMP, WorldServer world) + { + Position from = new Position(playerMP, true); + + double toX = playerMP.posX; + double toY = playerMP.posY; + double toZ = playerMP.posZ; + float toYaw = playerMP.rotationYaw; + float toPitch = playerMP.rotationPitch; + + if (packet.isMoving()) + { + toX = packet.getPositionX(); + toY = packet.getPositionY(); + toZ = packet.getPositionZ(); + } + + if (packet.getRotating()) + { + toYaw = packet.getYaw(); + toPitch = packet.getPitch(); + } + + Position to = new Position(toX, toY, toZ, toYaw, toPitch); + ReturnValue pos = new ReturnValue(to); + + if (!this.playerMoveListeners.all().onPlayerMove(playerMP, from, to, pos)) + { + playerMP.setPositionAndRotation(from.xCoord, from.yCoord, from.zCoord, playerMP.prevRotationYaw, playerMP.prevRotationPitch); + playerMP.playerNetServerHandler.sendPacket(new S08PacketPlayerPosLook(from.xCoord, from.yCoord, from.zCoord, + playerMP.prevRotationYaw, playerMP.prevRotationPitch, Collections.emptySet())); + return false; + } + + if (pos.isSet()) + { + Position newPos = pos.get(); + netHandler.setPlayerLocation(newPos.xCoord, newPos.yCoord, newPos.zCoord, newPos.yaw, newPos.pitch); + return false; + } + + return true; + } + + void onPlayerSettingsReceived(EntityPlayerMP player, C15PacketClientSettings packet) + { + PlayerEventState playerState = this.getPlayerState(player); + playerState.setTraceDistance(((IPacketClientSettings)packet).getViewDistance()); + playerState.setLocale(packet.getLang()); + } + + public PlayerEventState getPlayerState(EntityPlayerMP player) + { + PlayerEventState playerState = this.playerStates.get(player.getUniqueID()); + if (playerState == null) + { + playerState = new PlayerEventState(player, this); + this.playerStates.put(player.getUniqueID(), playerState); + this.playerStateList.add(playerState); + } + return playerState; + } + + protected void removePlayer(EntityPlayerMP player) + { + PlayerEventState playerState = this.playerStates.remove(player.getUniqueID()); + if (playerState != null) + { + this.playerStateList.remove(playerState); + } + } + + @Override + public void onShutDown() + { + for (ShutdownListener listener : this.shutdownListeners) + { + try + { + listener.onShutDown(); + } + catch (Throwable th) + { + th.printStackTrace(); + } + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderInterfaceManager.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderInterfaceManager.java new file mode 100644 index 00000000..2bbcd3bc --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderInterfaceManager.java @@ -0,0 +1,526 @@ +package com.mumfrey.liteloader.core; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.mumfrey.liteloader.api.Listener; +import com.mumfrey.liteloader.api.InterfaceObserver; +import com.mumfrey.liteloader.api.InterfaceProvider; +import com.mumfrey.liteloader.api.LiteAPI; +import com.mumfrey.liteloader.api.Observer; +import com.mumfrey.liteloader.api.exceptions.InvalidProviderException; +import com.mumfrey.liteloader.api.manager.APIAdapter; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.interfaces.FastIterable; +import com.mumfrey.liteloader.interfaces.InterfaceRegistry; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +/** + * The interface manager handles the allocation of interface consumers + * (implementors) to interface providers. During startup, registered providers + * are enumerated and handler mappings are created for every consumable + * interface the provider supports. Later on, consumers are enumerated against + * the available handler mappings and registered with the providers by calling + * the appropriate registration method. + * + * @author Adam Mummery-Smith + */ +public class LiteLoaderInterfaceManager implements InterfaceRegistry +{ + static int handlerAllocationOrder = 0; + + /** + * InterfaceHandler describes a mapping of a consumable interface to an + * InterfaceProvider instance and appropriate consumer registration method + * (which will be invoked via reflection). + * + * @author Adam Mummery-Smith + */ + class InterfaceHandler + { + /** + * Priority, for sorting handlers, NYI + */ + public final int priority; + + /** + * Order, for sorting handlers, NYI + */ + public final int order; + + /** + * Indicates that this handler must be the exclusive hander for this + * interface + */ + public final boolean exclusive; + + /** + * Interface Provider which handles this mapping + */ + public final InterfaceProvider provider; + + /** + * Type of interface for this mapping + */ + public final Class interfaceType; + + /** + * List of registered listeners, so we can avoid registering the same + * listener multiple times + */ + private final List registeredListeners = new ArrayList(); + + /** + * Callback method used to + */ + private final Method registrationMethod; + + /** + * @param provider + * @param interfaceType + * @param exclusive + * @param priority + */ + public InterfaceHandler(InterfaceProvider provider, Class interfaceType, boolean exclusive, int priority) + { + this.provider = provider; + this.interfaceType = interfaceType; + this.exclusive = exclusive; + this.priority = priority; + this.order = LiteLoaderInterfaceManager.handlerAllocationOrder++; + this.registrationMethod = this.findRegistrationMethod(provider, interfaceType); + } + + /** + * @param provider + * @param interfaceType + */ + @SuppressWarnings("unchecked") + private Method findRegistrationMethod(InterfaceProvider provider, Class interfaceType) + { + Method registrationMethod = null; + + Class providerClass = provider.getClass(); + while (registrationMethod == null && providerClass != null) + { + registrationMethod = this.findRegistrationMethod(providerClass, interfaceType); + providerClass = (Class)providerClass.getSuperclass(); + } + + return registrationMethod; + } + + private Method findRegistrationMethod(Class providerClass, Class interfaceType) + { + for (Method method : providerClass.getDeclaredMethods()) + { + if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(interfaceType)) + { + LiteLoaderLogger.debug("Found method %s for registering %s with provider %s", + method.getName(), interfaceType, providerClass.getSimpleName()); + return method; + } + } + + return null; + } + + /** + * After instantiation, called to check that a valid registration method + * was located + */ + public boolean isValid() + { + return this.registrationMethod != null; + } + + /** + * Proxy method which calls the registration method in the + * InterfaceProvider using reflection + * + * @param listener + */ + public boolean registerListener(Listener listener) + { + if (this.interfaceType.isAssignableFrom(listener.getClass()) && this.provider.getListenerBaseType().isAssignableFrom(listener.getClass())) + { + if (this.registeredListeners.contains(listener)) + { + return false; + } + + try + { + LiteLoaderLogger.debug("Calling registration method %s for %s on %s with %s", this.registrationMethod.getName(), + this.interfaceType.getSimpleName(), this.provider.getClass().getSimpleName(), listener.getClass().getSimpleName()); + this.registrationMethod.invoke(this.provider, listener); + + this.registeredListeners.add(listener); + + LiteLoaderInterfaceManager.this.observers.all().onRegisterListener(this.provider, this.interfaceType, listener); + + return true; + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + return false; + } + } + + /** + * API Provider instance + */ + private final APIAdapter apiAdapter; + + /** + * Map of providers to the API which supplied them + */ + private final Map providerToAPIMap = new HashMap(); + + /** + * All providers + */ + private final List allProviders = new ArrayList(); + + /** + * All consumers + */ + private final List listeners = new ArrayList(); + + /** + * Registered interface handler mappings + */ + private final List interfaceHandlers = new ArrayList(); + + /** + * Interface observers + */ + protected final FastIterable observers = new HandlerList(InterfaceObserver.class); + + /** + * True once the initial init phase (in which all registered providers are + * initialised) is completed, we use this flag to indicate that any new + * providers should be immediately initialised. + */ + private boolean initDone = false; + + /** + * The last startup phase causes all currently registered consumers to be + * enumerated and offered to all currently registered listeners, once this + * initial registration is done any new consumers should immediately + * offered to all registered listeners. + */ + private boolean registrationDone = false; + + /** + * Registratiob Delegate which is active for the current registration + * process. + */ + private InterfaceRegistrationDelegate activeRegistrationDelegate; + + /** + * @param apiAdapter + */ + LiteLoaderInterfaceManager(APIAdapter apiAdapter) + { + this.apiAdapter = apiAdapter; + } + + /** + * Callback from the core + */ + void onPostInit() + { + this.registerQueuedListeners(); + this.initProviders(); + } + + void registerInterfaces() + { + this.apiAdapter.registerInterfaces(this); + } + + /** + * @param api + */ + @Override + public void registerAPI(LiteAPI api) + { + List apiInterfaceProviders = api.getInterfaceProviders(); + if (apiInterfaceProviders != null) + { + for (InterfaceProvider provider : apiInterfaceProviders) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Registering interface provider %s for API %s", + provider.getClass().getName(), api.getName()); + if (this.registerProvider(provider)) + { + this.providerToAPIMap.put(provider, api); + } + } + } + + List observers = this.apiAdapter.getObservers(api); + + if (observers != null) + { + for (Observer observer : observers) + { + if (observer instanceof InterfaceObserver) + { + this.registerObserver((InterfaceObserver)observer); + } + } + } + } + + /** + * Register a new interface provider + * + * @param provider + */ + public boolean registerProvider(InterfaceProvider provider) + { + if (provider != null && !this.allProviders.contains(provider)) + { + try + { + if (this.activeRegistrationDelegate != null) + { + throw new IllegalStateException("registerProvider() was called whilst a registration process was still active"); + } + + InterfaceRegistrationDelegate delegate = new InterfaceRegistrationDelegate(this, provider); + this.activeRegistrationDelegate = delegate; + this.activeRegistrationDelegate.registerInterfaces(); + this.activeRegistrationDelegate = null; + + if (this.initDone) + { + provider.initProvider(); + } + + this.allProviders.add(provider); + + this.interfaceHandlers.addAll(delegate.getHandlers()); + + return true; + } + catch (Throwable th) + { + LiteLoaderLogger.warning(th, "Error while registering interface provider %s: %s", + provider.getClass().getSimpleName(), th.getClass().getSimpleName()); + } + } + + this.activeRegistrationDelegate = null; + return false; + } + + /** + * @param observer + */ + public void registerObserver(InterfaceObserver observer) + { + this.observers.add(observer); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.interfaces.InterfaceRegistry + * #registerInterface( + * com.mumfrey.liteloader.api.InterfaceProvider, java.lang.Class) + */ + @Override + public void registerInterface(InterfaceProvider provider, Class interfaceType) + { + this.registerInterface(provider, interfaceType, 0); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.interfaces.InterfaceRegistry + * #registerInterface(com.mumfrey.liteloader.api.InterfaceProvider, + * java.lang.Class, int) + */ + @Override + public void registerInterface(InterfaceProvider provider, Class interfaceType, int priority) + { + this.registerInterface(provider, interfaceType, priority, false); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.interfaces.InterfaceRegistry + * #registerInterface(com.mumfrey.liteloader.api.InterfaceProvider, + * java.lang.Class, int, boolean) + */ + @Override + public void registerInterface(InterfaceProvider provider, Class interfaceType, int priority, boolean exclusive) + { + InterfaceHandler handler = new InterfaceHandler(provider, interfaceType, exclusive, priority); + if (handler.isValid()) + { + // Check if a this provider is already registered + if (this.getProvidersFor(interfaceType).contains(provider)) + { + throw new InvalidProviderException("Attempting to register duplicate mapping for provider " + + provider.getClass() + " to " + interfaceType); + } + + if (exclusive) + { + this.removeHandlersFor(interfaceType, priority); + } + + if (this.registrationDone) + { + this.interfaceHandlers.add(handler); + + for (Listener consumer : this.listeners) + { + handler.registerListener(consumer); + } + } + else if (this.activeRegistrationDelegate != null) + { + this.activeRegistrationDelegate.addHandler(handler); + } + } + else + { + throw new InvalidProviderException("Provider " + provider.getClass() + " does not expose a registration method for " + interfaceType); + } + } + + /** + * @param interfaceType + */ + public List getProvidersFor(Class interfaceType) + { + List handlers = new ArrayList(); + + for (InterfaceHandler handler : this.interfaceHandlers) + { + if (handler.interfaceType == interfaceType) + { + handlers.add(handler.provider); + } + } + + if (this.activeRegistrationDelegate != null) + { + for (InterfaceHandler handler : this.activeRegistrationDelegate.getHandlers()) + { + if (handler.interfaceType == interfaceType) + { + handlers.add(handler.provider); + } + } + } + + return handlers; + } + + /** + * @param interfaceType + * @param priority + */ + private void removeHandlersFor(Class interfaceType, int priority) + { + Iterator iter = this.interfaceHandlers.iterator(); + while (iter.hasNext()) + { + InterfaceHandler handler = iter.next(); + if (handler.interfaceType.equals(interfaceType)) + { + if (handler.exclusive) + { + throw new RuntimeException("Attempt to register an exclusive handler when an exclusive handler already exists for " + + interfaceType); + } + + iter.remove(); + } + } + } + + /** + * Returns the API which supplied a particular provider, if the provider was + * supplied by an API, otherwise returns null. + * + * @param provider + */ + public LiteAPI getAPIForProvider(InterfaceProvider provider) + { + return this.providerToAPIMap.get(provider); + } + + /** + * Initialises all registered providers + */ + private void initProviders() + { + if (this.initDone) return; + this.initDone = true; + + for (InterfaceProvider provider : this.allProviders) + { + provider.initProvider(); + } + } + + /** + * Offers an interface listener to the manager, the listener will actually + * be registered with the interface handlers at the end of the startup + * process. + * + * @param listener + */ + public void offer(Listener listener) + { + if (listener instanceof InterfaceProvider) + { + this.registerProvider((InterfaceProvider)listener); + } + + this.listeners.add(listener); + + if (this.registrationDone) + { + this.registerListener(listener); + } + } + + /** + * Registers all enqueued consumers as listeners + */ + private void registerQueuedListeners() + { + for (Listener consumer : this.listeners) + { + this.registerListener(consumer); + } + + this.registrationDone = true; + } + + /** + * Registers a listener with all registered handlers + * + * @param listener + */ + public void registerListener(Listener listener) + { + for (InterfaceHandler handler : this.interfaceHandlers) + { + handler.registerListener(listener); + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderMods.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderMods.java new file mode 100644 index 00000000..7b13779c --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderMods.java @@ -0,0 +1,795 @@ +package com.mumfrey.liteloader.core; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.api.ModLoadObserver; +import com.mumfrey.liteloader.common.LoadingProgress; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.interfaces.FastIterableDeque; +import com.mumfrey.liteloader.interfaces.Loadable; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.interfaces.LoaderEnumerator; +import com.mumfrey.liteloader.interfaces.TweakContainer; +import com.mumfrey.liteloader.launch.ClassTransformerManager; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.modconfig.ConfigManager; +import com.mumfrey.liteloader.modconfig.ConfigStrategy; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +/** + * Separated from the core loader class for encapsulation purposes + * + * @author Adam Mummery-Smith + */ +public class LiteLoaderMods +{ + public static final String MOD_SYSTEM = "liteloader"; + + /** + * Reference to the loader + */ + protected final LiteLoader loader; + + /** + * Loader environment instance + */ + protected final LoaderEnvironment environment; + + /** + * Loader Properties adapter + */ + private final LoaderProperties properties; + + /** + * Mod enumerator instance + */ + protected final LoaderEnumerator enumerator; + + /** + * Configuration manager + */ + private final ConfigManager configManager; + + /** + * Mod load observers + */ + private FastIterableDeque observers = new HandlerList(ModLoadObserver.class); + + /** + * List of loaded mods, for crash reporting + */ + private String loadedModsList = "none"; + + /** + * Global list of mods which we can load + */ + protected final List allMods = new LinkedList(); + + /** + * Global list of mods which are still waiting for initialisiation + */ + protected final Deque initMods = new LinkedList(); + + /** + * Global list of mods which we have loaded + */ + protected final List loadedMods = new LinkedList(); + + /** + * Global list of mods which we found but ignored (eg. outdated, invalid) + */ + protected final List badMods = new LinkedList(); + + /** + * Mods which are loaded but disabled + */ + protected final List> disabledMods = new LinkedList>(); + + /** + * Bad containers + */ + protected final List> badContainers = new LinkedList>(); + + private int startupErrorCount, criticalErrorCount; + + LiteLoaderMods(LiteLoader loader, LoaderEnvironment environment, LoaderProperties properties, ConfigManager configManager) + { + this.loader = loader; + this.environment = environment; + this.enumerator = environment.getEnumerator(); + this.properties = properties; + this.configManager = configManager; + } + + void init(List observers) + { + this.observers.addAll(observers); + this.disabledMods.addAll(this.enumerator.getDisabledContainers()); + this.badContainers.addAll(this.enumerator.getBadContainers()); + } + + void onPostInit() + { + this.updateSharedModList(); + + this.environment.getEnabledModsList().save(); + } + + public EnabledModsList getEnabledModsList() + { + return this.environment.getEnabledModsList(); + } + + public List getAllMods() + { + return Collections.unmodifiableList(this.allMods); + } + + /** + * Used for crash reporting, returns a text list of all loaded mods + * + * @return List of loaded mods as a string + */ + public String getLoadedModsList() + { + return this.loadedModsList; + } + + /** + * Get a list containing all loaded mods + */ + public List>> getLoadedMods() + { + return this.loadedMods; + } + + /** + * Get a list containing all mod files which were NOT loaded + */ + public List> getDisabledMods() + { + return this.disabledMods; + } + + /** + * Get a list of all bad containers + */ + public List> getBadContainers() + { + return this.badContainers; + } + + /** + * Get the list of injected tweak containers + */ + public Collection>> getInjectedTweaks() + { + return this.enumerator.getInjectedTweaks(); + } + + public int getStartupErrorCount() + { + return this.startupErrorCount; + } + + public int getCriticalErrorCount() + { + return this.criticalErrorCount; + } + + public ModInfo getModInfo(LiteMod instance) + { + for (Mod mod : this.allMods) + { + if (instance == mod.getMod()) + { + return mod; + } + } + + return null; + } + + /** + * Get whether the specified mod is installed + * + * @param modName + */ + public boolean isModInstalled(String modName) + { + try + { + return this.getMod(modName) != null; + } + catch (IllegalArgumentException ex) + { + return false; + } + } + + /** + * Get a reference to a loaded mod, if the mod exists + * + * @param modName Mod's name, identifier or class name + */ + @SuppressWarnings("unchecked") + public T getMod(String modName) + { + if (modName == null) + { + throw new IllegalArgumentException("Attempted to get a reference to a mod without specifying a mod name"); + } + + for (Mod mod : this.allMods) + { + if (mod.matchesName(modName)) + { + return (T)mod.getMod(); + } + } + + return null; + } + + /** + * Get a reference to a loaded mod, if the mod exists + * + * @param modClass Mod class + */ + @SuppressWarnings("unchecked") + public T getMod(Class modClass) + { + for (Mod mod : this.allMods) + { + if (mod.getModClass().equals(modClass)) + { + return (T)mod.getMod(); + } + } + + return null; + } + + /** + * Get the mod which matches the specified identifier + * + * @param identifier + */ + public Class getModFromIdentifier(String identifier) + { + if (identifier == null) return null; + + for (Mod mod : this.allMods) + { + if (mod.matchesIdentifier(identifier)) + { + return mod.getModClass(); + } + } + + return null; + } + + /** + * Get a metadata value for the specified mod + * + * @param modNameOrId + * @param metaDataKey + * @param defaultValue + */ + public String getModMetaData(String modNameOrId, String metaDataKey, String defaultValue) throws IllegalArgumentException + { + return this.getModMetaData(this.getMod(modNameOrId), metaDataKey, defaultValue); + } + + /** + * Get a metadata value for the specified mod + * + * @param mod + * @param metaDataKey + * @param defaultValue + */ + public String getModMetaData(LiteMod mod, String metaDataKey, String defaultValue) + { + if (mod == null || metaDataKey == null) return defaultValue; + return this.enumerator.getModMetaData(mod.getClass(), metaDataKey, defaultValue); + } + + /** + * Get a metadata value for the specified mod + * + * @param modClass + * @param metaDataKey + * @param defaultValue + */ + public String getModMetaData(Class modClass, String metaDataKey, String defaultValue) + { + if (modClass == null || metaDataKey == null) return defaultValue; + return this.enumerator.getModMetaData(modClass, metaDataKey, defaultValue); + } + + /** + * Get the mod identifier, this is used for versioning, exclusivity, and + * enablement checks. + * + * @param modClass + */ + public String getModIdentifier(Class modClass) + { + return this.enumerator.getIdentifier(modClass); + } + + /** + * Get the mod identifier, this is used for versioning, exclusivity, and + * enablement checks. + * + * @param mod + */ + public String getModIdentifier(LiteMod mod) + { + return mod == null ? null : this.enumerator.getIdentifier(mod.getClass()); + } + + /** + * Get the container (mod file, classpath jar or folder) for the specified + * mod. + * + * @param modClass + */ + public LoadableMod getModContainer(Class modClass) + { + return this.enumerator.getContainer(modClass); + } + + /** + * Get the container (mod file, classpath jar or folder) for the specified + * mod. + * + * @param mod + */ + public LoadableMod getModContainer(LiteMod mod) + { + return mod == null ? null : this.enumerator.getContainer(mod.getClass()); + } + + /** + * @param identifier Identifier of the mod to enable + */ + public void enableMod(String identifier) + { + this.setModEnabled(identifier, true); + } + + /** + * @param identifier Identifier of the mod to disable + */ + public void disableMod(String identifier) + { + this.setModEnabled(identifier, false); + } + + /** + * @param identifier Identifier of the mod to enable/disable + * @param enabled + */ + public void setModEnabled(String identifier, boolean enabled) + { + this.environment.getEnabledModsList().setEnabled(this.environment.getProfile(), identifier, enabled); + this.environment.getEnabledModsList().save(); + } + + /** + * @param identifier + */ + public boolean isModEnabled(String identifier) + { + return this.environment.getEnabledModsList().isEnabled(LiteLoader.getProfile(), identifier); + } + + public boolean isModEnabled(String profile, String identifier) + { + return this.environment.getEnabledModsList().isEnabled(profile, identifier); + } + + /** + * @param identifier + */ + public boolean isModActive(String identifier) + { + if (identifier == null) return false; + + for (Mod mod : this.loadedMods) + { + if (mod.matchesIdentifier(identifier)) + { + return true; + } + } + + return false; + } + + /** + * Create mod instances from the enumerated classes + */ + void loadMods() + { + LoadingProgress.incTotalLiteLoaderProgress(this.enumerator.getModsToLoad().size()); + + for (ModInfo> mod : this.enumerator.getModsToLoad()) + { + LoadingProgress.incLiteLoaderProgress("Loading mod from %s...", mod.getModClassSimpleName()); + LoadableMod container = mod.getContainer(); + + try + { + String identifier = mod.getIdentifier(); + if (identifier == null || this.environment.getEnabledModsList().isEnabled(this.environment.getProfile(), identifier)) + { + if (!this.enumerator.checkDependencies(container)) + { + this.onModLoadFailed(container, identifier, "the mod was missing a required dependency", null); + continue; + } + + if (mod instanceof Mod) + { + this.loadMod((Mod)mod); + } + else + { + this.loadMod(identifier, mod.getModClass(), container); + } + } + else + { + this.onModLoadFailed(container, identifier, "excluded by filter", null); + } + } + catch (Throwable th) + { + this.onModLoadFailed(container, mod.getModClassName(), "an error occurred", th); + this.registerModStartupError(mod, th); + } + + this.observers.all().onPostModLoaded(mod); + } + } + + /** + * @param identifier + * @param modClass + * @param container + * @throws InstantiationException + * @throws IllegalAccessException + */ + void loadMod(String identifier, Class modClass, LoadableMod container) throws InstantiationException, IllegalAccessException + { + Mod mod = new Mod(container, modClass, identifier); + this.loadMod(mod); + } + + /** + * @param mod + * @throws InstantiationException + * @throws IllegalAccessException + */ + void loadMod(Mod mod) throws InstantiationException, IllegalAccessException + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Loading mod from %s", mod.getModClassName()); + + LiteMod newMod = mod.newInstance(); + + this.onModLoaded(mod); + + String modName = mod.getDisplayName(); + LiteLoaderLogger.info("Successfully added mod %s version %s", modName, newMod.getVersion()); + } + + /** + * @param mod + */ + void onModLoaded(Mod mod) + { + this.observers.all().onModLoaded(mod.getMod()); + + this.allMods.add(mod); + this.initMods.add(mod); + + LoadingProgress.incTotalLiteLoaderProgress(1); + } + + /** + * @param container + * @param identifier + * @param reason + * @param th + */ + void onModLoadFailed(LoadableMod container, String identifier, String reason, Throwable th) + { + LiteLoaderLogger.warning("Not loading mod %s, %s", identifier, reason); + + for (ModInfo mod : this.disabledMods) + { + if (mod.getContainer().equals(container)) + { + return; + } + } + + if (container != LoadableMod.NONE) + { + this.disabledMods.add(new NonMod(container, false)); + } + + this.observers.all().onModLoadFailed(container, identifier, reason, th); + } + + /** + * Initialise the mods which were loaded + */ + void initMods() + { + this.loadedModsList = ""; + int loadedModsCount = 0; + + while (this.initMods.size() > 0) + { + Mod mod = this.initMods.removeFirst(); + + try + { + this.initMod(mod); + loadedModsCount++; + } + catch (Throwable th) + { + this.registerModStartupError(mod, th); + LiteLoaderLogger.warning(th, "Error initialising mod '%s'", mod.getDisplayName()); + } + } + + this.loadedModsList = String.format("%s loaded mod(s)%s", loadedModsCount, this.loadedModsList); + } + + /** + * @param mod + */ + private void initMod(Mod mod) + { + LiteMod instance = mod.getMod(); + + LiteLoaderLogger.info(Verbosity.REDUCED, "Initialising mod %s version %s", instance.getName(), instance.getVersion()); + LoadingProgress.incLiteLoaderProgress("Initialising mod %s version %s...", instance.getName(), instance.getVersion()); + + this.onPreInitMod(instance); + + // initialise the mod + instance.init(LiteLoader.getCommonConfigFolder()); + + this.onPostInitMod(instance); + + this.loadedMods.add(mod); + this.loadedModsList += String.format("\n - %s version %s", mod.getDisplayName(), mod.getVersion()); + } + + /** + * @param instance + */ + private void onPreInitMod(LiteMod instance) + { + this.observers.all().onPreInitMod(instance); + + // register mod config panel if configurable + this.configManager.registerMod(instance); + + try + { + this.handleModVersionUpgrade(instance); + } + catch (Throwable th) + { + LiteLoaderLogger.warning("Error performing settings upgrade for %s. Settings may not be properly migrated", instance.getName()); + } + + // Init mod config if there is any + this.configManager.initConfig(instance); + } + + /** + * @param instance + */ + private void onPostInitMod(LiteMod instance) + { + this.observers.all().onPostInitMod(instance); + + // add the mod to all relevant listener queues + LiteLoader.getInterfaceManager().offer(instance); + + this.loader.onPostInitMod(instance); + } + + /** + * @param instance + */ + private void handleModVersionUpgrade(LiteMod instance) + { + String modKey = this.getModNameForConfig(instance.getClass(), instance.getName()); + + int currentRevision = LiteLoaderVersion.CURRENT.getLoaderRevision(); + int lastKnownRevision = this.properties.getLastKnownModRevision(modKey); + + LiteLoaderVersion lastModVersion = LiteLoaderVersion.getVersionFromRevision(lastKnownRevision); + if (currentRevision > lastModVersion.getLoaderRevision()) + { + File newConfigPath = LiteLoader.getConfigFolder(); + File oldConfigPath = this.environment.inflectVersionedConfigPath(lastModVersion); + + LiteLoaderLogger.info("Performing config upgrade for mod %s. Upgrading %s to %s...", + instance.getName(), lastModVersion, LiteLoaderVersion.CURRENT); + + this.observers.all().onMigrateModConfig(instance, newConfigPath, oldConfigPath); + + // Migrate versioned config if any is present + this.configManager.migrateModConfig(instance, newConfigPath, oldConfigPath); + + // Let the mod upgrade + instance.upgradeSettings(LiteLoaderVersion.CURRENT.getMinecraftVersion(), newConfigPath, oldConfigPath); + + this.properties.storeLastKnownModRevision(modKey); + LiteLoaderLogger.info("Config upgrade succeeded for mod %s", instance.getName()); + } + else if (currentRevision < lastKnownRevision && ConfigManager.getConfigStrategy(instance) == ConfigStrategy.Unversioned) + { + LiteLoaderLogger.warning("Mod %s has config from unknown loader revision %d. This may cause unexpected behaviour.", + instance.getName(), lastKnownRevision); + } + } + + /** + * Used by the version upgrade code, gets a version of the mod name suitable + * for inclusion in the properties file + * + * @param modName + */ + String getModNameForConfig(Class modClass, String modName) + { + if (modName == null || modName.isEmpty()) + { + modName = modClass.getSimpleName().toLowerCase(); + } + + return String.format("version.%s", modName.toLowerCase().replaceAll("[^a-z0-9_\\-\\.]", "")); + } + + void onStartupComplete() + { + this.validateModTransformers(); + } + + /** + * Check that all specified mod transformers were injected successfully, tag + * mods with failed transformers as critically errored. + */ + private void validateModTransformers() + { + ClassTransformerManager transformerManager = this.environment.getTransformerManager(); + Set injectedTransformers = transformerManager.getInjectedTransformers(); + + for (Mod mod : this.loadedMods) + { + if (mod.hasClassTransformers()) + { + List modTransformers = ((TweakContainer)mod.getContainer()).getClassTransformerClassNames(); + for (String modTransformer : modTransformers) + { + if (!injectedTransformers.contains(modTransformer)) + { + List throwables = transformerManager.getTransformerStartupErrors(modTransformer); + if (throwables != null) + { + for (Throwable th : throwables) + { + this.registerModStartupError(mod, th, true); + } + } + else + { + this.registerModStartupError(mod, new RuntimeException("Missing class transformer " + modTransformer), true); + } + } + } + } + } + } + + /** + * @param instance + * @param th + */ + public void onLateInitFailed(LiteMod instance, Throwable th) + { + ModInfo mod = this.getModInfo(instance); + if (mod != null) + { + this.registerModStartupError(mod, th); + } + } + + private void registerModStartupError(ModInfo mod, Throwable th) + { + // This is a critical error if a mod has already injected a transformer, since it may have injected + // callbacks which it is not in a position to handle! + boolean critical = this.hasModInjectedTransformers(mod); + + this.registerModStartupError(mod, th, critical); + } + + private boolean hasModInjectedTransformers(ModInfo mod) + { + if (!mod.hasClassTransformers()) return false; + + Set injectedTransformers = this.environment.getTransformerManager().getInjectedTransformers(); + List modTransformers = ((TweakContainer)mod.getContainer()).getClassTransformerClassNames(); + + for (String modTransformer : modTransformers) + { + if (injectedTransformers.contains(modTransformer)) + { + return true; + } + } + + return false; + } + + private void registerModStartupError(ModInfo mod, Throwable th, boolean critical) + { + this.startupErrorCount++; + if (critical) this.criticalErrorCount++; + mod.registerStartupError(th); + + if (!this.loadedMods.contains(mod) && !this.disabledMods.contains(mod)) + { + this.disabledMods.add(mod); + } + } + + void updateSharedModList() + { + Map> modList = this.enumerator.getSharedModList(); + if (modList == null) return; + + for (Mod mod : this.allMods) + { + String modKey = String.format("%s:%s", LiteLoaderMods.MOD_SYSTEM, mod.getIdentifier()); + modList.put(modKey, this.packModInfoToMap(mod)); + } + } + + private Map packModInfoToMap(Mod mod) + { + Map modInfo = new HashMap(); + + modInfo.put("modsystem", LiteLoaderMods.MOD_SYSTEM); + modInfo.put("id", mod.getIdentifier()); + modInfo.put("version", mod.getVersion()); + modInfo.put("name", mod.getDisplayName()); + modInfo.put("url", mod.getURL()); + modInfo.put("authors", mod.getAuthor()); + modInfo.put("description", mod.getDescription()); + + return modInfo; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderUpdateSite.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderUpdateSite.java new file mode 100644 index 00000000..8438867e --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderUpdateSite.java @@ -0,0 +1,171 @@ +package com.mumfrey.liteloader.core; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import com.google.common.io.ByteSink; +import com.google.common.io.Files; +import com.mumfrey.liteloader.launch.ClassPathUtilities; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.update.UpdateSite; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +public class LiteLoaderUpdateSite extends UpdateSite +{ + private static final String UPDATE_SITE_URL = "http://dl.liteloader.com/versions/"; + private static final String UPDATE_SITE_VERSIONS_JSON = "versions.json"; + private static final String UPDATE_SITE_ARTEFACT_NAME = "com.mumfrey:liteloader"; + + private String mcVersion; + + private File mcDir; + private File jarFile = null; + + private boolean updateForced = false; + + public LiteLoaderUpdateSite(String targetVersion, long currentTimeStamp) + { + super(LiteLoaderUpdateSite.UPDATE_SITE_URL, LiteLoaderUpdateSite.UPDATE_SITE_VERSIONS_JSON, targetVersion, + LiteLoaderUpdateSite.UPDATE_SITE_ARTEFACT_NAME, currentTimeStamp); + + this.mcVersion = targetVersion; + } + + public boolean canForceUpdate(LoaderProperties properties) + { + if (!properties.getAndStoreBooleanProperty(LoaderProperties.OPTION_FORCE_UPDATE, false)) + { + return false; + } + + if (this.hasJarFile()) return true; + return this.findJarFile(); + } + + /** + * + */ + private boolean findJarFile() + { + // Find the jar containing liteloader + File jarFile = ClassPathUtilities.getPathToResource(LiteLoader.class, "/" + LiteLoader.class.getName().replace('.', '/') + ".class"); + if (!jarFile.isFile()) return false; + + // Validate that the jar is in the expected name and location + this.mcDir = this.walkAndValidateParents(jarFile, "liteloader-" + this.mcVersion + ".jar", this.mcVersion, + "liteloader", "mumfrey", "com", "libraries"); + if (this.mcDir == null) return false; + + // Check that the jar we found is actually on the current classpath + if (!ClassPathUtilities.isJarOnClassPath(jarFile)) return false; + this.jarFile = jarFile; + return true; + } + + private File walkAndValidateParents(File file, String... breadcrumbs) + { + try + { + for (String breadcrumb : breadcrumbs) + { + if (file == null || !file.exists() || !file.getName().equals(breadcrumb)) return null; + file = file.getParentFile(); + } + + return file; + } + catch (Exception ex) + { + ex.printStackTrace(); + } + + return null; + } + + + public boolean canCheckForUpdate() + { + return !this.updateForced; + } + + public boolean hasJarFile() + { + return this.jarFile != null; + } + + public File getJarFile() + { + return this.jarFile; + } + + public boolean forceUpdate() + { + if (this.jarFile != null) + { + LiteLoaderLogger.info("Attempting to force update, extracting jar assassin..."); + + File jarAssassinOutput = new File(this.jarFile.getParentFile(), "liteloader-update-agent.jar"); + + if (!LiteLoaderUpdateSite.extractFile("/update/liteloader-update-agent.jar", jarAssassinOutput) || !jarAssassinOutput.isFile()) + { + LiteLoaderLogger.info("Couldn't extract jarassassin jar, can't force update"); + return false; + } + + File joptSimple = new File(this.mcDir, "libraries/net/sf/jopt-simple/jopt-simple/4.5/jopt-simple-4.5.jar"); + + ProcessBuilder jarAssassinProcBuilder = new ProcessBuilder( + LiteLoaderUpdateSite.getJavaExecutable().getAbsolutePath(), + "-cp", joptSimple.getAbsolutePath(), + "-jar", jarAssassinOutput.getAbsolutePath(), + "--jarFile", this.jarFile.getAbsolutePath()).directory(this.jarFile.getParentFile()); + try + { + System.err.println(jarAssassinProcBuilder.command()); + + @SuppressWarnings("unused") + Process jarAssassin = jarAssassinProcBuilder.start(); + + ClassPathUtilities.deleteClassPathJar(this.jarFile.getAbsolutePath()); + + return true; + } + catch (Throwable th) + { + LiteLoaderLogger.info("Couldn't execute jarassassin jar, can't force update"); + return false; + } + } + + return false; + } + + protected static boolean extractFile(String resourceName, File outputFile) + { + try + { + final InputStream inputStream = LiteLoaderUpdateSite.class.getResourceAsStream(resourceName); + final ByteSink outputSupplier = Files.asByteSink(outputFile); + outputSupplier.writeFrom(inputStream); + } + catch (NullPointerException ex) + { + return false; + } + catch (IOException ex) + { + return false; + } + + return true; + } + + protected static File getJavaExecutable() + { + File javaBin = new File(new File(System.getProperty("java.home")), "bin"); + File javaWin = new File(javaBin, "javaw.exe"); + String osName = System.getProperty("os.name").toLowerCase(); + return osName.contains("win") && javaWin.isFile() ? javaWin : new File(javaBin, "java"); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderVersion.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderVersion.java new file mode 100644 index 00000000..ac79d1b1 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/LiteLoaderVersion.java @@ -0,0 +1,138 @@ +package com.mumfrey.liteloader.core; + +import java.util.HashSet; +import java.util.Set; + +/** + * LiteLoader version table + * + * @author Adam Mummery-Smith + * @version 1.8.0_00 + */ +public enum LiteLoaderVersion +{ + LEGACY(0, 0, "-", "Unknown", "-"), + FUTURE(Integer.MAX_VALUE, Long.MAX_VALUE, "-", "Future", "-"), + + MC_1_5_2_R1(9, 0, "1.5.2", "1.5.2", "1.5.2" ), + MC_1_6_1_R0(11, 0, "1.6.1", "1.6.1", "1.6.1", "1.6.r1"), + MC_1_6_1_R1(11, 0, "1.6.1", "1.6.1", "1.6.1", "1.6.r1"), + MC_1_6_2_R0(12, 0, "1.6.2", "1.6.2", "1.6.2", "1.6.r2"), + MC_1_6_2_R1(12, 1374025480, "1.6.2", "1.6.2_01", "1.6.2", "1.6.r2"), + MC_1_6_2_R2(13, 1374709543, "1.6.2", "1.6.2_02", "1.6.2", "1.6.r2"), + MC_1_6_2_R3(14, 1375228794, "1.6.2", "1.6.2_03", "1.6.2", "1.6.r2"), + MC_1_6_2_R4(15, 1375662298, "1.6.2", "1.6.2_04", "1.6.2", "1.6.r2"), + MC_1_6_3_R0(16, 1375662298, "1.6.3", "1.6.3", "1.6.3", "1.6.r3"), + MC_1_6_4_R0(17, 1380279938, "1.6.4", "1.6.4", "1.6.4", "1.6.r4"), + MC_1_6_4_R1(18, 1380796916, "1.6.4", "1.6.4_01", "1.6.4", "1.6.r4"), + MC_1_6_4_R2(19, 1380796916, "1.6.4", "1.6.4_02", "1.6.4", "1.6.r4"), + MC_1_7_2_R0(20, 1386027226, "1.7.2", "1.7.2", "1.7.2", "1.7.r1"), + MC_1_7_2_R1(21, 1388455995, "1.7.2", "1.7.2_01", "1.7.2_01"), + MC_1_7_2_R2(22, 1391815963, "1.7.2", "1.7.2_02", "1.7.2_02"), + MC_1_7_2_R3(23, 1391890695, "1.7.2", "1.7.2_03", "1.7.2_02", "1.7.2_03"), + MC_1_7_2_R4(24, 1392487926, "1.7.2", "1.7.2_04", "1.7.2_02", "1.7.2_03", "1.7.2_04"), + MC_1_7_2_R5(25, 0, "1.7.2", "1.7.2_05", "1.7.2_02", "1.7.2_03", "1.7.2_04", "1.7.2_05"), + MC_1_7_2_R6(26, 0, "1.7.2", "1.7.2_06", "1.7.2_06"), + MC_1_7_10_R0(27, 1404330030, "1.7.10", "1.7.10", "1.7.10"), + MC_1_7_10_R1(28, 1404673785, "1.7.10", "1.7.10_01", "1.7.10"), + MC_1_7_10_R2(29, 1405369406, "1.7.10", "1.7.10_02", "1.7.10"), + MC_1_7_10_R3(30, 1407687918, "1.7.10", "1.7.10_03", "1.7.10", "1.7.10_03"), + MC_1_7_10_R4(31, 1414368553, "1.7.10", "1.7.10_04", "1.7.10", "1.7.10_03", "1.7.10_04"), + MC_1_8_0_R0(32, 0, "1.8", "1.8.0", "1.8", "1.8.0"); + + /** + * Current loader version + */ + public static final LiteLoaderVersion CURRENT = LiteLoaderVersion.MC_1_8_0_R0; + + private static final LiteLoaderUpdateSite updateSite = new LiteLoaderUpdateSite(LiteLoaderVersion.CURRENT.getMinecraftVersion(), + LiteLoaderVersion.CURRENT.getReleaseTimestamp()); + + private final int revision; + + private final long timestamp; + + private final String minecraftVersion; + + private final String loaderVersion; + + private final Set supportedVersions = new HashSet(); + + private LiteLoaderVersion(int revision, long timestamp, String minecraftVersion, String loaderVersion, String... supportedVersions) + { + this.revision = revision; + this.timestamp = timestamp; + this.minecraftVersion = minecraftVersion; + this.loaderVersion = loaderVersion; + + for (String supportedVersion : supportedVersions) + this.supportedVersions.add(supportedVersion); + } + + public int getLoaderRevision() + { + return this.revision; + } + + public long getReleaseTimestamp() + { + return this.timestamp; + } + + public String getMinecraftVersion() + { + return this.minecraftVersion; + } + + public String getLoaderVersion() + { + return this.loaderVersion; + } + + public static LiteLoaderVersion getVersionFromRevision(int revision) + { + if (revision > LiteLoaderVersion.CURRENT.revision) + { + return LiteLoaderVersion.FUTURE; + } + + for (LiteLoaderVersion version : LiteLoaderVersion.values()) + { + if (version.getLoaderRevision() == revision) + { + return version; + } + } + + return LiteLoaderVersion.LEGACY; + } + + public static int getRevisionFromVersion(String versionString) + { + for (LiteLoaderVersion version : LiteLoaderVersion.values()) + { + if (version.getLoaderVersion().equals(versionString)) + { + return version.getLoaderRevision(); + } + } + + return LiteLoaderVersion.LEGACY.getLoaderRevision(); + } + + public boolean isVersionSupported(String version) + { + return this.supportedVersions.contains(version); + } + + @Override + public String toString() + { + return this.loaderVersion; + } + + public static LiteLoaderUpdateSite getUpdateSite() + { + return LiteLoaderVersion.updateSite; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/Mod.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/Mod.java new file mode 100644 index 00000000..f9a5f626 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/Mod.java @@ -0,0 +1,205 @@ +package com.mumfrey.liteloader.core; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.interfaces.LoadableMod; + +/** + * ModInfo for an active mod instance + * + * @author Adam Mummery-Smith + */ +class Mod extends ModInfo> +{ + /** + * Mod class + */ + private final Class modClass; + + /** + * Mod's key identifier, usually the class simplename + */ + private final String key; + + /** + * Mod's identifier (from metadata) + */ + private final String identifier; + + /** + * Mod instance + */ + private LiteMod instance; + + /** + * Mod display name, initially read from metadata then replaced with real + * name once instanced. + */ + private String name; + + /** + * Mod display name, initially read from version then replaced with real + * version once instanced. + */ + private String version; + + /** + * @param container + * @param modClass + */ + public Mod(LoadableMod container, Class modClass) + { + this(container, modClass, container != null ? container.getIdentifier() : LiteLoaderEnumerator.getModClassName(modClass)); + } + + /** + * @param container + * @param modClass + * @param identifier + */ + public Mod(LoadableMod container, Class modClass, String identifier) + { + super(container != null ? container : LoadableMod.NONE, true); + + this.modClass = modClass; + this.key = modClass.getSimpleName(); + this.identifier = identifier.toLowerCase(); + this.name = this.container.getDisplayName(); + this.version = this.container.getVersion(); + } + + /** + * Called by the mod manager to instance the mod + * + * @throws InstantiationException + * @throws IllegalAccessException + */ + LiteMod newInstance() throws InstantiationException, IllegalAccessException + { + if (this.instance != null) + { + throw new InstantiationException("Attempted to create an instance of " + this.key + " but the instance was already created"); + } + + this.instance = this.modClass.newInstance(); + + String name = this.instance.getName(); + if (name != null) this.name = name; + + String version = this.instance.getVersion(); + if (version != null) this.version = version; + + return this.instance; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#isToggleable() + */ + @Override + public boolean isToggleable() + { + return true; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getMod() + */ + @Override + public LiteMod getMod() + { + return this.instance; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getModClass() + */ + @Override + public Class getModClass() + { + return this.modClass; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getDisplayName() + */ + @Override + public String getDisplayName() + { + return this.name; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getVersion() + */ + @Override + public String getVersion() + { + return this.version; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getModClassName() + */ + @Override + public String getModClassName() + { + return this.modClass.getName(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getModClassSimpleName() + */ + @Override + public String getModClassSimpleName() + { + return this.key; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getIdentifier() + */ + @Override + public String getIdentifier() + { + return this.identifier; + } + + /** + * Get whether any of the valid identifiers match the supplied name + * + * @param name + */ + public boolean matchesName(String name) + { + return (name.equalsIgnoreCase(this.instance.getName()) || name.equalsIgnoreCase(this.identifier) || name.equalsIgnoreCase(this.key)); + } + + /** + * Get whether ths mod identifier matches the supplied identifier + * + * @param identifier + */ + public boolean matchesIdentifier(String identifier) + { + return identifier.equalsIgnoreCase(this.identifier); + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object other) + { + if (other == null) return false; + if (!(other instanceof Mod)) return false; + return ((Mod)other).key.equals(this.key); + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() + { + return this.key.hashCode(); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/ModInfo.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/ModInfo.java new file mode 100644 index 00000000..5f3c6eaf --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/ModInfo.java @@ -0,0 +1,230 @@ +package com.mumfrey.liteloader.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.interfaces.Loadable; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.interfaces.MixinContainer; +import com.mumfrey.liteloader.interfaces.TweakContainer; + +/** + * ModInfo is used to keep runtime information about a mod (or other injectable) + * together with relevant environmental information (such as startup errors) and + * its container. + * + * @author Adam Mummery-Smith + * + * @param type of container + */ +public abstract class ModInfo> +{ + /** + * List of built-in APIs, used to filter for 3rd-party APIs + */ + protected static final Set BUILT_IN_APIS = ImmutableSet.of("liteloader"); + + /** + * Container instance + */ + protected final TContainer container; + + /** + * True if this mod is active/injected or not active/errored + */ + protected final boolean active; + + /** + * Startup errors encountered whilst loading this mod + */ + private final List startupErrors = new ArrayList(); + + /** + * @param container + * @param active + */ + protected ModInfo(TContainer container, boolean active) + { + this.container = container; + this.active = active; + } + + /** + * Get whether this mod is currently active + */ + public final boolean isActive() + { + return this.active; + } + + /** + * Get whether this mod is valid + */ + public boolean isValid() + { + return true; + } + + /** + * Get whether this mod can be toggled + */ + public boolean isToggleable() + { + return this.container.isToggleable(); + } + + /** + * Get whether this mod has a container + */ + public final boolean hasContainer() + { + return this.container != LoadableMod.NONE; + } + + /** + * Get the container for this mod + */ + public final TContainer getContainer() + { + return this.container; + } + + /** + * Callback to allow the mod manager to register a startup error + */ + void registerStartupError(Throwable th) + { + this.startupErrors.add(th); + } + + /** + * Get startup errors for this instance + */ + public List getStartupErrors() + { + return Collections.unmodifiableList(this.startupErrors); + } + + /** + * Get the display name for this mod + */ + public String getDisplayName() + { + return this.container.getDisplayName(); + } + + /** + * Get the mod version + */ + public String getVersion() + { + return this.container.getVersion(); + } + + /** + * Get the nod identifier + */ + public String getIdentifier() + { + return this.container.getIdentifier(); + } + + /** + * Get the mod URL + */ + public String getURL() + { + return this.container instanceof LoadableMod ? ((LoadableMod)this.container).getMetaValue("url", "") : null; + } + + /** + * Get the mod author(s) + */ + public String getAuthor() + { + return this.container.getAuthor(); + } + + /** + * Get the mod description + */ + public String getDescription() + { + return this.container.getDescription(null); + } + + /** + * If this container has a tweak + */ + public boolean hasTweakClass() + { + return (this.container instanceof TweakContainer && ((TweakContainer)this.container).hasTweakClass()); + } + + /** + * If this has transformers (NOT robots in disguise, the other kind) + */ + public boolean hasClassTransformers() + { + return (this.container instanceof TweakContainer && ((TweakContainer)this.container).hasClassTransformers()); + } + + /** + * If this has JSON event transformers + */ + public boolean hasEventTransformers() + { + return (this.container instanceof TweakContainer && ((TweakContainer)this.container).hasEventTransformers()); + } + + /** + * If this has mixins + */ + public boolean hasMixins() + { + return (this.container instanceof MixinContainer && ((MixinContainer)this.container).hasMixins()); + } + + /** + * Get whether this mod uses external (3rd-party) API + */ + public boolean usesAPI() + { + if (this.container instanceof LoadableMod) + { + for (String requiredAPI : ((LoadableMod)this.container).getRequiredAPIs()) + { + if (!ModInfo.BUILT_IN_APIS.contains(requiredAPI)) + { + return true; + } + } + } + + return false; + } + + /** + * Get the mod instance + */ + public abstract LiteMod getMod(); + + /** + * Get the mod class + */ + public abstract Class getModClass(); + + /** + * Get the mod class full name + */ + public abstract String getModClassName(); + + /** + * Get the mod class simple name + */ + public abstract String getModClassSimpleName(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/NonMod.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/NonMod.java new file mode 100644 index 00000000..45857623 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/NonMod.java @@ -0,0 +1,57 @@ +package com.mumfrey.liteloader.core; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.interfaces.Loadable; + +/** + * ModInfo for unloaded containers and injected tweaks + * + * @author Adam Mummery-Smith + */ +public class NonMod extends ModInfo> +{ + /** + * @param container + * @param active + */ + public NonMod(Loadable container, boolean active) + { + super(container, active); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getMod() + */ + @Override + public LiteMod getMod() + { + return null; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getModClass() + */ + @Override + public Class getModClass() + { + return null; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getModClassName() + */ + @Override + public String getModClassName() + { + return null; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ModInfo#getModClassSimpleName() + */ + @Override + public String getModClassSimpleName() + { + return null; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/PacketEvents.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/PacketEvents.java new file mode 100644 index 00000000..108c3439 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/PacketEvents.java @@ -0,0 +1,317 @@ +package com.mumfrey.liteloader.core; + +import java.util.List; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.INetHandler; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.Packet; +import net.minecraft.network.login.server.S02PacketLoginSuccess; +import net.minecraft.network.play.client.C01PacketChatMessage; +import net.minecraft.network.play.client.C15PacketClientSettings; +import net.minecraft.network.play.client.C17PacketCustomPayload; +import net.minecraft.network.play.server.S01PacketJoinGame; +import net.minecraft.network.play.server.S02PacketChat; +import net.minecraft.network.play.server.S3FPacketCustomPayload; +import net.minecraft.util.IThreadListener; + +import com.mumfrey.liteloader.PacketHandler; +import com.mumfrey.liteloader.ServerChatFilter; +import com.mumfrey.liteloader.api.InterfaceProvider; +import com.mumfrey.liteloader.api.Listener; +import com.mumfrey.liteloader.common.transformers.PacketEventInfo; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.core.event.HandlerList.ReturnLogicOp; +import com.mumfrey.liteloader.core.runtime.Packets; +import com.mumfrey.liteloader.interfaces.FastIterable; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Packet event handling + * + * @author Adam Mummery-Smith + */ +public abstract class PacketEvents implements InterfaceProvider +{ + protected static PacketEvents instance; + + class PacketHandlerList extends HandlerList + { + private static final long serialVersionUID = 1L; + + /** + * ctor + */ + PacketHandlerList() + { + super(PacketHandler.class, ReturnLogicOp.AND_BREAK_ON_FALSE); + } + } + + /** + * Reference to the loader instance + */ + protected final LiteLoader loader; + + private PacketHandlerList[] packetHandlers = new PacketHandlerList[Packets.count()]; + + private FastIterable serverChatFilters = new HandlerList(ServerChatFilter.class, + ReturnLogicOp.AND_BREAK_ON_FALSE); + + private final int loginSuccessPacketId = Packets.S02PacketLoginSuccess.getIndex(); + private final int serverChatPacketId = Packets.S02PacketChat.getIndex(); + private final int clientChatPacketId = Packets.C01PacketChatMessage.getIndex(); + private final int joinGamePacketId = Packets.S01PacketJoinGame.getIndex(); + private final int serverPayloadPacketId = Packets.S3FPacketCustomPayload.getIndex(); + private final int clientPayloadPacketId = Packets.C17PacketCustomPayload.getIndex(); + private final int clientSettingsPacketId = Packets.C15PacketClientSettings.getIndex(); + + /** + * ctor + */ + public PacketEvents() + { + PacketEvents.instance = this; + this.loader = LiteLoader.getInstance(); + } + + @Override + public Class getListenerBaseType() + { + return Listener.class; + } + + @Override + public void registerInterfaces(InterfaceRegistrationDelegate delegate) + { + delegate.registerInterface(PacketHandler.class); + delegate.registerInterface(ServerChatFilter.class); + } + + @Override + public void initProvider() + { + } + + /** + * @param serverChatFilter + */ + public void registerServerChatFilter(ServerChatFilter serverChatFilter) + { + this.serverChatFilters.add(serverChatFilter); + } + + /** + * Register a new packet handler + * + * @param handler + */ + public void registerPacketHandler(PacketHandler handler) + { + List> handledPackets = handler.getHandledPackets(); + if (handledPackets != null) + { + for (Class packetClass : handledPackets) + { + String packetClassName = packetClass.getName(); + int packetId = Packets.indexOf(packetClassName); + if (packetId == -1 || packetId >= this.packetHandlers.length) + { + LiteLoaderLogger.warning("PacketHandler %s attempted to register a handler for unupported packet class %s", + handler.getName(), packetClassName); + continue; + } + + if (this.packetHandlers[packetId] == null) + { + this.packetHandlers[packetId] = new PacketHandlerList(); + } + + this.packetHandlers[packetId].add(handler); + } + } + } + + /** + * Event callback + * + * @param e + * @param netHandler + */ + public static void handlePacket(PacketEventInfo e, INetHandler netHandler) + { + PacketEvents.instance.handlePacket(e, netHandler, e.getPacketId()); + } + + private void handlePacket(PacketEventInfo e, INetHandler netHandler, int packetId) + { + Packets packetInfo = Packets.packets[e.getPacketId()]; + IThreadListener threadListener = this.getPacketContextListener(packetInfo.getContext()); + if (threadListener != null && !threadListener.isCallingFromMinecraftThread()) + { + this.handleAsyncPacketEvent(e, netHandler, packetId); + return; + } + + if (this.handlePacketEvent(e, netHandler, packetId) || this.packetHandlers[packetId] == null || e.isCancelled()) + { + return; + } + + if (this.packetHandlers[packetId].all().handlePacket(netHandler, e.getSource())) + { + return; + } + + e.cancel(); + } + + /** + * @param context + */ + protected abstract IThreadListener getPacketContextListener(Packets.Context context); + + /** + * @param e + * @param netHandler + * @param packetId + */ + protected void handleAsyncPacketEvent(PacketEventInfo e, INetHandler netHandler, int packetId) + { + Packet packet = e.getSource(); + + if (packetId == this.loginSuccessPacketId) + { + this.handlePacket(e, netHandler, (S02PacketLoginSuccess)packet); + } + } + + /** + * @param e + * @param netHandler + * @param packetId + * @return true if the packet was handled by a local handler and shouldn't + * be forwarded to later handlers + */ + protected boolean handlePacketEvent(PacketEventInfo e, INetHandler netHandler, int packetId) + { + Packet packet = e.getSource(); + + if (packetId == this.serverChatPacketId) + { + this.handlePacket(e, netHandler, (S02PacketChat)packet); + return true; + } + + if (packetId == this.clientChatPacketId) + { + this.handlePacket(e, netHandler, (C01PacketChatMessage)packet); + return true; + } + + if (packetId == this.joinGamePacketId) + { + this.handlePacket(e, netHandler, (S01PacketJoinGame)packet); + return true; + } + + if (packetId == this.serverPayloadPacketId) + { + this.handlePacket(e, netHandler, (S3FPacketCustomPayload)packet); + return true; + } + + if (packetId == this.clientPayloadPacketId) + { + this.handlePacket(e, netHandler, (C17PacketCustomPayload)packet); + return true; + } + + if (packetId == this.clientSettingsPacketId) + { + this.handlePacket(e, netHandler, (C15PacketClientSettings)packet); + return true; + } + + return false; + } + + /** + * @param e + * @param netHandler + * @param packet + */ + protected abstract void handlePacket(PacketEventInfo e, INetHandler netHandler, S02PacketLoginSuccess packet); + + /** + * S02PacketChat::processPacket() + * + * @param netHandler + * @param packet + */ + protected abstract void handlePacket(PacketEventInfo e, INetHandler netHandler, S02PacketChat packet); + + /** + * S02PacketChat::processPacket() + * + * @param netHandler + * @param packet + */ + protected void handlePacket(PacketEventInfo e, INetHandler netHandler, C01PacketChatMessage packet) + { + EntityPlayerMP player = netHandler instanceof NetHandlerPlayServer ? ((NetHandlerPlayServer)netHandler).playerEntity : null; + + if (!this.serverChatFilters.all().onChat(player, packet, packet.getMessage())) + { + e.cancel(); + } + } + + /** + * S01PacketJoinGame::processPacket() + * + * @param netHandler + * @param packet + */ + protected void handlePacket(PacketEventInfo e, INetHandler netHandler, S01PacketJoinGame packet) + { + this.loader.onJoinGame(netHandler, packet); + } + + /** + * S3FPacketCustomPayload::processPacket() + * + * @param netHandler + * @param packet + */ + protected void handlePacket(PacketEventInfo e, INetHandler netHandler, S3FPacketCustomPayload packet) + { + LiteLoader.getClientPluginChannels().onPluginChannelMessage(packet); + } + + /** + * C17PacketCustomPayload::processPacket() + * + * @param netHandler + * @param packet + */ + protected void handlePacket(PacketEventInfo e, INetHandler netHandler, C17PacketCustomPayload packet) + { + LiteLoader.getServerPluginChannels().onPluginChannelMessage(netHandler, packet); + } + + /** + * C15PacketClientSettings::processPacket() + * + * @param e + * @param netHandler + * @param packet + */ + private void handlePacket(PacketEventInfo e, INetHandler netHandler, C15PacketClientSettings packet) + { + if (netHandler instanceof NetHandlerPlayServer) + { + LiteLoaderEventBroker.broker.onPlayerSettingsReceived(((NetHandlerPlayServer)netHandler).playerEntity, packet); + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/PlayerEventState.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/PlayerEventState.java new file mode 100644 index 00000000..8e6bbe7c --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/PlayerEventState.java @@ -0,0 +1,139 @@ +package com.mumfrey.liteloader.core; + +import java.lang.ref.WeakReference; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.BlockPos; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.MovingObjectPosition.MovingObjectType; + +import com.mumfrey.liteloader.PlayerInteractionListener.MouseButton; +import com.mumfrey.liteloader.core.LiteLoaderEventBroker.InteractType; +import com.mumfrey.liteloader.util.EntityUtilities; + +public class PlayerEventState implements IEventState +{ + private static long MISS = new BlockPos(-1, -1, -1).toLong(); + + private WeakReference playerRef; + + private final LiteLoaderEventBroker broker; + + private double traceDistance = 256.0; + + private int suppressLeftTicks; + private int suppressRightTicks; + private boolean leftClick; + private boolean rightClick; + + private MovingObjectPosition hit; + + private String locale = "en_US"; + + public PlayerEventState(EntityPlayerMP player, LiteLoaderEventBroker broker) + { + this.playerRef = new WeakReference(player); + this.broker = broker; + } + + public void setTraceDistance(int renderDistance) + { + this.traceDistance = renderDistance * 16.0; + } + + public double getTraceDistance() + { + return this.traceDistance; + } + + public void setLocale(String lang) + { + if (lang.matches("^[a-z]{2}_[A-Z]{2}$")) + { + this.locale = lang; + } + } + + public String getLocale() + { + return this.locale; + } + + public EntityPlayerMP getPlayer() + { + return this.playerRef.get(); + } + + public void onSpawned() + { + } + + @Override + public void onTick(MinecraftServer server) + { + if (this.leftClick && this.suppressLeftTicks == 0) + { + this.broker.onPlayerClickedAir(this.getPlayer(), MouseButton.LEFT, this.hit.getBlockPos(), this.hit.sideHit, this.hit.typeOfHit); + } + + if (this.rightClick && this.suppressRightTicks == 0) + { + this.broker.onPlayerClickedAir(this.getPlayer(), MouseButton.RIGHT, this.hit.getBlockPos(), this.hit.sideHit, this.hit.typeOfHit); + } + + if (this.suppressLeftTicks > 0) this.suppressLeftTicks--; + if (this.suppressRightTicks > 0) this.suppressRightTicks--; + + this.leftClick = false; + this.rightClick = false; + } + + public boolean onPlayerInteract(InteractType action, EntityPlayerMP player, BlockPos position, EnumFacing side) + { + this.hit = EntityUtilities.rayTraceFromEntity(player, this.traceDistance, 0.0F); + + if (action == InteractType.LEFT_CLICK) + { + this.leftClick = true; + return true; + } + + if (action == InteractType.RIGHT_CLICK) + { + this.rightClick = true; + return true; + } + + if ((action == InteractType.LEFT_CLICK_BLOCK || action == InteractType.DIG_BLOCK_MAYBE) && this.suppressLeftTicks == 0) + { + this.suppressLeftTicks += 2; + return this.broker.onPlayerClickedBlock(player, MouseButton.LEFT, position, side); + } + + if (action == InteractType.PLACE_BLOCK_MAYBE) + { + if (this.suppressRightTicks > 0) + { + return true; + } + + if (position.toLong() == PlayerEventState.MISS) + { + MovingObjectPosition actualHit = EntityUtilities.rayTraceFromEntity(player, player.capabilities.isCreativeMode ? 5.0 : 4.5, 0.0F); + if (actualHit.typeOfHit == MovingObjectType.MISS) + { + this.rightClick = true; + return true; + } + } + + this.suppressRightTicks++; + this.suppressLeftTicks++; + return this.broker.onPlayerClickedBlock(player, MouseButton.RIGHT, position, side); + } + + return true; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/PluginChannels.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/PluginChannels.java new file mode 100644 index 00000000..1622695b --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/PluginChannels.java @@ -0,0 +1,237 @@ +package com.mumfrey.liteloader.core; + +import io.netty.buffer.Unpooled; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.minecraft.network.INetHandler; +import net.minecraft.network.PacketBuffer; + +import com.google.common.base.Charsets; +import com.mumfrey.liteloader.api.InterfaceProvider; +import com.mumfrey.liteloader.interfaces.FastIterableDeque; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Manages plugin channel connections and subscriptions for LiteLoader + * + * @author Adam Mummery-Smith + */ +public abstract class PluginChannels implements InterfaceProvider +{ + // reserved channel consts + protected static final String CHANNEL_REGISTER = "REGISTER"; + protected static final String CHANNEL_UNREGISTER = "UNREGISTER"; + + /** + * Number of faults for a specific listener before a warning is generated + */ + protected static final int WARN_FAULT_THRESHOLD = 1000; + + /** + * Mapping of plugin channel names to listeners + */ + protected final HashMap> pluginChannels = new HashMap>(); + + /** + * List of mods which implement PluginChannelListener interface + */ + protected final FastIterableDeque pluginChannelListeners; + + /** + * Plugin channels that we know the server supports + */ + protected final Set remotePluginChannels = new HashSet(); + + /** + * Keep track of faulting listeners so that we can periodically log a + * message if a listener is throwing LOTS of exceptions. + */ + protected final Map faultingPluginChannelListeners = new HashMap(); + + /** + * Package private + */ + PluginChannels() + { + this.pluginChannelListeners = this.createHandlerList(); + } + + /** + * Spawn the handler list instance for this channel manager + */ + protected abstract FastIterableDeque createHandlerList(); + + /** + * Get the current set of registered client-side channels + */ + public Set getLocalChannels() + { + return Collections.unmodifiableSet(this.pluginChannels.keySet()); + } + + /** + * Get the current set of registered server channels + */ + public Set getRemoteChannels() + { + return Collections.unmodifiableSet(this.remotePluginChannels); + } + + /** + * Check whether a server plugin channel is registered + * + * @param channel + * @return true if the channel is registered at the server side + */ + public boolean isRemoteChannelRegistered(String channel) + { + return this.remotePluginChannels.contains(channel); + } + + /** + * @param pluginChannelListener + */ + protected void addPluginChannelListener(L pluginChannelListener) + { + this.pluginChannelListeners.add(pluginChannelListener); + } + + /** + * Connecting to a new server, clear plugin channels + * + * @param netHandler + */ + protected void clearPluginChannels(INetHandler netHandler) + { + this.pluginChannels.clear(); + this.remotePluginChannels.clear(); + this.faultingPluginChannelListeners.clear(); + } + + /** + * @param data + */ + protected void onRegisterPacketReceived(PacketBuffer data) + { + try + { + byte[] bytes = new byte[data.readableBytes()]; + data.readBytes(bytes); + String channels = new String(bytes, Charsets.UTF_8); + for (String channel : channels.split("\u0000")) + { + this.remotePluginChannels.add(channel); + } + } + catch (Exception ex) + { + LiteLoaderLogger.warning(ex, "Error decoding REGISTER packet from remote host %s", ex.getClass().getSimpleName()); + } + } + + /** + * + */ + protected PacketBuffer getRegistrationData() + { + // If any mods have registered channels, send the REGISTER packet + if (this.pluginChannels.keySet().size() > 0) + { + StringBuilder channelList = new StringBuilder(); + boolean separator = false; + + for (String channel : this.pluginChannels.keySet()) + { + if (separator) channelList.append("\u0000"); + channelList.append(channel); + separator = true; + } + + PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); + buffer.writeBytes(channelList.toString().getBytes(Charsets.UTF_8)); + return buffer; + } + + return null; + } + + /** + * Adds plugin channels for the specified listener to the local channels + * collection + * + * @param pluginChannelListener + */ + protected void addPluginChannelsFor(L pluginChannelListener) + { + List channels = pluginChannelListener.getChannels(); + + if (channels != null) + { + for (String channel : channels) + { + if (channel.length() > 16 || channel.toUpperCase().equals(CHANNEL_REGISTER) || channel.toUpperCase().equals(CHANNEL_UNREGISTER)) + { + continue; + } + + if (!this.pluginChannels.containsKey(channel)) + { + this.pluginChannels.put(channel, new LinkedList()); + } + + this.pluginChannels.get(channel).add(pluginChannelListener); + } + } + } + + /** + * Policy for dispatching plugin channel packets + * + * @author Adam Mummery-Smith + */ + public enum ChannelPolicy + { + /** + * Dispatch the message, throw an exception if the channel is not + * registered + */ + DISPATCH, + + /** + * Dispatch the message, return false if the channel is not registered + */ + DISPATCH_IF_REGISTERED, + + /** + * Dispatch the message + */ + DISPATCH_ALWAYS; + + /** + * True if this policy allows outbound traffic on the specified channel + * + * @param channel + */ + public boolean allows(PluginChannels channels, String channel) + { + if (this == ChannelPolicy.DISPATCH_ALWAYS) return true; + return channels.isRemoteChannelRegistered(channel); + } + + /** + * True if this policy does not throw an exception for unregistered + * outbound channels + */ + public boolean isSilent() + { + return (this != ChannelPolicy.DISPATCH_IF_REGISTERED); + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/Proxy.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/Proxy.java new file mode 100644 index 00000000..f494b5fe --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/Proxy.java @@ -0,0 +1,140 @@ +package com.mumfrey.liteloader.core; + +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.mojang.authlib.GameProfile; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.play.client.C03PacketPlayer; +import net.minecraft.network.play.client.C07PacketPlayerDigging; +import net.minecraft.network.play.client.C08PacketPlayerBlockPlacement; +import net.minecraft.network.play.client.C0APacketAnimation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.management.ItemInWorldManager; +import net.minecraft.server.management.ServerConfigurationManager; +import net.minecraft.util.BlockPos; +import net.minecraft.util.EnumFacing; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; + +public abstract class Proxy +{ + private static LiteLoaderEventBroker broker; + + protected Proxy() {} + + protected static void onStartupComplete() + { + Proxy.broker = LiteLoaderEventBroker.broker; + + if (Proxy.broker == null) + { + throw new RuntimeException("LiteLoader failed to start up properly." + + " The game is in an unstable state and must shut down now. Check the developer log for startup errors"); + } + } + + public static void onInitializePlayerConnection(ServerConfigurationManager source, NetworkManager netManager, EntityPlayerMP player) + { + Proxy.broker.onInitializePlayerConnection(source, netManager, player); + } + + public static void onPlayerLogin(ServerConfigurationManager source, EntityPlayerMP player) + { + Proxy.broker.onPlayerLogin(source, player); + } + + public static void onPlayerLogout(ServerConfigurationManager source, EntityPlayerMP player) + { + Proxy.broker.onPlayerLogout(source, player); + } + + public static void onSpawnPlayer(CallbackInfoReturnable cir, ServerConfigurationManager source, GameProfile profile) + { + Proxy.broker.onSpawnPlayer(source, cir.getReturnValue(), profile); + } + + public static void onRespawnPlayer(CallbackInfoReturnable cir, ServerConfigurationManager source, EntityPlayerMP oldPlayer, + int dimension, boolean won) + { + Proxy.broker.onRespawnPlayer(source, cir.getReturnValue(), oldPlayer, dimension, won); + } + + public static void onServerTick(MinecraftServer mcServer) + { + Proxy.broker.onServerTick(mcServer); + } + + public static void onPlaceBlock(CallbackInfo ci, NetHandlerPlayServer netHandler, C08PacketPlayerBlockPlacement packet) + { + if (!Proxy.broker.onPlaceBlock(netHandler, netHandler.playerEntity, packet.getPosition(), + EnumFacing.getFront(packet.getPlacedBlockDirection()))) + { + ci.cancel(); + } + } + + public static void onClickedAir(CallbackInfo ci, NetHandlerPlayServer netHandler, C0APacketAnimation packet) + { + if (!Proxy.broker.onClickedAir(netHandler)) + { + ci.cancel(); + } + } + + public static void onPlayerDigging(CallbackInfo ci, NetHandlerPlayServer netHandler, C07PacketPlayerDigging packet) + { + if (packet.getStatus() == C07PacketPlayerDigging.Action.START_DESTROY_BLOCK) + { + if (!Proxy.broker.onPlayerDigging(netHandler, packet.getPosition(), netHandler.playerEntity)) + { + ci.cancel(); + } + } + } + + public static void onUseItem(CallbackInfoReturnable ci, EntityPlayer player, World world, ItemStack itemStack, BlockPos pos, + EnumFacing side, float par8, float par9, float par10) + { + if (!(player instanceof EntityPlayerMP)) + { + return; + } + + if (!Proxy.broker.onUseItem(pos, side, (EntityPlayerMP)player)) + { + ci.setReturnValue(false); + } + } + + public static void onBlockClicked(CallbackInfo ci, ItemInWorldManager manager, BlockPos pos, EnumFacing side) + { + if (!Proxy.broker.onBlockClicked(pos, side, manager)) + { + ci.cancel(); + } + } + + public static void onPlayerMoved(CallbackInfo ci, NetHandlerPlayServer netHandler, C03PacketPlayer packet, WorldServer world, double oldPosX, + double oldPosY, double oldPosZ) + { + if (!Proxy.broker.onPlayerMove(netHandler, packet, netHandler.playerEntity, world)) + { + ci.cancel(); + } + } + + public static void onPlayerMoved(CallbackInfo ci, NetHandlerPlayServer netHandler, C03PacketPlayer packet, WorldServer world, double oldPosX, + double oldPosY, double oldPosZ, double deltaMoveSq, double deltaX, double deltaY, double deltaZ) + { + if (!Proxy.broker.onPlayerMove(netHandler, packet, netHandler.playerEntity, world)) + { + ci.cancel(); + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/ServerPluginChannels.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/ServerPluginChannels.java new file mode 100644 index 00000000..b95bf5ae --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/ServerPluginChannels.java @@ -0,0 +1,262 @@ +package com.mumfrey.liteloader.core; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.INetHandler; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.PacketBuffer; +import net.minecraft.network.play.client.C17PacketCustomPayload; +import net.minecraft.network.play.server.S3FPacketCustomPayload; + +import com.mumfrey.liteloader.ServerPluginChannelListener; +import com.mumfrey.liteloader.api.Listener; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.core.exceptions.UnregisteredChannelException; +import com.mumfrey.liteloader.interfaces.FastIterableDeque; +import com.mumfrey.liteloader.permissions.PermissionsManagerServer; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Handler for server plugin channels + * + * @author Adam Mummery-Smith + */ +public class ServerPluginChannels extends PluginChannels +{ + private static ServerPluginChannels instance; + + public ServerPluginChannels() + { + if (ServerPluginChannels.instance != null) + { + InstantiationException inner = new InstantiationException("Only a single instance of ServerPluginChannels is allowed"); + throw new RuntimeException("Plugin Channels Startup Error", inner); + } + ServerPluginChannels.instance = this; + } + + @Override + protected FastIterableDeque createHandlerList() + { + return new HandlerList(ServerPluginChannelListener.class); + } + + public static ServerPluginChannels getInstance() + { + return instance; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#initProvider() + */ + @Override + public void initProvider() + { + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#getListenerBaseType() + */ + @Override + public Class getListenerBaseType() + { + return Listener.class; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#registerInterfaces( + * com.mumfrey.liteloader.core.InterfaceRegistrationDelegate) + */ + @Override + public void registerInterfaces(InterfaceRegistrationDelegate delegate) + { + delegate.registerInterface(ServerPluginChannelListener.class); + } + + void addServerPluginChannelListener(ServerPluginChannelListener pluginChannelListener) + { + super.addPluginChannelListener(pluginChannelListener); + } + + void onServerStartup() + { + this.clearPluginChannels(null); + + // Enumerate mods for plugin channels + for (ServerPluginChannelListener pluginChannelListener : this.pluginChannelListeners) + { + this.addPluginChannelsFor(pluginChannelListener); + } + } + + void onPlayerJoined(EntityPlayerMP player) + { + this.sendRegisteredPluginChannels(player); + } + + /** + * Callback for the plugin channel hook + * + * @param netHandler + * @param customPayload + */ + public void onPluginChannelMessage(INetHandler netHandler, C17PacketCustomPayload customPayload) + { + if (customPayload != null && customPayload.getChannelName() != null) + { + String channel = customPayload.getChannelName(); + PacketBuffer data = customPayload.getBufferData(); + + EntityPlayerMP sender = ((NetHandlerPlayServer)netHandler).playerEntity; + this.onPluginChannelMessage(sender, channel, data); + } + } + + /** + * @param channel + * @param data + */ + private final void onPluginChannelMessage(EntityPlayerMP sender, String channel, PacketBuffer data) + { + if (PluginChannels.CHANNEL_REGISTER.equals(channel)) + { + this.onRegisterPacketReceived(data); + } + else if (this.pluginChannels.containsKey(channel)) + { + try + { + PermissionsManagerServer permissionsManager = LiteLoader.getServerPermissionsManager(); + if (permissionsManager != null) + { + permissionsManager.onCustomPayload(sender, channel, data); + } + } + catch (Exception ex) {} + + this.onModPacketReceived(sender, channel, data); + } + } + + /** + * @param sender + * @param channel + * @param data + */ + protected void onModPacketReceived(EntityPlayerMP sender, String channel, PacketBuffer data) + { + for (ServerPluginChannelListener pluginChannelListener : this.pluginChannels.get(channel)) + { + try + { + pluginChannelListener.onCustomPayload(sender, channel, data); + throw new RuntimeException(); + } + catch (Exception ex) + { + int failCount = 1; + if (this.faultingPluginChannelListeners.containsKey(pluginChannelListener)) + { + failCount = this.faultingPluginChannelListeners.get(pluginChannelListener).intValue() + 1; + } + + if (failCount >= PluginChannels.WARN_FAULT_THRESHOLD) + { + LiteLoaderLogger.warning("Plugin channel listener %s exceeded fault threshold on channel %s with %s", + pluginChannelListener.getName(), channel, ex.getClass().getSimpleName()); + this.faultingPluginChannelListeners.remove(pluginChannelListener); + } + else + { + this.faultingPluginChannelListeners.put(pluginChannelListener, Integer.valueOf(failCount)); + } + } + } + } + + protected void sendRegisteredPluginChannels(EntityPlayerMP player) + { + try + { + PacketBuffer registrationData = this.getRegistrationData(); + if (registrationData != null) + { + this.sendRegistrationData(player, registrationData); + } + } + catch (Exception ex) + { + LiteLoaderLogger.warning(ex, "Error dispatching REGISTER packet to client %s", player.getDisplayName()); + } + } + + /** + * @param recipient + * @param registrationData + */ + private void sendRegistrationData(EntityPlayerMP recipient, PacketBuffer registrationData) + { + ServerPluginChannels.dispatch(recipient, new S3FPacketCustomPayload(CHANNEL_REGISTER, registrationData)); + } + + /** + * Send a message to the specified client on a plugin channel + * + * @param recipient + * @param channel Channel to send, must not be a reserved channel name + * @param data + */ + public static boolean sendMessage(EntityPlayerMP recipient, String channel, PacketBuffer data, ChannelPolicy policy) + { + if (ServerPluginChannels.instance != null) + { + return ServerPluginChannels.instance.send(recipient, channel, data, policy); + } + + return false; + } + + /** + * Send a message to the specified client on a plugin channel + * + * @param recipient Recipient to send to + * @param channel Channel to send, must not be a reserved channel name + * @param data + */ + private boolean send(EntityPlayerMP recipient, String channel, PacketBuffer data, ChannelPolicy policy) + { + if (recipient == null) return false; + + if (channel == null || channel.length() > 16 || CHANNEL_REGISTER.equals(channel) || CHANNEL_UNREGISTER.equals(channel)) + { + throw new RuntimeException("Invalid channel name specified"); + } + + if (!policy.allows(this, channel)) + { + if (policy.isSilent()) return false; + throw new UnregisteredChannelException(channel); + } + + S3FPacketCustomPayload payload = new S3FPacketCustomPayload(channel, data); + return ServerPluginChannels.dispatch(recipient, payload); + } + + /** + * @param recipient + * @param payload + */ + static boolean dispatch(EntityPlayerMP recipient, S3FPacketCustomPayload payload) + { + try + { + if (recipient != null && recipient.playerNetServerHandler != null) + { + recipient.playerNetServerHandler.sendPacket(payload); + return true; + } + } + catch (Exception ex) {} + + return false; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/api/DefaultClassValidator.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/DefaultClassValidator.java new file mode 100644 index 00000000..0a2266d2 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/DefaultClassValidator.java @@ -0,0 +1,48 @@ +package com.mumfrey.liteloader.core.api; + +import java.util.List; + +import com.mumfrey.liteloader.api.ModClassValidator; + +public class DefaultClassValidator implements ModClassValidator +{ + private final Class superClass; + + private final List supportedPrefixes; + + public DefaultClassValidator(Class superClass, List supportedPrefixes) + { + this.supportedPrefixes = supportedPrefixes; + this.superClass = superClass; + } + + @Override + public boolean validateName(String className) + { + return this.supportedPrefixes == null + || this.supportedPrefixes.size() == 0 + || DefaultClassValidator.startsWithAny(className, this.supportedPrefixes); + } + + @Override + public boolean validateClass(ClassLoader classLoader, Class candidateClass) + { + return (candidateClass != null + && !this.superClass.equals(candidateClass) + && this.superClass.isAssignableFrom(candidateClass) + && !candidateClass.isInterface()); + } + + private static boolean startsWithAny(String string, List candidates) + { + for (String candidate : candidates) + { + if (string.startsWith(candidate)) + { + return true; + } + } + + return false; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/api/DefaultEnumeratorPlugin.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/DefaultEnumeratorPlugin.java new file mode 100644 index 00000000..1aee5970 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/DefaultEnumeratorPlugin.java @@ -0,0 +1,190 @@ +package com.mumfrey.liteloader.core.api; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.mumfrey.liteloader.api.ContainerRegistry; +import com.mumfrey.liteloader.api.EnumeratorPlugin; +import com.mumfrey.liteloader.api.ModClassValidator; +import com.mumfrey.liteloader.api.manager.APIProvider; +import com.mumfrey.liteloader.core.exceptions.OutdatedLoaderException; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +public class DefaultEnumeratorPlugin implements EnumeratorPlugin +{ + private LoaderEnvironment environment; + + @Override + public void init(LoaderEnvironment environment, LoaderProperties properties) + { + this.environment = environment; + } + + @Override + public boolean checkEnabled(ContainerRegistry containers, LoadableMod container) + { + return container.isEnabled(this.environment); + } + + @Override + public boolean checkAPIRequirements(ContainerRegistry containers, LoadableMod container) + { + boolean result = true; + APIProvider apiProvider = this.environment.getAPIProvider(); + + for (String identifier : container.getRequiredAPIs()) + { + if (!apiProvider.isAPIAvailable(identifier)) + { + container.registerMissingAPI(identifier); + result = false; + } + } + + return result; + } + + @Override + public boolean checkDependencies(ContainerRegistry containers, LoadableMod base) + { + if (base == null || !base.hasDependencies()) return true; + + HashSet circularDependencySet = new HashSet(); + circularDependencySet.add(base.getIdentifier()); + + boolean result = this.checkDependencies(containers, base, base, circularDependencySet); + LiteLoaderLogger.info(Verbosity.REDUCED, "Dependency check for %s %s", base.getIdentifier(), result ? "passed" : "failed"); + + return result; + } + + private boolean checkDependencies(ContainerRegistry containers, LoadableMod base, LoadableMod container, Set circularDependencySet) + { + if (container.getDependencies().size() == 0) + { + return true; + } + + boolean result = true; + + for (String dependency : container.getDependencies()) + { + if (!circularDependencySet.contains(dependency)) + { + circularDependencySet.add(dependency); + + LoadableMod dependencyContainer = containers.getEnabledContainer(dependency); + if (dependencyContainer != LoadableMod.NONE) + { + String identifier = dependency; + if (this.environment.getEnabledModsList().isEnabled(this.environment.getProfile(), identifier)) + { + result &= this.checkDependencies(containers, base, dependencyContainer, circularDependencySet); + } + else + { +// LiteLoaderLogger.warning("Dependency %s required by %s is currently disabled", dependency, base.getIdentifier()); + base.registerMissingDependency(dependency); + result = false; + } + } + else + { +// LiteLoaderLogger.info("Dependency %s for %s is was not located, no container ", dependency, base.getIdentifier()); + base.registerMissingDependency(dependency); + result = false; + } + } + } + + return result; + } + + /** + * Enumerate classes on the classpath which are subclasses of the specified + * class + */ + @Override + public List> getClasses(LoadableMod container, ClassLoader classloader, ModClassValidator validator) + { + List> classes = new ArrayList>(); + + if (container != null) + { + try + { + for (String fullClassName : container.getContainedClassNames()) + { + boolean isDefaultPackage = fullClassName.lastIndexOf('.') == -1; + String className = isDefaultPackage ? fullClassName : fullClassName.substring(fullClassName.lastIndexOf('.') + 1); + if (validator.validateName(className)) + { + Class clazz = DefaultEnumeratorPlugin.checkClass(classloader, validator, fullClassName); + if (clazz != null && !classes.contains(clazz)) + { + classes.add(clazz); + } + } + } + } + catch (OutdatedLoaderException ex) + { + classes.clear(); + LiteLoaderLogger.info(Verbosity.REDUCED, "Error searching in '%s', missing API component '%s', your loader is probably out of date", + container, ex.getMessage()); + } + catch (Throwable th) + { + LiteLoaderLogger.warning(th, "Enumeration error"); + } + } + + return classes; + } + + @SuppressWarnings("unchecked") + private static Class checkClass(ClassLoader classLoader, ModClassValidator validator, String className) + throws OutdatedLoaderException + { + if (className.indexOf('$') > -1) + { + return null; + } + + try + { + Class candidateClass = classLoader.loadClass(className); + + if (validator.validateClass(classLoader, candidateClass)) + { + return (Class)candidateClass; + } + } + catch (Throwable th) + { + th.printStackTrace(); + + if (th.getCause() != null) + { + String missingClassName = th.getCause().getMessage(); + if (th.getCause() instanceof NoClassDefFoundError && missingClassName != null) + { + if (missingClassName.startsWith("com/mumfrey/liteloader/")) + { + throw new OutdatedLoaderException(missingClassName.substring(missingClassName.lastIndexOf('/') + 1)); + } + } + } + + LiteLoaderLogger.warning(th, "checkAndAddClass error while checking '%s'", className); + } + + return null; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/api/EnumeratorModuleClassPath.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/EnumeratorModuleClassPath.java new file mode 100644 index 00000000..ecf5ec37 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/EnumeratorModuleClassPath.java @@ -0,0 +1,143 @@ +package com.mumfrey.liteloader.core.api; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.launchwrapper.LaunchClassLoader; + +import com.mumfrey.liteloader.api.EnumeratorModule; +import com.mumfrey.liteloader.common.LoadingProgress; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.interfaces.ModularEnumerator; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +/** + * Enumerator module which searches for mods on the classpath + * + * @author Adam Mummery-Smith + */ +public class EnumeratorModuleClassPath implements EnumeratorModule +{ + /** + * Array of class path entries specified to the JVM instance + */ + private final String[] classPathEntries; + + /** + * URLs to add once init is completed + */ + private final List> loadableMods = new ArrayList>(); + + private boolean loadTweaks; + + public EnumeratorModuleClassPath() + { + // Read the JVM class path into the local array + this.classPathEntries = this.readClassPath(); + } + + @Override + public String toString() + { + return ""; + } + + @Override + public void init(LoaderEnvironment environment, LoaderProperties properties) + { + this.loadTweaks = properties.loadTweaksEnabled(); + } + + @Override + public void writeSettings(LoaderEnvironment environment, LoaderProperties properties) + { + } + + /** + * Reads the class path entries that were supplied to the JVM and returns + * them as an array. + */ + private String[] readClassPath() + { + LiteLoaderLogger.info("Enumerating class path..."); + + String classPath = System.getProperty("java.class.path"); + String classPathSeparator = System.getProperty("path.separator"); + String[] classPathEntries = classPath.split(classPathSeparator); + + LiteLoaderLogger.info("Class path separator=\"%s\"", classPathSeparator); + LiteLoaderLogger.info("Class path entries=(\n classpathEntry=%s\n)", classPath.replace(classPathSeparator, "\n classpathEntry=")); + return classPathEntries; + } + + @Override + public void enumerate(ModularEnumerator enumerator, String profile) + { + if (this.loadTweaks) + { + LiteLoaderLogger.info("Discovering tweaks on class path..."); + + for (String classPathPart : this.classPathEntries) + { + try + { + File packagePath = new File(classPathPart); + if (packagePath.exists()) + { + LoadableModClassPath classPathMod = new LoadableModClassPath(packagePath); + if (enumerator.registerModContainer(classPathMod)) + { + this.loadableMods.add(classPathMod); + if (classPathMod.requiresPreInitInjection()) + { + enumerator.registerTweakContainer(classPathMod); + } + } + else + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Mod %s is disabled or missing a required dependency, not injecting tranformers", + classPathMod.getIdentifier()); + } + } + } + catch (Throwable th) + { + LiteLoaderLogger.warning(th, "Error encountered whilst inspecting %s", classPathPart); + } + } + } + } + + @Override + public void injectIntoClassLoader(ModularEnumerator enumerator, LaunchClassLoader classLoader) + { + } + + /** + * @param classLoader + */ + @Override + public void registerMods(ModularEnumerator enumerator, LaunchClassLoader classLoader) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Discovering mods on class path..."); + LoadingProgress.incTotalLiteLoaderProgress(this.loadableMods.size()); + + for (LoadableMod classPathMod : this.loadableMods) + { + LiteLoaderLogger.info("Searching %s...", classPathMod); + LoadingProgress.incLiteLoaderProgress("Searching for mods in " + classPathMod.getModName() + "..."); + try + { + enumerator.registerModsFrom(classPathMod, true); + } + catch (Exception ex) + { + LiteLoaderLogger.warning(ex, "Error encountered whilst searching in %s...", classPathMod); + } + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/api/EnumeratorModuleFolder.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/EnumeratorModuleFolder.java new file mode 100644 index 00000000..7313fed0 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/EnumeratorModuleFolder.java @@ -0,0 +1,411 @@ +package com.mumfrey.liteloader.core.api; + +import java.io.File; +import java.io.FilenameFilter; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + +import net.minecraft.launchwrapper.LaunchClassLoader; + +import com.google.common.base.Charsets; +import com.mumfrey.liteloader.api.EnumeratorModule; +import com.mumfrey.liteloader.common.LoadingProgress; +import com.mumfrey.liteloader.core.LiteLoaderVersion; +import com.mumfrey.liteloader.interfaces.LoadableFile; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.interfaces.ModularEnumerator; +import com.mumfrey.liteloader.interfaces.TweakContainer; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +/** + * Enumerator module which searches for mods and tweaks in a folder + * + * @author Adam Mummery-Smith + */ +public class EnumeratorModuleFolder implements FilenameFilter, EnumeratorModule +{ + /** + * Ordered sets used to sort mods by version/revision + */ + protected final Map>> versionOrderingSets = new HashMap>>(); + + /** + * Mods to add once init is completed + */ + protected final List> loadableMods = new ArrayList>(); + + protected LiteLoaderCoreAPI coreAPI; + + protected File directory; + + protected boolean readJarFiles; + protected boolean loadTweaks; + protected boolean forceInjection; + + /** + * True if this is a versioned folder and the enumerator should also try to + * load tweak jars which would normally be ignored. + */ + protected final boolean loadTweakJars; + + public EnumeratorModuleFolder(LiteLoaderCoreAPI coreAPI, File directory, boolean loadTweakJars) + { + this.coreAPI = coreAPI; + this.directory = directory; + this.loadTweakJars = loadTweakJars; + } + + @Override + public void init(LoaderEnvironment environment, LoaderProperties properties) + { + this.loadTweaks = properties.loadTweaksEnabled(); + this.readJarFiles = properties.getAndStoreBooleanProperty(LoaderProperties.OPTION_SEARCH_JARFILES, true); + this.forceInjection = properties.getAndStoreBooleanProperty(LoaderProperties.OPTION_FORCE_INJECTION, false); + + this.coreAPI.writeDiscoverySettings(); + } + + /** + * Write settings + */ + @Override + public void writeSettings(LoaderEnvironment environment, LoaderProperties properties) + { + properties.setBooleanProperty(LoaderProperties.OPTION_SEARCH_JARFILES, this.readJarFiles); + properties.setBooleanProperty(LoaderProperties.OPTION_FORCE_INJECTION, this.forceInjection); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + return this.directory.getAbsolutePath(); + } + + /** + * Get the directory this module will inspect + */ + public File getDirectory() + { + return this.directory; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.Enumerator#getLoadableMods() + */ + public List> getLoadableMods() + { + return this.loadableMods; + } + + /** + * For FilenameFilter interface + * + * @see java.io.FilenameFilter#accept(java.io.File, java.lang.String) + */ + @Override + public boolean accept(File dir, String fileName) + { + fileName = fileName.toLowerCase(); + + if (fileName.endsWith(".litemod.zip")) + { + LiteLoaderLogger.warning("Found %s with unsupported extension .litemod.zip." + + " Please change file extension to .litemod to allow this file to be loaded!", fileName); + return true; + } + + return fileName.endsWith(".litemod") || fileName.endsWith(".jar"); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.Enumerator + * #enumerate(com.mumfrey.liteloader.core.EnabledModsList, + * java.lang.String) + */ + @Override + public void enumerate(ModularEnumerator enumerator, String profile) + { + if (this.directory.exists() && this.directory.isDirectory()) + { + LiteLoaderLogger.info("Discovering valid mod files in folder %s", this.directory.getPath()); + + this.findValidFiles(enumerator); + this.sortAndRegisterFiles(enumerator); + } + } + + /** + * Search the folder for (potentially) valid files + */ + private void findValidFiles(ModularEnumerator enumerator) + { + for (File file : this.directory.listFiles(this.getFilenameFilter())) + { + LoadableFile candidateFile = new LoadableFile(file); + candidateFile.setForceInjection(this.forceInjection); + try + { + this.inspectFile(enumerator, candidateFile); + } + catch (Exception ex) + { + LiteLoaderLogger.warning(ex, "An error occurred whilst inspecting %s", candidateFile); + } + } + } + + /** + * Check whether a particular file is valid, and add it to the candiates + * list if it appears to be acceptable. + * + * @param enumerator + * @param candidateFile + */ + protected void inspectFile(ModularEnumerator enumerator, LoadableFile candidateFile) + { + if (this.isValidFile(enumerator, candidateFile)) + { + String metaData = candidateFile.getFileContents(LoadableMod.METADATA_FILENAME, Charsets.UTF_8); + if (metaData != null) + { + LoadableMod modFile = this.getModFile(candidateFile, metaData); + this.addModFile(enumerator, modFile); + return; + } + else if (this.isValidTweakContainer(candidateFile)) + { + TweakContainer container = this.getTweakFile(candidateFile); + this.addTweakFile(enumerator, container); + return; + } + else + { + LiteLoaderLogger.info("Ignoring %s", candidateFile); +// enumerator.registerBadContainer(candidateFile, "No metadata"); + } + } +// else +// { +// enumerator.registerBadContainer(candidateFile, "Not a valid file"); +// } + } + + /** + * Check whether the specified file is a valid mod container + * + * @param enumerator + * @param candidateFile + */ + protected boolean isValidFile(ModularEnumerator enumerator, LoadableFile candidateFile) + { + String filename = candidateFile.getName().toLowerCase(); + if (filename.endsWith(".litemod.zip")) + { + enumerator.registerBadContainer(candidateFile, "Invalid file extension .litemod.zip"); + return false; + } + else if (filename.endsWith(".litemod")) + { + return true; + } + else if (filename.endsWith(".jar")) + { + Set modSystems = candidateFile.getModSystems(); + boolean hasLiteLoader = modSystems.contains("LiteLoader"); + if (modSystems.size() > 0) + { + LiteLoaderLogger.info("%s supports mod systems %s", candidateFile, modSystems); + if (!hasLiteLoader) return false; + } + + return this.loadTweakJars || this.readJarFiles || hasLiteLoader; + } + + return false; + } + + /** + * Called only if the file is not a valid mod container (has no mod + * metadata) to check whether it could instead be a potential tweak + * container. + * + * @param candidateFile + */ + protected boolean isValidTweakContainer(LoadableFile candidateFile) + { + return this.loadTweakJars && this.loadTweaks && candidateFile.getName().toLowerCase().endsWith(".jar"); + } + + /** + * Get the {@link FilenameFilter} to use to filter candidate files + */ + protected FilenameFilter getFilenameFilter() + { + return this; + } + + /** + * @param modFile + */ + protected boolean isFileSupported(LoadableMod modFile) + { + return LiteLoaderVersion.CURRENT.isVersionSupported(modFile.getTargetVersion()); + } + + /** + * @param candidateFile + * @param metaData + */ + protected LoadableMod getModFile(LoadableFile candidateFile, String metaData) + { + return new LoadableModFile(candidateFile, metaData); + } + + /** + * @param candidateFile + */ + protected TweakContainer getTweakFile(LoadableFile candidateFile) + { + return candidateFile; + } + + /** + * @param enumerator + * @param modFile + */ + protected void addModFile(ModularEnumerator enumerator, LoadableMod modFile) + { + if (modFile.hasValidMetaData()) + { + // Only add the mod if the version matches, we add candidates to the versionOrderingSets in + // order to determine the most recent version available. + if (this.isFileSupported(modFile)) + { + if (!this.versionOrderingSets.containsKey(modFile.getName())) + { + this.versionOrderingSets.put(modFile.getModName(), new TreeSet>()); + } + + LiteLoaderLogger.info("Considering valid mod file: %s", modFile); + this.versionOrderingSets.get(modFile.getModName()).add(modFile); + } + else + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Not adding invalid or version-mismatched mod file: %s", modFile); + enumerator.registerBadContainer(modFile, "Version not supported"); + } + } + } + + /** + * @param enumerator + * @param container + */ + protected void addTweakFile(ModularEnumerator enumerator, TweakContainer container) + { + enumerator.registerTweakContainer(container); + } + + /** + * @param enumerator + */ + protected void sortAndRegisterFiles(ModularEnumerator enumerator) + { + // Copy the first entry in every version set into the modfiles list + for (Entry>> modFileEntry : this.versionOrderingSets.entrySet()) + { + LoadableMod newestVersion = modFileEntry.getValue().iterator().next(); + this.registerFile(enumerator, newestVersion); + } + + this.versionOrderingSets.clear(); + } + + /** + * @param enumerator + * @param modFile + */ + @SuppressWarnings("unchecked") + protected void registerFile(ModularEnumerator enumerator, LoadableMod modFile) + { + if (enumerator.registerModContainer(modFile)) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Adding newest valid mod file '%s' at revision %.4f", modFile, modFile.getRevision()); + this.loadableMods.add(modFile); + } + else + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Not adding valid mod file '%s', the specified mod is disabled or missing a required dependency", + modFile); + } + + if (this.loadTweaks) + { + try + { + if (modFile instanceof TweakContainer) + { + this.addTweakFile(enumerator, (TweakContainer)modFile); + } + } + catch (Throwable th) + { + LiteLoaderLogger.warning("Error adding tweaks from '%s'", modFile); + } + } + } + + @Override + public void injectIntoClassLoader(ModularEnumerator enumerator, LaunchClassLoader classLoader) + { + LiteLoaderLogger.info("Injecting external mods into class path..."); + + for (LoadableMod loadableMod : this.loadableMods) + { + try + { + if (loadableMod.injectIntoClassPath(classLoader, false)) + { + LiteLoaderLogger.info("Successfully injected mod file '%s' into classpath", loadableMod); + } + } + catch (MalformedURLException ex) + { + LiteLoaderLogger.warning("Error injecting '%s' into classPath. The mod will not be loaded", loadableMod); + } + } + } + + @Override + public void registerMods(ModularEnumerator enumerator, LaunchClassLoader classLoader) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Discovering mods in valid mod files..."); + LoadingProgress.incTotalLiteLoaderProgress(this.loadableMods.size()); + + for (LoadableMod modFile : this.loadableMods) + { + LoadingProgress.incLiteLoaderProgress("Searching for mods in " + modFile.getModName() + "..."); + LiteLoaderLogger.info("Searching %s...", modFile); + try + { + enumerator.registerModsFrom(modFile, true); + } + catch (Exception ex) + { + LiteLoaderLogger.warning("Error encountered whilst searching in %s...", modFile); + } + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/api/LiteLoaderCoreAPI.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/LiteLoaderCoreAPI.java new file mode 100644 index 00000000..fcd7610c --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/LiteLoaderCoreAPI.java @@ -0,0 +1,176 @@ +package com.mumfrey.liteloader.core.api; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel; + +import com.mumfrey.liteloader.api.EnumeratorModule; +import com.mumfrey.liteloader.api.LiteAPI; +import com.mumfrey.liteloader.api.MixinConfigProvider; +import com.mumfrey.liteloader.core.LiteLoaderVersion; +import com.mumfrey.liteloader.interfaces.ObjectFactory; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.launch.LoaderProperties; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * LiteLoader's API impl. + * + * @author Adam Mummery-Smith + */ +public abstract class LiteLoaderCoreAPI implements LiteAPI, MixinConfigProvider +{ + protected static final String PKG_LITELOADER = "com.mumfrey.liteloader"; + protected static final String PKG_LITELOADER_COMMON = LiteLoaderCoreAPI.PKG_LITELOADER + ".common"; + + protected LoaderEnvironment environment; + + protected LoaderProperties properties; + + protected boolean searchClassPath; + protected boolean searchModsFolder; + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getIdentifier() + */ + @Override + public String getIdentifier() + { + return "liteloader"; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getName() + */ + @Override + public String getName() + { + return "LiteLoader core API"; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getVersion() + */ + @Override + public String getVersion() + { + return LiteLoaderVersion.CURRENT.getLoaderVersion(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getRevision() + */ + @Override + public int getRevision() + { + return LiteLoaderVersion.CURRENT.getLoaderRevision(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getModClassPrefix() + */ + @Override + public String getModClassPrefix() + { + return "LiteMod"; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#init( + * com.mumfrey.liteloader.launch.LoaderEnvironment, + * com.mumfrey.liteloader.launch.LoaderProperties) + */ + @Override + public void init(LoaderEnvironment environment, LoaderProperties properties) + { + this.environment = environment; + this.properties = properties; + } + + /** + * Get the discovery settings from the properties file + */ + void readDiscoverySettings() + { + this.searchModsFolder = this.properties.getAndStoreBooleanProperty(LoaderProperties.OPTION_SEARCH_MODS, true); + this.searchClassPath = this.properties.getAndStoreBooleanProperty(LoaderProperties.OPTION_SEARCH_CLASSPATH, true); + + if (!this.searchModsFolder && !this.searchClassPath) + { + LiteLoaderLogger.warning("Invalid configuration, no search locations defined. Enabling all search locations."); + + this.searchModsFolder = true; + this.searchClassPath = true; + } + } + + /** + * Write settings + */ + void writeDiscoverySettings() + { + this.properties.setBooleanProperty(LoaderProperties.OPTION_SEARCH_MODS, this.searchModsFolder); + this.properties.setBooleanProperty(LoaderProperties.OPTION_SEARCH_CLASSPATH, this.searchClassPath); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.LiteAPI#getEnumeratorModules() + */ + @Override + public List getEnumeratorModules() + { + this.readDiscoverySettings(); + + List enumeratorModules = new ArrayList(); + + if (this.searchClassPath) + { + enumeratorModules.add(new EnumeratorModuleClassPath()); + } + + if (this.searchModsFolder) + { + File modsFolder = this.environment.getModsFolder(); + enumeratorModules.add(new EnumeratorModuleFolder(this, modsFolder, false)); + + File versionedModsFolder = this.environment.getVersionedModsFolder(); + enumeratorModules.add(new EnumeratorModuleFolder(this, versionedModsFolder, true)); + } + + return Collections.unmodifiableList(enumeratorModules); + } + + /** + * Get the ObjectFactory + */ + public abstract ObjectFactory getObjectFactory(); + + @Override + public MixinConfigProvider getMixins() + { + return this; + } + + @Override + public CompatibilityLevel getCompatibilityLevel() + { + return null; + } + + @Override + public String[] getMixinConfigs() + { + return new String[] { + "mixins.liteloader.core.json" + }; + } + + @Override + public String[] getErrorHandlers() + { + return null; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/api/LoadableModClassPath.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/LoadableModClassPath.java new file mode 100644 index 00000000..778036f4 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/LoadableModClassPath.java @@ -0,0 +1,91 @@ +package com.mumfrey.liteloader.core.api; + +import java.io.File; +import java.net.MalformedURLException; + +import net.minecraft.launchwrapper.LaunchClassLoader; + +import com.mumfrey.liteloader.core.LiteLoaderVersion; + +/** + * Mod file reference for a file loaded from class path + * + * @author Adam Mummery-Smith + */ +public class LoadableModClassPath extends LoadableModFile +{ + private static final long serialVersionUID = -4759310661966590773L; + + private boolean modNameRequired = false; + + LoadableModClassPath(File file) + { + this(file, null); + } + + LoadableModClassPath(File file, String fallbackName) + { + super(file, LoadableModFile.getVersionMetaDataString(file)); + + if (this.modName == null) + { + if (fallbackName != null) + { + this.modName = fallbackName; + } + else if (this.isFile()) + { + this.modName = this.getName().substring(0, this.getName().lastIndexOf('.')); + } + else + { + String parentFileName = this.getParentFile() != null ? this.getParentFile().getName().toLowerCase() : ""; + this.modName = String.format("%s.%s", parentFileName, this.getName().toLowerCase()); + this.modNameRequired = true; + } + } + + if (this.targetVersion == null) this.targetVersion = LiteLoaderVersion.CURRENT.getMinecraftVersion(); + } + + @Override + protected void readJarMetaData() + { + // Nope + } + + @Override + protected String getDefaultName() + { + return null; + } + + @Override + public String getDisplayName() + { + return this.getModName(); + } + + @Override + public boolean injectIntoClassPath(LaunchClassLoader classLoader, boolean injectIntoParent) throws MalformedURLException + { + // Can't inject a class path entry into the class path! + return false; + } + + @Override + public boolean isInjected() + { + return true; + } + + @Override + public void addContainedMod(String modName) + { + if (this.modNameRequired) + { + this.modNameRequired = false; + this.modName = modName; + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/api/LoadableModFile.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/LoadableModFile.java new file mode 100644 index 00000000..011e9afc --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/api/LoadableModFile.java @@ -0,0 +1,598 @@ +package com.mumfrey.liteloader.core.api; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import joptsimple.internal.Strings; + +import com.google.common.base.Charsets; +import com.google.common.io.ByteStreams; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.mumfrey.liteloader.api.manager.APIProvider; +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.interfaces.LoadableFile; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.launch.InjectionStrategy; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Wrapper for file which represents a mod file to load with associated version + * information and metadata. Retrieve this from litemod.json at enumeration + * time. We also override comparable to provide our own custom sorting logic + * based on version info. + * + * @author Adam Mummery-Smith + */ +public class LoadableModFile extends LoadableFile implements LoadableMod +{ + private static final long serialVersionUID = -7952147161905688459L; + + /** + * Maximum recursion depth for mod discovery + */ + private static final int MAX_DISCOVERY_DEPTH = 16; + + /** + * Gson parser for JSON + */ + protected static Gson gson = new Gson(); + + /** + * True if the metadata information is parsed successfully, the mod will be + * added. + */ + protected boolean valid = false; + + /** + * Name of the mod specified in the JSON file, this can be any string but + * should be the same between mod versions. + */ + protected String modName; + + /** + * Loader version + */ + protected String targetVersion; + + /** + * Name of the class transof + */ + protected List classTransformerClassNames = new ArrayList(); + + /** + * File time stamp, used as sorting criteria when no revision information is + * found. + */ + protected long timeStamp; + + /** + * Revision number from the json file + */ + protected float revision = 0.0F; + + /** + * True if the revision number was successfully read, used as a semaphore so + * that we know when revision is a valid number. + */ + protected boolean hasRevision = false; + + /** + * ALL of the parsed metadata from the file, associated with the mod later + * on for retrieval via the loader. + */ + protected Map metaData = new HashMap(); + + /** + * Dependencies declared in the metadata + */ + private Set dependencies = new HashSet(); + + /** + * Dependencies which are missing + */ + private Set missingDependencies = new HashSet();; + + /** + * Required APIs declared in the metadata + */ + private Set requiredAPIs = new HashSet(); + + /** + * Required APIs which are missing + */ + private Set missingAPIs = new HashSet(); + + /** + * Classes in this container + */ + protected List classNames = null; + + /** + * @param file + * @param metaData + */ + protected LoadableModFile(File file, String metaData) + { + super(file.getAbsolutePath()); + this.init(metaData); + } + + /** + * @param file + * @param metaData + */ + protected LoadableModFile(LoadableFile file, String metaData) + { + super(file); + this.init(metaData); + } + + /** + * @param metaData + */ + @SuppressWarnings("unchecked") + protected void init(String metaData) + { + this.timeStamp = this.lastModified(); + this.tweakPriority = 0; + + if (!Strings.isNullOrEmpty(metaData)) + { + try + { + this.metaData = LoadableModFile.gson.fromJson(metaData, HashMap.class); + } + catch (JsonSyntaxException jsx) + { + LiteLoaderLogger.warning("Error reading %s in %s, JSON syntax exception: %s", + LoadableMod.METADATA_FILENAME, this.getAbsolutePath(), jsx.getMessage()); + return; + } + + this.valid = this.parseMetaData(); + } + } + + protected boolean parseMetaData() + { + try + { + this.modName = this.getMetaValue("name", this.getDefaultName()); + this.displayName = this.getMetaValue("displayName", this.modName); + this.version = this.getMetaValue("version", "Unknown"); + this.author = this.getMetaValue("author", "Unknown"); + + if (!this.parseVersions()) return false; + + this.injectionStrategy = InjectionStrategy.parseStrategy(this.getMetaValue("injectAt", null)); + + this.tweakClassName = this.getMetaValue("tweakClass", this.tweakClassName); + + this.getMetaValuesInto(this.classTransformerClassNames, "classTransformerClasses", ","); + this.getMetaValuesInto(this.dependencies, "dependsOn", ","); + this.getMetaValuesInto(this.requiredAPIs, "requiredAPIs", ","); + this.getMetaValuesInto(this.mixinConfigs, "mixinConfigs", ","); + } + catch (ClassCastException ex) + { + LiteLoaderLogger.debug(ex); + LiteLoaderLogger.warning("Error parsing version metadata file in %s, check the format of the file", this.getAbsolutePath()); + } + + return true; + } + + public boolean parseVersions() + { + this.targetVersion = this.getMetaValue("mcversion", null); + if (this.targetVersion == null) + { + LiteLoaderLogger.warning("Mod in %s has no loader version number reading %s", this.getAbsolutePath(), LoadableMod.METADATA_FILENAME); + return false; + } + + try + { + this.revision = Float.parseFloat(this.getMetaValue("revision", null)); + this.hasRevision = true; + } + catch (NullPointerException ex) {} + catch (Exception ex) + { + LiteLoaderLogger.warning("Mod in %s has an invalid revision number reading %s", this.getAbsolutePath(), LoadableMod.METADATA_FILENAME); + } + + return true; + } + + protected String getDefaultName() + { + return this.getName().replaceAll("[^a-zA-Z]", ""); + } + + @Override + public String getModName() + { + return this.modName; + } + + @Override + public String getIdentifier() + { + return this.modName.toLowerCase(); + } + + @Override + public String getDescription(String key) + { + if (this.missingAPIs.size() > 0) + { + return LiteLoader.translate("gui.description.missingapis", "\n" + this.compileMissingAPIList()); + } + + if (this.missingDependencies.size() > 0) + { + return LiteLoader.translate("gui.description.missingdeps", "\n" + this.missingDependencies.toString()); + } + + String descriptionKey = "description"; + if (key != null && key.length() > 0) + { + descriptionKey += "." + key.toLowerCase(); + } + + return this.getMetaValue(descriptionKey, this.getMetaValue("description", "")); + } + + private String compileMissingAPIList() + { + StringBuilder missingAPIList = new StringBuilder(); + + for (String missingAPI : this.missingAPIs) + { + if (missingAPI != null) + { + if (missingAPI.contains("@")) + { + Matcher matcher = APIProvider.idAndRevisionPattern.matcher(missingAPI); + if (matcher.matches()) + { + missingAPIList.append(" ").append(matcher.group(1)).append(" (revision ").append(matcher.group(2)).append(")\n"); + continue; + } + } + + missingAPIList.append(" ").append(missingAPI).append("\n"); + } + } + + return missingAPIList.toString(); + } + + @Override + public boolean isEnabled(LoaderEnvironment environment) + { + return this.missingDependencies.size() == 0 && this.missingAPIs.size() == 0 && super.isEnabled(environment); + } + + @Override + public boolean isExternalJar() + { + return false; + } + + @Override + public boolean isToggleable() + { + return true; + } + + @Override + public boolean hasValidMetaData() + { + return this.valid; + } + + @Override + public String getTargetVersion() + { + return this.targetVersion; + } + + @Override + public float getRevision() + { + return this.revision; + } + + protected Object getMetaValue(String metaKey) + { + Object metaValue = this.metaData.get(metaKey); + if (metaValue != null) return metaValue; + return this.metaData.get(metaKey.toLowerCase()); + } + + @Override + public String getMetaValue(String metaKey, String defaultValue) + { + Object metaValue = this.getMetaValue(metaKey); + return metaValue != null ? metaValue.toString() : defaultValue; + } + + @SuppressWarnings("unchecked") + public String[] getMetaValues(String metaKey, String separator) + { + Object metaValue = this.getMetaValue(metaKey); + + if (metaValue instanceof String) + { + return ((String)metaValue).split(separator); + } + else if (metaValue instanceof ArrayList) + { + return ((ArrayList)metaValue).toArray(new String[0]); + } + + return new String[0]; + } + + protected void getMetaValuesInto(Collection collection, String metaKey, String separator) + { + for (String name : this.getMetaValues(metaKey, separator)) + { + if (!Strings.isNullOrEmpty(name)) + { + collection.add(name); + } + } + } + + @Override + public Set getMetaDataKeys() + { + return Collections.unmodifiableSet(this.metaData.keySet()); + } + + @Override + public boolean hasClassTransformers() + { + return this.classTransformerClassNames.size() > 0; + } + + @Override + public List getClassTransformerClassNames() + { + return this.classTransformerClassNames; + } + + @Override + public boolean hasResources() + { + return true; + } + + @Override + public boolean hasDependencies() + { + return this.dependencies.size() > 0; + } + + @Override + public Set getDependencies() + { + return this.dependencies; + } + + @Override + public void registerMissingDependency(String dependency) + { + this.missingDependencies.add(dependency); + } + + @Override + public Set getMissingDependencies() + { + return this.missingDependencies; + } + + @Override + public Set getRequiredAPIs() + { + return this.requiredAPIs; + } + + @Override + public void registerMissingAPI(String identifier) + { + this.missingAPIs.add(identifier); + } + + @Override + public Set getMissingAPIs() + { + return this.missingAPIs; + } + + @Override + public List getContainedClassNames() + { + if (this.classNames == null) + { + this.classNames = this.enumerateClassNames(); + } + + return this.classNames; + } + + protected List enumerateClassNames() + { + if (this.isDirectory()) + { + return LoadableModFile.enumerateDirectory(new ArrayList(), this, "", 0); + } + + return LoadableModFile.enumerateZipFile(this); + } + + @Override + public void addContainedMod(String modName) + { + } + + @Override + public int compareTo(File other) + { + if (other == null || !(other instanceof LoadableModFile)) return -1; + + LoadableModFile otherMod = (LoadableModFile)other; + + // If the other object has a revision, compare revisions + if (otherMod.hasRevision) + { + return this.hasRevision && this.revision - otherMod.revision > 0 ? -1 : 1; + } + + // If we have a revision and the other object doesn't, then we are higher + if (this.hasRevision) + { + return -1; + } + + // Give up and use timestamp + return (int)(otherMod.timeStamp - this.timeStamp); + } + + protected static List enumerateZipFile(File file) + { + List classes = new ArrayList(); + + ZipFile zipFile; + try + { + zipFile = new ZipFile(file); + } + catch (IOException ex) + { + return classes; + } + + @SuppressWarnings("unchecked") + Enumeration entries = (Enumeration)zipFile.entries(); + while (entries.hasMoreElements()) + { + ZipEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (entry.getSize() > 0 && entryName.endsWith(".class")) + { + classes.add(entryName.substring(0, entryName.length() - 6).replace('/', '.')); + } + } + + try + { + zipFile.close(); + } + catch (IOException ex) {} + + return classes; + } + + /** + * Recursive function to enumerate classes inside a classpath folder + * + * @param classes + * @param packagePath + * @param packageName + */ + protected static List enumerateDirectory(List classes, File packagePath, String packageName, int depth) + { + // Prevent crash due to broken recursion + if (depth > MAX_DISCOVERY_DEPTH) + { + return classes; + } + + File[] classFiles = packagePath.listFiles(); + + for (File classFile : classFiles) + { + if (classFile.isDirectory()) + { + LoadableModFile.enumerateDirectory(classes, classFile, packageName + classFile.getName() + ".", depth + 1); + } + else + { + if (classFile.getName().endsWith(".class")) + { + String classFileName = classFile.getName(); + classes.add(packageName + classFileName.substring(0, classFileName.length() - 6)); + } + } + } + + return classes; + } + + /** + * @param zip + * @param entry + * @throws IOException + */ + public static String zipEntryToString(ZipFile zip, ZipEntry entry) throws IOException + { + InputStream stream = null; + Charset charset = Charsets.UTF_8; + int bomOffset = 0; + byte[] bytes; + + try + { + stream = zip.getInputStream(entry); + bytes = ByteStreams.toByteArray(stream); + } + finally + { + if (stream != null) stream.close(); + } + + if (bytes == null || bytes.length == 0) return ""; + + // Handle unicode by looking for BOM + if (bytes.length > 1) + { + if (bytes[0] == (byte)0xFF && bytes[1] == (byte)0xFE) + { + charset = Charsets.UTF_16LE; + bomOffset = 2; + } + else if (bytes[0] == (byte)0xFE && bytes[1] == (byte)0xFF) + { + charset = Charsets.UTF_16BE; + bomOffset = 2; + } + } + + return new String(bytes, bomOffset, bytes.length - bomOffset, charset); + } + + protected static String getVersionMetaDataString(File file) + { + return LoadableFile.getFileContents(file, LoadableMod.METADATA_FILENAME, Charsets.UTF_8); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/event/Cancellable.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/Cancellable.java new file mode 100644 index 00000000..c88d5ca7 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/Cancellable.java @@ -0,0 +1,28 @@ +package com.mumfrey.liteloader.core.event; + +/** + * Interface for (potentially) cancellable things :) + * + * @author Adam Mummery-Smith + */ +public interface Cancellable +{ + /** + * Get whether this is actually cancellable + */ + public abstract boolean isCancellable(); + + /** + * Get whether this is cancelled + */ + public abstract boolean isCancelled(); + + /** + * If the object is cancellable, cancels the object, implementors may throw + * an EventCancellationException if the object is not actually cancellable. + * + * @throws EventCancellationException (optional) may be thrown if the object + * is not actually cancellable + */ + public abstract void cancel() throws EventCancellationException; +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/event/EventCancellationException.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/EventCancellationException.java new file mode 100644 index 00000000..de7f18a5 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/EventCancellationException.java @@ -0,0 +1,25 @@ +package com.mumfrey.liteloader.core.event; + +public class EventCancellationException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public EventCancellationException() + { + } + + public EventCancellationException(String message) + { + super(message); + } + + public EventCancellationException(Throwable cause) + { + super(cause); + } + + public EventCancellationException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/event/EventProxy.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/EventProxy.java new file mode 100644 index 00000000..00288a22 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/EventProxy.java @@ -0,0 +1,212 @@ +package com.mumfrey.liteloader.core.event; + +import java.util.concurrent.Callable; + +import javax.management.RuntimeErrorException; + +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; + +import org.objectweb.asm.Type; + +import com.mumfrey.liteloader.transformers.ByteCodeUtilities; +import com.mumfrey.liteloader.transformers.event.EventInfo; + +/** + * EventProxy is a special class used by the EventInjectionTransformer, it is a + * stub class into which all of the injected event callback methods are + * injected. Each event handler method contains a try/catch block which invokes + * one of the error reporting methods contained below when an error occurs, in + * order to provide more meaningful information to the user and to mod makers. + * + * @author Adam Mummery-Smith + */ +public final class EventProxy +{ + static String error; + static StringBuilder errorDetails; + + static + { + new Exception().printStackTrace(); + if (true) throw new InstantiationError("EventProxy was loaded before transformation, this is bad!"); + } + + /** + * Private because we never instance this class! + */ + private EventProxy() {} + + // The event injection subsystem creates event stubs in this class which (if written in java) would + // look something like the following: + // + // public static void $event00000(EventInfo e) + // { + // try + // { + // // Handlers sorted by priority + // com.example.mod.EventHandler.onWhateverEvent(e); + // com.example.anothermod.FooClass.onBarEvent(e); + // } + // catch (NoClassDefFoundError err) + // { + // onMissingClass(err, e); + // } + // catch (NoSuchMethodError err) + // { + // onMissingHandler(err, e); + // } + // } + + protected static void onMissingClass(Error err, EventInfo e) + { + EventProxy.error = "Missing Event Handler Class!"; + EventProxy.errorDetails = new StringBuilder(); + + EventProxy.addCrashDetailLine("\n"); + EventProxy.addCrashDetailLine("You are seeing this message because an event callback was injected by the Event"); + EventProxy.addCrashDetailLine("Injection Subsystem but the specified callback class was not defined! The"); + EventProxy.addCrashDetailLine("details of the missing callback are as follows:"); + EventProxy.addDetailLineBreak(); + EventProxy.addCrashDetailLine(" Event Name: " + e.getName()); + EventProxy.addCrashDetailLine(" Cancellable: " + e.isCancellable()); + EventProxy.addDetailLineBreak(); + EventProxy.addCrashDetailLine(" Callback class: " + err.getMessage().replace('/', '.')); + EventProxy.addDetailLineBreak(); + EventProxy.addCrashDetailLine("If you are the mod author then in order to fix the error you must provide an"); + EventProxy.addCrashDetailLine("implementation for the specified class, or check that the class name and package"); + EventProxy.addCrashDetailLine("are correct."); + EventProxy.addDetailLineBreak(); + EventProxy.addCrashDetailLine("This is an unrecoverable error, please report it to the mod author and remove"); + EventProxy.addCrashDetailLine("the offending mod."); + EventProxy.addStackTrace(err); + + throw new RuntimeErrorException(err, "Missing event handler class for event " + e.getName() + ", see crash report for details"); + } + + protected static void onMissingHandler(Error err, EventInfo e) + { + String descriptor = err.getMessage(); + int dotPos = descriptor.lastIndexOf('.'); + int bracketPos = descriptor.indexOf('('); + + String signature = descriptor.substring(bracketPos); + String sourceClass = e.getSource() != null ? e.getSource().getClass().getSimpleName() : "?"; + + EventProxy.error = "Missing Event Handler Method!"; + EventProxy.errorDetails = new StringBuilder(); + + EventProxy.addCrashDetailLine("\n"); + EventProxy.addCrashDetailLine("You are seeing this message because an event callback was injected by the Event"); + EventProxy.addCrashDetailLine("Injection Subsystem but the specified callback method was not defined. The"); + EventProxy.addCrashDetailLine("details of the missing callback are as follows:"); + EventProxy.addDetailLineBreak(); + EventProxy.addCrashDetailLine(" Event Name: " + e.getName()); + EventProxy.addCrashDetailLine(" Cancellable: " + e.isCancellable()); + EventProxy.addDetailLineBreak(); + EventProxy.addCrashDetailLine(" Callback class: " + descriptor.substring(0, dotPos)); + EventProxy.addCrashDetailLine(" Callback method: " + descriptor.substring(dotPos + 1, bracketPos)); + EventProxy.addDetailLineBreak(); + EventProxy.addCrashDetailLine("If you are the mod author then in order to fix the error you must add a suitable"); + EventProxy.addCrashDetailLine("callback method in the above class. The method signature should be as follows:"); + EventProxy.addDetailLineBreak(); + EventProxy.addCrashDetailLine(EventProxy.generateHandlerTemplate(descriptor.substring(dotPos + 1, bracketPos), signature, sourceClass)); + EventProxy.addDetailLineBreak(); + EventProxy.addCrashDetailLine("This is an unrecoverable error, please report it to the mod author and remove"); + EventProxy.addCrashDetailLine("the offending mod."); + EventProxy.addStackTrace(err); + + throw new RuntimeErrorException(err, "Missing event handler method for event " + e.getName() + ", see crash report for details"); + } + + private static void addStackTrace(Error err) + { + EventProxy.addDetailLineBreak(); + EventProxy.errorDetails.append("Stacktrace:").append('\n'); + EventProxy.addDetailLineBreak(); + + StackTraceElement[] stackTrace = err.getStackTrace(); + for (int i = 0; i < stackTrace.length; i++) + { + EventProxy.addCrashDetailLine(String.format(" %3d) %s", i + 1, stackTrace[i])); + } + } + + protected static String generateHandlerTemplate(String methodName, String signature, String sourceClass) + { + Type[] argTypes = Type.getArgumentTypes(signature); + + StringBuilder tpl = new StringBuilder(); + tpl.append(" public static void ").append(methodName).append('('); + for (int var = 0; var < argTypes.length; var++) + { + if (EventProxy.appendTypeName(tpl, argTypes[var], sourceClass)) tpl.append("[]"); + if (var == 0) tpl.append(" e"); + if (var > 0) tpl.append(" arg").append(String.valueOf(var)); + if (var < argTypes.length - 1) tpl.append(", "); + } + tpl.append(")\n\t {\n\t // handler code here\n\t }"); + + String template = tpl.toString(); + if (template.contains(", ReturnType>")) + { + template = template.replace("static void", "static void"); + } + return template; + } + + private static boolean appendTypeName(StringBuilder tpl, Type type, String sourceClass) + { + switch (type.getSort()) + { + case Type.ARRAY: + EventProxy.appendTypeName(tpl, type.getElementType(), sourceClass); + return true; + case Type.OBJECT: + String typeName = type.getClassName(); + typeName = typeName.substring(typeName.lastIndexOf('.') + 1); + tpl.append(typeName); + if (typeName.endsWith("ReturnEventInfo")) + { + tpl.append('<').append(sourceClass).append(", ReturnType>"); + } + else if (typeName.endsWith("EventInfo")) + { + tpl.append('<').append(sourceClass).append('>'); + } + return false; + default: + tpl.append(ByteCodeUtilities.getTypeName(type)); + return false; + } + } + + private static void addDetailLineBreak() + { + System.err.println(); + EventProxy.errorDetails.append('\n'); + } + + private static void addCrashDetailLine(String string) + { + System.err.println(string); + EventProxy.errorDetails.append('\t').append(string).append('\n'); + } + + public static void populateCrashReport(CrashReport crashReport) + { + if (EventProxy.error != null) + { + CrashReportCategory category = crashReport.makeCategoryDepth("Event Handler Error", 1); + + category.addCrashSectionCallable(EventProxy.error, new Callable() + { + @Override + public String call() throws Exception + { + return EventProxy.errorDetails.toString(); + } + }); + } + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/event/HandlerList.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/HandlerList.java new file mode 100644 index 00000000..9eb77168 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/HandlerList.java @@ -0,0 +1,1112 @@ +package com.mumfrey.liteloader.core.event; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import net.minecraft.launchwrapper.IClassTransformer; +import net.minecraft.launchwrapper.Launch; + +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.core.helpers.Booleans; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.objectweb.asm.util.CheckClassAdapter; + +import com.mumfrey.liteloader.Priority; +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.interfaces.FastIterableDeque; +import com.mumfrey.liteloader.transformers.ByteCodeUtilities; +import com.mumfrey.liteloader.util.SortableValue; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * HandlerList is a generic class which supports baking a list of event handlers + * into a dynamic inner class for invocation at runtime. + * + * @author Adam Mummery-Smith + * + * @param + */ +public class HandlerList extends LinkedList implements FastIterableDeque +{ + private static final long serialVersionUID = 1L; + + private static final int MAX_UNCOLLECTED_CLASSES = 5000; + + private static int uncollectedHandlerLists = 0; + + /** + * Enum for logic operations supported between handlers which return bool + */ + public enum ReturnLogicOp + { + /** + * Logical OR applied between handlers, return FALSE unless one or more + * handlers returns TRUE + */ + OR(true, false), + + /** + * Logical OR, returns TRUE at the first handler to return TRUE and + * doesn't process any further handlers. + */ + OR_BREAK_ON_TRUE(true, true), + + /** + * Logical OR, but with the difference than an EMPTY handler list will + * return TRUE. + */ + OR_ASSUME_TRUE(true, false, true), + + /** + * Logical AND, returns TRUE if the list is empty or if all handlers + * return TRUE. + */ + AND(false, false), + + /** + * Logical AND, returns FALSE at the first handler to return FALSE and + * doesn't process any further handlers. + */ + AND_BREAK_ON_FALSE(false, true); + + private final boolean isOr; + + private final boolean breakOnMatch; + + private final boolean assumeTrue; + + private ReturnLogicOp(boolean isOr, boolean breakOnMatch) + { + this(isOr, breakOnMatch, false); + } + + private ReturnLogicOp(boolean isOr, boolean breakOnMatch, boolean assumeTrue) + { + this.isOr = isOr; + this.breakOnMatch = breakOnMatch; + this.assumeTrue = assumeTrue; + } + + boolean isOr() + { + return this.isOr; + } + + public boolean breakOnMatch() + { + return this.breakOnMatch; + } + + boolean assumeTrue() + { + return this.assumeTrue; + } + } + + /** + * Type of the interface for objects in this handler list + */ + private final Class type; + + /** + * + */ + private final ReturnLogicOp logicOp; + + /** + * Current baked handler list, we cook them at gas mark 5 for 30 minutes in + * a disposable classloader whic also handles the transformation for us. + */ + private BakedHandlerList bakedHandler; + + /** + * True to sort the list when baking + */ + private boolean sorted = true; + + /** + * @param type + */ + public HandlerList(Class type) + { + this(type, ReturnLogicOp.AND_BREAK_ON_FALSE); + } + + /** + * @param type + * @param logicOp Logical operation to apply to interface methods which + * return boolean + */ + public HandlerList(Class type, ReturnLogicOp logicOp) + { + this(type, logicOp, true); + } + + /** + * @param type + * @param logicOp Logical operation to apply to interface methods which + * return boolean + * @param sorted True to sort the list when baking (doesn't sort the + * underlying list) + */ + public HandlerList(Class type, ReturnLogicOp logicOp, boolean sorted) + { + if (!type.isInterface()) + { + throw new IllegalArgumentException("HandlerList type argument must be an interface"); + } + + this.type = type; + this.logicOp = logicOp; + this.sorted = sorted; + } + + /** + * True if the list will be sorted by priority on bake + */ + public boolean isSorted() + { + return this.sorted; + } + + /** + * Set whether to sort list entries before baking them + */ + public void setSorted(boolean sorted) + { + this.sorted = sorted; + this.invalidate(); + } + + @SuppressWarnings("unchecked") + protected List getSortedList() + { + if (!this.sorted) return this; + + SortableValue[] sortable = new SortableValue[this.size()]; + for (int s = 0; s < this.size(); s++) + { + T value = this.get(s); + sortable[s] = new SortableValue(this.getPriority(value), s, value); + } + + Arrays.sort(sortable); + + List sortedList = new ArrayList(this.size()); + for (int s = 0; s < sortable.length; s++) + { + sortedList.add(sortable[s].getValue()); + } + + return sortedList; + } + + private int getPriority(T value) + { + Priority priority = value.getClass().getAnnotation(Priority.class); + if (priority != null) + { + return priority.value(); + } + + return 1000; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.interfaces.FastIterable#all() + */ + @Override + public T all() + { + if (this.bakedHandler == null) + { + this.bake(); + } + + return this.bakedHandler.get(); + } + + /** + * Bake the current handler list + */ + protected void bake() + { + HandlerListClassLoader classLoader = new HandlerListClassLoader(this.type, this.logicOp, this.getDecorator()); + this.bakedHandler = classLoader.newHandler(this); + if (classLoader instanceof Closeable) + { + try + { + ((Closeable)classLoader).close(); + } + catch (IOException ex) {} + } + } + + protected IHandlerListDecorator getDecorator() + { + return null; + } + + /** + * Invalidate current baked list + */ + @Override + public void invalidate() + { + if (this.bakedHandler == null) + { + return; + } + + this.bakedHandler = null; + HandlerList.uncollectedHandlerLists++; + if (HandlerList.uncollectedHandlerLists > HandlerList.MAX_UNCOLLECTED_CLASSES) + { + System.gc(); + HandlerList.uncollectedHandlerLists = 0; + } + } + + /* (non-Javadoc) + * @see java.util.LinkedList#add(java.lang.Object) + */ + @Override + public boolean add(T listener) + { + if (!this.contains(listener)) + { + super.add(listener); + this.invalidate(); + } + + return true; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#offer(java.lang.Object) + */ + @Override + public boolean offer(T listener) + { + return this.add(listener); + } + + /* (non-Javadoc) + * @see java.util.LinkedList#offerFirst(java.lang.Object) + */ + @Override + public boolean offerFirst(T listener) + { + this.addFirst(listener); + return true; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#offerLast(java.lang.Object) + */ + @Override + public boolean offerLast(T listener) + { + this.addLast(listener); + return true; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#add(int, java.lang.Object) + */ + @Override + public void add(int index, T listener) + { + if (!this.contains(listener)) + { + super.add(index, listener); + this.invalidate(); + } + } + + /* (non-Javadoc) + * @see java.util.LinkedList#addFirst(java.lang.Object) + */ + @Override + public void addFirst(T listener) + { + if (!this.contains(listener)) + { + super.addFirst(listener); + this.invalidate(); + } + } + + /* (non-Javadoc) + * @see java.util.LinkedList#addLast(java.lang.Object) + */ + @Override + public void addLast(T listener) + { + if (!this.contains(listener)) + { + super.addLast(listener); + this.invalidate(); + } + } + + /* (non-Javadoc) + * @see java.util.LinkedList#addAll(java.util.Collection) + */ + @Override + public boolean addAll(Collection listeners) + { + for (T listener : listeners) + { + if (!this.contains(listener)) + { + super.add(listener); + } + } + + this.invalidate(); + return true; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#addAll(int, java.util.Collection) + */ + @Override + public boolean addAll(int index, Collection listeners) + { + throw new UnsupportedOperationException("'addAll' is not supported for HandlerList"); + } + + /* (non-Javadoc) + * @see java.util.LinkedList#remove() + */ + @Override + public T remove() + { + return this.removeFirst(); + } + + /* (non-Javadoc) + * @see java.util.LinkedList#remove(int) + */ + @Override + public T remove(int index) + { + T removed = super.remove(index); + this.invalidate(); + return removed; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#remove(java.lang.Object) + */ + @Override + public boolean remove(Object listener) + { + boolean removed = super.remove(listener); + this.invalidate(); + return removed; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#removeFirst() + */ + @Override + public T removeFirst() + { + T removed = super.removeFirst(); + this.invalidate(); + return removed; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#removeFirstOccurrence(java.lang.Object) + */ + @Override + public boolean removeFirstOccurrence(Object listener) + { + return this.remove(listener); + } + + /* (non-Javadoc) + * @see java.util.LinkedList#removeLast() + */ + @Override + public T removeLast() + { + T removed = super.removeLast(); + this.invalidate(); + return removed; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#removeLastOccurrence(java.lang.Object) + */ + @Override + public boolean removeLastOccurrence(Object listener) + { + boolean removed = super.removeLastOccurrence(listener); + this.invalidate(); + return removed; + } + + /* (non-Javadoc) + * @see java.util.AbstractCollection#removeAll(java.util.Collection) + */ + @Override + public boolean removeAll(Collection listeners) + { + boolean removed = super.removeAll(listeners); + this.invalidate(); + return removed; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#poll() + */ + @Override + public T poll() + { + T polled = super.poll(); + this.invalidate(); + return polled; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#pollFirst() + */ + @Override + public T pollFirst() + { + T polled = super.pollFirst(); + this.invalidate(); + return polled; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#pollLast() + */ + @Override + public T pollLast() + { + T polled = super.pollLast(); + this.invalidate(); + return polled; + } + + /* (non-Javadoc) + * @see java.util.LinkedList#push(java.lang.Object) + */ + @Override + public void push(T listener) + { + this.addFirst(listener); + } + + /* (non-Javadoc) + * @see java.util.LinkedList#pop() + */ + @Override + public T pop() + { + return this.removeFirst(); + } + + /* (non-Javadoc) + * @see java.util.LinkedList#set(int, java.lang.Object) + */ + @Override + public T set(int index, T listener) + { + T oldValue = null; + + if (!this.contains(listener)) + { + oldValue = super.set(index, listener); + this.invalidate(); + } + + return oldValue; + } + + /** + * Base class for baked handler lists + * + * @author Adam Mummery-Smith + * + * @param + */ + public abstract static class BakedHandlerList + { + public abstract T get(); + + public abstract BakedHandlerList populate(List listeners); + } + + /** + * Exception to throw when failing to bake a handler list + * + * @author Adam Mummery-Smith + */ + static class BakingFailedException extends RuntimeException + { + private static final long serialVersionUID = 1L; + + public BakingFailedException(Throwable cause) + { + super("An unexpected error occurred while baking the handler list", cause); + } + } + + /** + * ClassLoader which generates the baked handler list + * + * @author Adam Mummery-Smith + * @param + */ + static class HandlerListClassLoader extends URLClassLoader + { + private static final String HANDLER_VAR_PREFIX = "handler$"; + + public static final boolean DUMP = Booleans.parseBoolean(System.getProperty("liteloader.debug.dump"), false); + + public static final boolean VALIDATE = Booleans.parseBoolean(System.getProperty("liteloader.debug.validate"), false); + + /** + * Unique index number, just to ensure no name clashes + */ + private static int handlerIndex; + + /** + * Interface type which this classloader is generating handler for + */ + private final Class type; + + /** + * Calculated class ref for the class type so that we don't have to keep + * calling getName().replace('.', '/') + */ + private final String typeRef; + + /** + * Logic operation to apply when running a callback with a boolean + */ + private final ReturnLogicOp logicOp; + + /** + * Bytecode decorator + */ + private final IHandlerListDecorator decorator; + + /** + * Size of the handler list + */ + private int size; + + /** + * @param type + * @param logicOp + */ + HandlerListClassLoader(Class type, ReturnLogicOp logicOp, IHandlerListDecorator decorator) + { + super(new URL[0], Launch.classLoader); + this.type = type; + this.typeRef = type.getName().replace('.', '/'); + this.logicOp = logicOp; + this.decorator = decorator; + } + + /** + * Create and return a new baked handler list + */ + @SuppressWarnings("unchecked") + public BakedHandlerList newHandler(HandlerList list) + { + this.size = list.size(); + List sortedList = list.getSortedList(); + + if (this.decorator != null) + { + this.decorator.prepare(sortedList); + } + + Class> handlerClass = null; + + try + { + // Inflect the class name and attempt to generate the class + String className = HandlerListClassLoader.getNextClassName(Obf.HandlerList.name, this.type.getSimpleName()); + handlerClass = (Class>)this.loadClass(className); + } + catch (ClassNotFoundException ex) + { + throw new BakingFailedException(ex); + } + + try + { + // Create an instance of the class, populate the entries from the supplied list and return it + BakedHandlerList handlerList = this.createInstance(handlerClass); + return handlerList.populate(sortedList); + } + catch (InstantiationException ex) + { + throw new BakingFailedException(ex); + } + } + + /** + * Create an instance of the baked class + * + * @param handlerClass Baked HandlerList class + * @return new instance of the Baked HandlerList class + * @throws InstantiationException if the handler can't be created for + * some reason + */ + private BakedHandlerList createInstance(Class> handlerClass) throws InstantiationException + { + try + { + if (this.decorator != null) + { + return this.decorator.createInstance(handlerClass); + } + + Constructor> ctor = handlerClass.getDeclaredConstructor(); + ctor.setAccessible(true); + return ctor.newInstance(); + } + catch (Exception ex) + { + InstantiationException ie = new InstantiationException("Error instantiating class " + handlerClass); + ie.setStackTrace(ex.getStackTrace()); + throw ie; + } + } + + /* (non-Javadoc) + * @see java.net.URLClassLoader#findClass(java.lang.String) + */ + @Override + protected Class findClass(String name) throws ClassNotFoundException + { + try + { + // Read the basic class template + byte[] bytes = ByteCodeUtilities.applyTransformers(this.getTemplate().name, + Launch.classLoader.getClassBytes(this.getTemplate().name)); + ClassReader classReader = new ClassReader(bytes); + ClassNode classNode = new ClassNode(); + classReader.accept(classNode, ClassReader.EXPAND_FRAMES); + + // Apply all transformations to the class, injects our custom code + this.transform(name, classNode); + + // Write the class + ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + classNode.accept(classWriter); + bytes = classWriter.toByteArray(); + + if (HandlerListClassLoader.VALIDATE) + { + classNode.accept(new CheckClassAdapter(new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES))); + } + + if (HandlerListClassLoader.DUMP) + { + FileUtils.writeByteArrayToFile(new File(".classes/" + name.replace('.', '/') + ".class"), bytes); + } + + // Delegate to ClassLoader's usual behaviour to load the class we just generated + return this.defineClass(name, bytes, 0, bytes.length); + } + catch (Throwable th) + { + th.printStackTrace(); + return null; + } + } + + private Obf getTemplate() + { + if (this.decorator != null) + { + return this.decorator.getTemplate(); + } + + return Obf.BakedHandlerList; + } + + /** + * Perform all class bytecode transformations + * + * @param name + * @param classNode + * @throws IOException + */ + private void transform(String name, ClassNode classNode) throws IOException + { + LiteLoaderLogger.info("Baking listener list for %s with %d listeners", this.type.getSimpleName(), this.size); + LiteLoaderLogger.debug("Generating: %s", name); + + this.populateClass(name, classNode); + this.transformMethods(name, classNode); + + Set generatedMethods = new HashSet(); + this.injectInterfaceMethods(classNode, this.type.getName(), generatedMethods); + } + + /** + * Populate the class node itself + * + * @param name + * @param classNode + */ + private void populateClass(String name, ClassNode classNode) + { + classNode.access = classNode.access & ~Opcodes.ACC_ABSTRACT; + classNode.name = name.replace('.', '/'); + classNode.superName = this.getTemplate().ref; + classNode.interfaces.add(this.typeRef); + classNode.sourceFile = name.substring(name.lastIndexOf('.') + 1) + ".java"; + + for (int handlerIndex = 0; handlerIndex < this.size; handlerIndex++) + { + classNode.fields.add(new FieldNode(Opcodes.ACC_PRIVATE, HandlerListClassLoader.HANDLER_VAR_PREFIX + handlerIndex, + "L" + this.typeRef + ";", null, null)); + } + + if (this.decorator != null) + { + this.decorator.populateClass(name, classNode); + } + } + + /** + * Transform existing methods in the template class + * + * @param name + * @param classNode + */ + private void transformMethods(String name, ClassNode classNode) + { + for (Iterator methodIterator = classNode.methods.iterator(); methodIterator.hasNext();) + { + MethodNode method = methodIterator.next(); + if (Obf.constructor.name.equals(method.name)) + { + this.processCtor(classNode, method); + } + else if ("get".equals(method.name)) + { + this.processGet(classNode, method); + } + else if ("populate".equals(method.name)) + { + this.processPopulate(classNode, method); + } + } + } + + /** + * Transform the constructor + * + * @param classNode + * @param method + */ + private void processCtor(ClassNode classNode, MethodNode method) + { + for (Iterator iter = method.instructions.iterator(); iter.hasNext();) + { + AbstractInsnNode insn = iter.next(); + if (insn instanceof MethodInsnNode) + { + MethodInsnNode methodInsn = (MethodInsnNode)insn; + if (methodInsn.getOpcode() == Opcodes.INVOKESPECIAL && methodInsn.name.equals(Obf.constructor.name)) + { + methodInsn.owner = this.getTemplate().ref; + } + } + } + + if (this.decorator != null) + { + this.decorator.processCtor(classNode, method); + } + } + + /** + * Transform .get() + * + * @param classNode + * @param method + */ + private void processGet(ClassNode classNode, MethodNode method) + { + method.access = method.access & ~Opcodes.ACC_ABSTRACT; + method.instructions.clear(); + + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + method.instructions.add(new InsnNode(Opcodes.ARETURN)); + + method.maxStack = 1; + method.maxLocals = 1; + } + + /** + * Transform .processPopulate() + * + * @param classNode + * @param method + */ + private void processPopulate(ClassNode classNode, MethodNode method) + { + method.access = method.access & ~Opcodes.ACC_ABSTRACT; + method.instructions.clear(); + + for (int handlerIndex = 0; handlerIndex < this.size; handlerIndex++) + { + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); + method.instructions.add(handlerIndex > Short.MAX_VALUE ? new LdcInsnNode(new Integer(handlerIndex)) + : new IntInsnNode(Opcodes.SIPUSH, handlerIndex)); + method.instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true)); + method.instructions.add(new TypeInsnNode(Opcodes.CHECKCAST, this.typeRef)); + method.instructions.add(new FieldInsnNode(Opcodes.PUTFIELD, classNode.name, HandlerListClassLoader.HANDLER_VAR_PREFIX + handlerIndex, + "L" + this.typeRef + ";")); + } + + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + method.instructions.add(new InsnNode(Opcodes.ARETURN)); + + method.maxStack = 3; + method.maxLocals = 2; + } + + /** + * Recurse down the interface inheritance hierarchy and inject methods + * to handle each interface. + * + * @param classNode + * @param interfaceName + * @param generatedMethods + * @throws IOException + */ + private void injectInterfaceMethods(ClassNode classNode, String interfaceName, Set generatedMethods) throws IOException + { + ClassReader interfaceReader = new ClassReader(HandlerListClassLoader.getInterfaceBytes(interfaceName)); + ClassNode interfaceNode = new ClassNode(); + interfaceReader.accept(interfaceNode, 0); + + for (MethodNode interfaceMethod : interfaceNode.methods) + { + String signature = interfaceMethod.name + interfaceMethod.desc; + if (generatedMethods.contains(signature)) continue; + generatedMethods.add(signature); + classNode.methods.add(interfaceMethod); + this.populateInterfaceMethod(classNode, interfaceMethod); + } + + for (String parentInterface : interfaceNode.interfaces) + { + this.injectInterfaceMethods(classNode, parentInterface.replace('/', '.'), generatedMethods); + } + } + + /** + * Inject the supplied interface method into the target class and + * populate it with method calls to the list members + * + * @param classNode + * @param method + */ + private void populateInterfaceMethod(ClassNode classNode, MethodNode method) + { + Type returnType = Type.getReturnType(method.desc); + Type[] args = Type.getArgumentTypes(method.desc); + + if (returnType.equals(Type.BOOLEAN_TYPE)) + { + method.access = Opcodes.ACC_PUBLIC; + this.populateBooleaninvocationChain(classNode, method, args); + } + else + { + method.access = Opcodes.ACC_PUBLIC; + this.populateVoidinvocationChain(classNode, method, args, returnType); + } + + if (this.decorator != null) + { + this.decorator.populateInterfaceMethod(classNode, method); + } + } + + /** + * @param classNode + * @param method + * @param args + */ + private void populateVoidinvocationChain(ClassNode classNode, MethodNode method, Type[] args, Type returnType) + { + int returnSize = returnType.getSize(); + for (int handlerIndex = 0; handlerIndex < this.size; handlerIndex++) + { + this.invokeHandler(handlerIndex, classNode, method, args); + if (returnSize > 0) + { + method.instructions.add(new InsnNode(returnSize == 1 ? Opcodes.POP : Opcodes.POP2)); + } + } + + if (returnSize > 0) + { + if (returnType.getSort() == Type.OBJECT) + { + method.instructions.add(new InsnNode(Opcodes.ACONST_NULL)); + } + else if (returnSize == 1) + { + method.instructions.add(new InsnNode(Opcodes.ICONST_0)); + } + else if (returnSize == 2) + { + method.instructions.add(new InsnNode(Opcodes.DCONST_0)); + } + } + + method.instructions.add(new InsnNode(returnType.getOpcode(Opcodes.IRETURN))); + + int argsSize = ByteCodeUtilities.getArgsSize(args); + method.maxLocals = argsSize + 1; + method.maxStack = argsSize + 1; + } + + /** + * @param classNode + * @param method + * @param args + */ + private void populateBooleaninvocationChain(ClassNode classNode, MethodNode method, Type[] args) + { + boolean isOrOperation = this.logicOp.isOr(); + boolean breakOnMatch = this.logicOp.breakOnMatch(); + int initialValue = isOrOperation && (!this.logicOp.assumeTrue() || this.size > 0) ? Opcodes.ICONST_0 : Opcodes.ICONST_1; + int localIndex = ByteCodeUtilities.getArgsSize(args) + 1; + + method.instructions.add(new InsnNode(initialValue)); + method.instructions.add(new VarInsnNode(Opcodes.ISTORE, localIndex)); + + for (int handlerIndex = 0; handlerIndex < this.size; handlerIndex++) + { + this.invokeHandler(handlerIndex, classNode, method, args); // invoke the method, this will leave the return value on the stack + + int jumpCondition = isOrOperation ? Opcodes.IFEQ : Opcodes.IFNE; // jump if zero for OR, jump if one for AND + int semaphore = isOrOperation ? Opcodes.ICONST_1 : Opcodes.ICONST_0; // will push TRUE for OR, will push FALSE for AND + + LabelNode lbl = new LabelNode(); + method.instructions.add(new JumpInsnNode(jumpCondition, lbl)); // jump over the set/return based on the condition + method.instructions.add(new InsnNode(semaphore)); // push TRUE or FALSE onto the stack + // set local or return + method.instructions.add(breakOnMatch ? new InsnNode(Opcodes.IRETURN) : new VarInsnNode(Opcodes.ISTORE, localIndex)); + method.instructions.add(lbl); // jump here + } + + method.instructions.add(new VarInsnNode(Opcodes.ILOAD, localIndex)); + method.instructions.add(new InsnNode(Opcodes.IRETURN)); + + method.maxLocals = localIndex + 2; + method.maxStack = localIndex + 1; + } + + /** + * @param handlerIndex + * @param classNode + * @param method + * @param args + */ + private void invokeHandler(int handlerIndex, ClassNode classNode, MethodNode method, Type[] args) + { + LabelNode lineNumberLabel = new LabelNode(new Label()); + method.instructions.add(lineNumberLabel); + method.instructions.add(new LineNumberNode(100 + handlerIndex, lineNumberLabel)); + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + method.instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, HandlerListClassLoader.HANDLER_VAR_PREFIX + handlerIndex, + "L" + this.typeRef + ";")); + + if (this.decorator != null) + { + this.decorator.preInvokeInterfaceMethod(handlerIndex, classNode, method, args); + } + + this.invokeInterfaceMethod(method, args); + + if (this.decorator != null) + { + this.decorator.postInvokeInterfaceMethod(handlerIndex, classNode, method, args); + } + } + + /** + * Inject instructions into the supplied method to invoke the same + * method on the supplied interface. + * + * @param method + * @param args + */ + private void invokeInterfaceMethod(MethodNode method, Type[] args) + { + int argNumber = 1; + for (Type type : args) + { + method.instructions.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), argNumber)); + argNumber += type.getSize(); + } + + method.instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, this.typeRef, method.name, method.desc, true)); + } + + /** + * @param baseName + * @param typeName + */ + private static String getNextClassName(String baseName, String typeName) + { + return String.format("%s$%s%d", baseName, typeName, HandlerListClassLoader.handlerIndex++); + } + + /** + * @param name + * @throws IOException + */ + private static byte[] getInterfaceBytes(String name) throws IOException + { + byte[] bytes = Launch.classLoader.getClassBytes(name); + + for (final IClassTransformer transformer : Launch.classLoader.getTransformers()) + { + bytes = transformer.transform(name, name, bytes); + } + + return bytes; + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/event/IHandlerListDecorator.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/IHandlerListDecorator.java new file mode 100644 index 00000000..7dc184eb --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/IHandlerListDecorator.java @@ -0,0 +1,63 @@ +package com.mumfrey.liteloader.core.event; + +import java.util.List; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +import com.mumfrey.liteloader.core.event.HandlerList.BakedHandlerList; +import com.mumfrey.liteloader.core.runtime.Obf; + +/** + * Essentially a "mini plugin" for HandlerListClassLoader which allows + * alterations of the generated bytecode. + * + * @author Adam Mummery-Smith + * + * @param + */ +public interface IHandlerListDecorator +{ + /** + * Get the template class name + */ + public abstract Obf getTemplate(); + + /** + * Prepare the decorator to accept the specified list of invokees + */ + public abstract void prepare(List sortedList); + + /** + * Create an instance of the handler class + */ + public abstract BakedHandlerList createInstance(Class> handlerClass) throws Exception; + + /** + * Called when populating the classNode + */ + public abstract void populateClass(String name, ClassNode classNode); + + /** + * Called when processing the ctor + */ + public abstract void processCtor(ClassNode classNode, MethodNode method); + + /** + * Called immediately before the interface method invocation bytecode is + * injected. + */ + public abstract void preInvokeInterfaceMethod(int handlerIndex, ClassNode classNode, MethodNode method, Type[] args); + + /** + * Called immediately after the interface method invocation bytecode is + * injected. + */ + public abstract void postInvokeInterfaceMethod(int handlerIndex, ClassNode classNode, MethodNode method, Type[] args); + + /** + * Called at the end of populateInterfaceMethod + */ + public abstract void populateInterfaceMethod(ClassNode classNode, MethodNode method); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/event/ProfilingHandlerList.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/ProfilingHandlerList.java new file mode 100644 index 00000000..c8fbd451 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/event/ProfilingHandlerList.java @@ -0,0 +1,237 @@ +package com.mumfrey.liteloader.core.event; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.profiler.Profiler; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +import com.mumfrey.liteloader.api.Listener; +import com.mumfrey.liteloader.core.runtime.Obf; + +/** + * A HandlerList which calls Profiler.beginSection and Profiler.endSection + * before every invocation. + * + * @author Adam Mummery-Smith + * + * @param + */ +public class ProfilingHandlerList extends HandlerList +{ + private static final long serialVersionUID = 1L; + + /** + * Profiler to pass in to baked handler lists + */ + private final Profiler profiler; + + /** + * @param type + * @param profiler + */ + public ProfilingHandlerList(Class type, Profiler profiler) + { + super(type); + this.profiler = profiler; + } + + /** + * @param type + * @param logicOp + * @param profiler + */ + public ProfilingHandlerList(Class type, ReturnLogicOp logicOp, Profiler profiler) + { + super(type, logicOp); + this.profiler = profiler; + } + + /** + * @param type + * @param logicOp + * @param sorted + * @param profiler + */ + public ProfilingHandlerList(Class type, ReturnLogicOp logicOp, boolean sorted, Profiler profiler) + { + super(type, logicOp, sorted); + this.profiler = profiler; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.event.HandlerList#getDecorator() + */ + @Override + protected IHandlerListDecorator getDecorator() + { + return new ProfilingHandlerListDecorator(this.profiler); + } + + /** + * Decorator which adds the profiler section calls to the invocation lists + */ + static class ProfilingHandlerListDecorator implements IHandlerListDecorator + { + private final Profiler profiler; + + private final List names = new ArrayList();; + + protected ProfilingHandlerListDecorator(Profiler profiler) + { + this.profiler = profiler; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.event.IHandlerListDecorator + * #getTemplate() + */ + @Override + public Obf getTemplate() + { + return Obf.BakedProfilingHandlerList; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.event.IHandlerListDecorator + * #prepare(java.util.List) + */ + @Override + public void prepare(List sortedList) + { + this.names.clear(); + + for (Listener l : sortedList) + { + String name = l.getName(); + this.names.add(name != null ? name : l.getClass().getSimpleName()); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.event.IHandlerListDecorator + * #createInstance(java.lang.Class) + */ + @Override + public BakedHandlerList createInstance(Class> handlerClass) throws Exception + { + try + { + Constructor> ctor = handlerClass.getDeclaredConstructor(Profiler.class); + ctor.setAccessible(true); + return ctor.newInstance(this.profiler); + } + catch (Exception ex) + { + ex.printStackTrace(); + throw ex; + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.event.IHandlerListDecorator + * #populateClass(java.lang.String, + * org.objectweb.asm.tree.ClassNode) + */ + @Override + public void populateClass(String name, ClassNode classNode) + { + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.event.IHandlerListDecorator + * #processCtor(org.objectweb.asm.tree.ClassNode, + * org.objectweb.asm.tree.MethodNode) + */ + @Override + public void processCtor(ClassNode classNode, MethodNode method) + { + // Actually replace the ctor code because it's easier + method.instructions.clear(); + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); + method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Obf.BakedProfilingHandlerList.ref, Obf.constructor.name, + method.desc, false)); + method.instructions.add(new InsnNode(Opcodes.RETURN)); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.event.IHandlerListDecorator + * #preInvokeInterfaceMethod(int, org.objectweb.asm.tree.ClassNode, + * org.objectweb.asm.tree.MethodNode, org.objectweb.asm.Type[]) + */ + @Override + public void preInvokeInterfaceMethod(int handlerIndex, ClassNode classNode, MethodNode method, Type[] args) + { + // Call this.startSection + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + method.instructions.add(new LdcInsnNode(this.names.get(handlerIndex))); + method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, classNode.superName, "startSection", "(Ljava/lang/String;)V", false)); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.event.IHandlerListDecorator + * #postInvokeInterfaceMethod(int, org.objectweb.asm.tree.ClassNode, + * org.objectweb.asm.tree.MethodNode, org.objectweb.asm.Type[]) + */ + @Override + public void postInvokeInterfaceMethod(int handlerIndex, ClassNode classNode, MethodNode method, Type[] args) + { + // Call this.endSection + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, classNode.superName, "endSection", "()V", false)); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.event.IHandlerListDecorator + * #populateInterfaceMethod(org.objectweb.asm.tree.ClassNode, + * org.objectweb.asm.tree.MethodNode) + */ + @Override + public void populateInterfaceMethod(ClassNode classNode, MethodNode method) + { + } + } + + /** + * Template class for the profiling handler lists + * + * @author Adam Mummery-Smith + * + * @param + */ + public abstract static class BakedList extends HandlerList.BakedHandlerList + { + private final Profiler profiler; + + public BakedList(Profiler profiler) + { + this.profiler = profiler; + } + + @Override + public abstract T get(); + + @Override + public abstract BakedHandlerList populate(List listeners); + + protected void startSection(String name) + { + this.profiler.startSection(name); + } + + protected void endSection() + { + this.profiler.endSection(); + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/OutdatedLoaderException.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/OutdatedLoaderException.java new file mode 100644 index 00000000..fe15f1df --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/OutdatedLoaderException.java @@ -0,0 +1,21 @@ +package com.mumfrey.liteloader.core.exceptions; + +/** + * Exception thrown when a mod class references a liteloader interface which + * does not exist, which more than likely means that it requires a more + * up-to-date version of the loader than is currently installed. + * + * @author Adam Mummery-Smith + */ +public class OutdatedLoaderException extends Exception +{ + private static final long serialVersionUID = 8770358290208830747L; + + /** + * @param missingAPI Name of the referenced class which is missing + */ + public OutdatedLoaderException(String missingAPI) + { + super(missingAPI); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/ProfilerCrossThreadAccessException.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/ProfilerCrossThreadAccessException.java new file mode 100644 index 00000000..a1ec1896 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/ProfilerCrossThreadAccessException.java @@ -0,0 +1,18 @@ +package com.mumfrey.liteloader.core.exceptions; + +/** + * Exception to throw if startSection or endSection are called from a thread + * other than the Minecraft main thread. This should NEVER happen and is an + * attempt to identify the culprit of some profiler stack corruption causes. + * + * @author Adam Mummery-Smith + */ +public class ProfilerCrossThreadAccessException extends RuntimeException +{ + private static final long serialVersionUID = 3225047722943528251L; + + public ProfilerCrossThreadAccessException(String message) + { + super("Calling thread name \"" + message + "\""); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/ProfilerStackCorruptionException.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/ProfilerStackCorruptionException.java new file mode 100644 index 00000000..2cda09a2 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/ProfilerStackCorruptionException.java @@ -0,0 +1,17 @@ +package com.mumfrey.liteloader.core.exceptions; + +/** + * Exception to throw when a mod corrupts the profiler stack, this avoids + * throwing a (somewhat cryptic) NoSuchElementException inside HookProfiler + * + * @author Adam Mummery-Smith + */ +public class ProfilerStackCorruptionException extends RuntimeException +{ + private static final long serialVersionUID = -7745831270297368169L; + + public ProfilerStackCorruptionException(String message) + { + super(message); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/UnregisteredChannelException.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/UnregisteredChannelException.java new file mode 100644 index 00000000..6b4cd82b --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/exceptions/UnregisteredChannelException.java @@ -0,0 +1,11 @@ +package com.mumfrey.liteloader.core.exceptions; + +public class UnregisteredChannelException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public UnregisteredChannelException(String message) + { + super(message); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Methods.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Methods.java new file mode 100644 index 00000000..1dc81595 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Methods.java @@ -0,0 +1,91 @@ +package com.mumfrey.liteloader.core.runtime; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import com.mumfrey.liteloader.transformers.event.MethodInfo; + +/** + * + * @author Adam Mummery-Smith + */ +public abstract class Methods +{ + // CHECKSTYLE:OFF + + // Client & General + @Deprecated public static final MethodInfo startGame = new MethodInfo(Obf.Minecraft, Obf.startGame, Void.TYPE); + @Deprecated public static final MethodInfo runGameLoop = new MethodInfo(Obf.Minecraft, Obf.runGameLoop, Void.TYPE); + @Deprecated public static final MethodInfo runTick = new MethodInfo(Obf.Minecraft, Obf.runTick, Void.TYPE); + @Deprecated public static final MethodInfo updateFramebufferSize = new MethodInfo(Obf.Minecraft, Obf.updateFramebufferSize, Void.TYPE); + @Deprecated public static final MethodInfo framebufferRender = new MethodInfo(Obf.FrameBuffer, Obf.framebufferRender, Void.TYPE, Integer.TYPE, Integer.TYPE); + @Deprecated public static final MethodInfo framebufferRenderExt = new MethodInfo(Obf.FrameBuffer, Obf.framebufferRenderExt, Void.TYPE, Integer.TYPE, Integer.TYPE, Boolean.TYPE); + @Deprecated public static final MethodInfo bindFramebufferTexture = new MethodInfo(Obf.FrameBuffer, Obf.bindFramebufferTexture, Void.TYPE); + @Deprecated public static final MethodInfo sendChatMessage = new MethodInfo(Obf.EntityPlayerSP, Obf.sendChatMessage, Void.TYPE, String.class); + @Deprecated public static final MethodInfo renderWorld = new MethodInfo(Obf.EntityRenderer, Obf.renderWorld, Void.TYPE, Float.TYPE, Long.TYPE); + @Deprecated public static final MethodInfo renderWorldPass = new MethodInfo(Obf.EntityRenderer, Obf.renderWorldPass, Void.TYPE, Integer.TYPE, Float.TYPE, Long.TYPE); + @Deprecated public static final MethodInfo updateCameraAndRender = new MethodInfo(Obf.EntityRenderer, Obf.updateCameraAndRender, Void.TYPE, Float.TYPE); + @Deprecated public static final MethodInfo renderGameOverlay = new MethodInfo(Obf.GuiIngame, Obf.renderGameOverlay, Void.TYPE, Float.TYPE); + @Deprecated public static final MethodInfo drawChat = new MethodInfo(Obf.GuiNewChat, Obf.drawChat, Void.TYPE, Integer.TYPE); + @Deprecated public static final MethodInfo integratedServerCtor = new MethodInfo(Obf.IntegratedServer, Obf.constructor, Void.TYPE, Obf.Minecraft, String.class, String.class, Obf.WorldSettings); + @Deprecated public static final MethodInfo initPlayerConnection = new MethodInfo(Obf.ServerConfigurationManager, Obf.initializeConnectionToPlayer, Void.TYPE, Obf.NetworkManager, Obf.EntityPlayerMP); + @Deprecated public static final MethodInfo playerLoggedIn = new MethodInfo(Obf.ServerConfigurationManager, Obf.playerLoggedIn, Void.TYPE, Obf.EntityPlayerMP); + @Deprecated public static final MethodInfo playerLoggedOut = new MethodInfo(Obf.ServerConfigurationManager, Obf.playerLoggedOut, Void.TYPE, Obf.EntityPlayerMP); + @Deprecated public static final MethodInfo spawnPlayer = new MethodInfo(Obf.ServerConfigurationManager, Obf.spawnPlayer, Obf.EntityPlayerMP, Obf.GameProfile); + @Deprecated public static final MethodInfo respawnPlayer = new MethodInfo(Obf.ServerConfigurationManager, Obf.respawnPlayer, Obf.EntityPlayerMP, Obf.EntityPlayerMP, Integer.TYPE, Boolean.TYPE); + @Deprecated public static final MethodInfo glClear = new MethodInfo(Obf.GlStateManager, Obf.clear, Void.TYPE, Integer.TYPE); + @Deprecated public static final MethodInfo getProfile = new MethodInfo(Obf.Session, Obf.getProfile, Obf.GameProfile); + @Deprecated public static final MethodInfo saveScreenshot = new MethodInfo(Obf.ScreenShotHelper, Obf.saveScreenshot, Obf.IChatComponent, File.class, String.class, Integer.TYPE, Integer.TYPE, Obf.FrameBuffer); + @Deprecated public static final MethodInfo isFramebufferEnabled = new MethodInfo(Obf.OpenGlHelper, Obf.isFramebufferEnabled, Boolean.TYPE); + @Deprecated public static final MethodInfo doRenderEntity = new MethodInfo(Obf.RenderManager, Obf.doRenderEntity, Boolean.TYPE, Obf.Entity, Double.TYPE, Double.TYPE, Double.TYPE, Float.TYPE, Float.TYPE, Boolean.TYPE); + @Deprecated public static final MethodInfo doRender = new MethodInfo(Obf.Render, Obf.doRender, Void.TYPE, Obf.Entity, Double.TYPE, Double.TYPE, Double.TYPE, Float.TYPE, Float.TYPE); + @Deprecated public static final MethodInfo doRenderShadowAndFire = new MethodInfo(Obf.Render, Obf.doRenderShadowAndFire, Void.TYPE, Obf.Entity, Double.TYPE, Double.TYPE, Double.TYPE, Float.TYPE, Float.TYPE); + @Deprecated public static final MethodInfo realmsPlay = new MethodInfo(Obf.RealmsMainScreen, "play", Void.TYPE, Long.TYPE); + @Deprecated public static final MethodInfo realmsStopFetcher = new MethodInfo(Obf.RealmsMainScreen, "stopRealmsFetcherAndPinger", Void.TYPE); + @Deprecated public static final MethodInfo onBlockClicked = new MethodInfo(Obf.ItemInWorldManager, Obf.onBlockClicked, Void.TYPE, Obf.BlockPos, Obf.EnumFacing); + @Deprecated public static final MethodInfo activateBlockOrUseItem = new MethodInfo(Obf.ItemInWorldManager, Obf.activateBlockOrUseItem, Boolean.TYPE, Obf.EntityPlayer, Obf.World, Obf.ItemStack, Obf.BlockPos, Obf.EnumFacing, Float.TYPE, Float.TYPE, Float.TYPE); + @Deprecated public static final MethodInfo processBlockPlacement = new MethodInfo(Obf.NetHandlerPlayServer, Obf.processPlayerBlockPlacement, Void.TYPE, Packets.C08PacketPlayerBlockPlacement); + @Deprecated public static final MethodInfo handleAnimation = new MethodInfo(Obf.NetHandlerPlayServer, Obf.handleAnimation, Void.TYPE, Packets.C0APacketAnimation); + @Deprecated public static final MethodInfo processPlayerDigging = new MethodInfo(Obf.NetHandlerPlayServer, Obf.processPlayerDigging, Void.TYPE, Packets.C07PacketPlayerDigging); + @Deprecated public static final MethodInfo serverJobs = new MethodInfo(Obf.MinecraftServer, Obf.updateTimeLightAndEntities, Void.TYPE); + @Deprecated public static final MethodInfo checkThreadAndEnqueue = new MethodInfo(Obf.PacketThreadUtil, Obf.checkThreadAndEnqueue); + @Deprecated public static final MethodInfo processPlayer = new MethodInfo(Obf.NetHandlerPlayServer, Obf.processPlayer, Void.TYPE, Packets.C03PacketPlayer); + @Deprecated public static final MethodInfo renderSky = new MethodInfo(Obf.RenderGlobal, Obf.renderSky, Void.TYPE, Float.TYPE, Integer.TYPE); + @Deprecated public static final MethodInfo renderCloudsCheck = new MethodInfo(Obf.EntityRenderer, Obf.renderCloudsCheck, Void.TYPE, Obf.RenderGlobal, Float.TYPE, Integer.TYPE); + @Deprecated public static final MethodInfo setupFog = new MethodInfo(Obf.EntityRenderer, Obf.setupFog, Void.TYPE, Integer.TYPE, Float.TYPE); + + // Profiler + @Deprecated public static final MethodInfo startSection = new MethodInfo(Obf.Profiler, Obf.startSection, Void.TYPE, String.class); + @Deprecated public static final MethodInfo endSection = new MethodInfo(Obf.Profiler, Obf.endSection, Void.TYPE); + @Deprecated public static final MethodInfo endStartSection = new MethodInfo(Obf.Profiler, Obf.endStartSection, Void.TYPE, String.class); + + // Dedicated Server + @Deprecated public static final MethodInfo startServer = new MethodInfo(Obf.DedicatedServer, Obf.startServer, Boolean.TYPE); + @Deprecated public static final MethodInfo startServerThread = new MethodInfo(Obf.MinecraftServer, Obf.startServerThread, Void.TYPE); + + private Methods() {} + + private static final Map methodMap = new HashMap(); + + static + { + try + { + for (Field fd : Methods.class.getFields()) + { + if (fd.getType().equals(MethodInfo.class)) + { + Methods.methodMap.put(fd.getName(), (MethodInfo)fd.get(null)); + } + } + } + catch (IllegalAccessException ex) {} + } + + public static MethodInfo getByName(String name) + { + return Methods.methodMap.get(name); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Obf.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Obf.java new file mode 100644 index 00000000..07cb0a1d --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Obf.java @@ -0,0 +1,432 @@ +package com.mumfrey.liteloader.core.runtime; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Centralised obfuscation table for LiteLoader + * + * @author Adam Mummery-Smith + * TODO Obfuscation 1.8 + */ +public class Obf +{ + // Non-obfuscated references, here for convenience + // ----------------------------------------------------------------------------------------- + public static final Obf EventProxy = new Obf("com.mumfrey.liteloader.core.event.EventProxy" ); + public static final Obf HandlerList = new Obf("com.mumfrey.liteloader.core.event.HandlerList" ); + public static final Obf BakedHandlerList = new Obf("com.mumfrey.liteloader.core.event.HandlerList$BakedHandlerList" ); + public static final Obf BakedProfilingHandlerList = new Obf("com.mumfrey.liteloader.core.event.ProfilingHandlerList$BakedList" ); + public static final Obf PacketEvents = new Obf("com.mumfrey.liteloader.core.PacketEvents" ); + public static final Obf PacketEventsClient = new Obf("com.mumfrey.liteloader.client.PacketEventsClient" ); + public static final Obf LoadingBar = new Obf("com.mumfrey.liteloader.client.gui.startup.LoadingBar" ); + public static final Obf GameProfile = new Obf("com.mojang.authlib.GameProfile" ); + public static final Obf MinecraftMain = new Obf("net.minecraft.client.main.Main" ); + public static final Obf MinecraftServer = new Obf("net.minecraft.server.MinecraftServer" ); + public static final Obf GL11 = new Obf("org.lwjgl.opengl.GL11" ); + public static final Obf RealmsMainScreen = new Obf("com.mojang.realmsclient.RealmsMainScreen" ); + public static final Obf init = new Obf("init" ); + public static final Obf postInit = new Obf("postInit" ); + public static final Obf constructor = new Obf("" ); + + // Overlays and Accessor Interfaces + // ----------------------------------------------------------------------------------------- + public static final Obf IMinecraft = new Obf("com.mumfrey.liteloader.client.overlays.IMinecraft" ); + public static final Obf IGuiTextField = new Obf("com.mumfrey.liteloader.client.overlays.IGuiTextField" ); + public static final Obf IEntityRenderer = new Obf("com.mumfrey.liteloader.client.overlays.IEntityRenderer" ); + public static final Obf ISoundHandler = new Obf("com.mumfrey.liteloader.client.overlays.ISoundHandler" ); + + // CHECKSTYLE:OFF + + // Classes + // ----------------------------------------------------------------------------------------- + public static final Obf Minecraft = new Obf("net.minecraft.client.Minecraft", "bsu" ); + public static final Obf EntityRenderer = new Obf("net.minecraft.client.renderer.EntityRenderer", "cji" ); + public static final Obf Blocks = new Obf("net.minecraft.init.Blocks", "aty" ); + public static final Obf CrashReport$6 = new Obf("net.minecraft.crash.CrashReport$6", "h" ); + public static final Obf INetHandler = new Obf("net.minecraft.network.INetHandler", "hg" ); + public static final Obf Items = new Obf("net.minecraft.init.Items", "amk" ); + + // Fields + // ----------------------------------------------------------------------------------------- + public static final Obf tileEntityNameToClassMap = new Obf("field_145855_i", "f" ); + public static final Obf tileEntityClassToNameMap = new Obf("field_145853_j", "g" ); + + // Methods + // ----------------------------------------------------------------------------------------- + public static final Obf startGame = new Obf("func_71384_a", "aj" ); + public static final Obf startSection = new Obf("func_76320_a", "a" ); + public static final Obf endSection = new Obf("func_76319_b", "b" ); + public static final Obf endStartSection = new Obf("func_76318_c", "c" ); + public static final Obf processPacket = new Obf("func_148833_a", "a" ); + + // Legacy + // ----------------------------------------------------------------------------------------- + @Deprecated public static final Obf GuiIngame = new Obf("net.minecraft.client.gui.GuiIngame", "btz" ); + @Deprecated public static final Obf Profiler = new Obf("net.minecraft.profiler.Profiler", "uw" ); + @Deprecated public static final Obf IntegratedServer = new Obf("net.minecraft.server.integrated.IntegratedServer", "cyk" ); + @Deprecated public static final Obf WorldSettings = new Obf("net.minecraft.world.WorldSettings", "arb" ); + @Deprecated public static final Obf ServerConfigurationManager = new Obf("net.minecraft.server.management.ServerConfigurationManager", "sn" ); + @Deprecated public static final Obf EntityPlayerMP = new Obf("net.minecraft.entity.player.EntityPlayerMP", "qw" ); + @Deprecated public static final Obf NetworkManager = new Obf("net.minecraft.network.NetworkManager", "gr" ); + @Deprecated public static final Obf DedicatedServer = new Obf("net.minecraft.server.dedicated.DedicatedServer", "po" ); + @Deprecated public static final Obf EntityPlayerSP = new Obf("net.minecraft.client.entity.EntityPlayerSP", "cio" ); + @Deprecated public static final Obf FrameBuffer = new Obf("net.minecraft.client.shader.Framebuffer", "ckw" ); + @Deprecated public static final Obf GuiNewChat = new Obf("net.minecraft.client.gui.GuiNewChat", "buh" ); + @Deprecated public static final Obf GlStateManager = new Obf("net.minecraft.client.renderer.GlStateManager", "cjm" ); + @Deprecated public static final Obf Session = new Obf("net.minecraft.util.Session", "btw" ); + @Deprecated public static final Obf IChatComponent = new Obf("net.minecraft.util.IChatComponent", "ho" ); + @Deprecated public static final Obf ScreenShotHelper = new Obf("net.minecraft.util.ScreenShotHelper", "btt" ); + @Deprecated public static final Obf OpenGlHelper = new Obf("net.minecraft.client.renderer.OpenGlHelper", "dax" ); + @Deprecated public static final Obf Entity = new Obf("net.minecraft.entity.Entity", "wv" ); + @Deprecated public static final Obf RenderManager = new Obf("net.minecraft.client.renderer.entity.RenderManager", "cpt" ); + @Deprecated public static final Obf Render = new Obf("net.minecraft.client.renderer.entity.Render", "cpu" ); + @Deprecated public static final Obf GuiTextField = new Obf("net.minecraft.client.gui.GuiTextField", "bul" ); + @Deprecated public static final Obf SoundHandler = new Obf("net.minecraft.client.audio.SoundHandler", "czh" ); + @Deprecated public static final Obf BlockPos = new Obf("net.minecraft.util.BlockPos", "dt" ); + @Deprecated public static final Obf EnumFacing = new Obf("net.minecraft.util.EnumFacing", "ej" ); + @Deprecated public static final Obf ItemInWorldManager = new Obf("net.minecraft.server.management.ItemInWorldManager", "qx" ); + @Deprecated public static final Obf NetHandlerPlayServer = new Obf("net.minecraft.network.NetHandlerPlayServer", "rj" ); + @Deprecated public static final Obf EntityPlayer = new Obf("net.minecraft.entity.player.EntityPlayer", "ahd" ); + @Deprecated public static final Obf World = new Obf("net.minecraft.world.World", "aqu" ); + @Deprecated public static final Obf ItemStack = new Obf("net.minecraft.item.ItemStack", "amj" ); + @Deprecated public static final Obf PacketThreadUtil = new Obf("net.minecraft.network.PacketThreadUtil", "ig" ); + @Deprecated public static final Obf RenderGlobal = new Obf("net.minecraft.client.renderer.RenderGlobal", "ckn" ); + @Deprecated public static final Obf minecraftProfiler = new Obf("field_71424_I", "y" ); + @Deprecated public static final Obf entityRenderMap = new Obf("field_78729_o", "k" ); + @Deprecated public static final Obf reloadListeners = new Obf("field_110546_b", "d" ); + @Deprecated public static final Obf networkManager = new Obf("field_147393_d", "d" ); + @Deprecated public static final Obf registryObjects = new Obf("field_82596_a", "c" ); + @Deprecated public static final Obf underlyingIntegerMap = new Obf("field_148759_a", "a" ); + @Deprecated public static final Obf identityMap = new Obf("field_148749_a", "a" ); + @Deprecated public static final Obf objectList = new Obf("field_148748_b", "b" ); + @Deprecated public static final Obf mapSpecialRenderers = new Obf("field_147559_m", "m" ); + @Deprecated public static final Obf timer = new Obf("field_71428_T", "U" ); + @Deprecated public static final Obf mcProfiler = new Obf("field_71424_I", "y" ); + @Deprecated public static final Obf running = new Obf("field_71425_J", "z" ); + @Deprecated public static final Obf defaultResourcePacks = new Obf("field_110449_ao", "aw" ); + @Deprecated public static final Obf serverName = new Obf("field_71475_ae", "am" ); + @Deprecated public static final Obf serverPort = new Obf("field_71477_af", "an" ); + @Deprecated public static final Obf shaderResourceLocations = new Obf("field_147712_ad", "ab" ); + @Deprecated public static final Obf shaderIndex = new Obf("field_147713_ae", "ac" ); + @Deprecated public static final Obf useShader = new Obf("field_175083_ad", "ad" ); + @Deprecated public static final Obf viewDistance = new Obf("field_149528_b", "b" ); + @Deprecated public static final Obf entityPosY = new Obf("field_70163_u", "t" ); + @Deprecated public static final Obf chatComponent = new Obf("field_148919_a", "a" ); + @Deprecated public static final Obf runGameLoop = new Obf("func_71411_J", "as" ); + @Deprecated public static final Obf runTick = new Obf("func_71407_l", "r" ); + @Deprecated public static final Obf updateCameraAndRender = new Obf("func_78480_b", "b" ); + @Deprecated public static final Obf renderWorld = new Obf("func_78471_a", "a" ); + @Deprecated public static final Obf renderGameOverlay = new Obf("func_175180_a", "a" ); + @Deprecated public static final Obf spawnPlayer = new Obf("func_148545_a", "f" ); + @Deprecated public static final Obf respawnPlayer = new Obf("func_72368_a", "a" ); + @Deprecated public static final Obf initializeConnectionToPlayer = new Obf("func_72355_a", "a" ); + @Deprecated public static final Obf playerLoggedIn = new Obf("func_72377_c", "c" ); + @Deprecated public static final Obf playerLoggedOut = new Obf("func_72367_e", "e" ); + @Deprecated public static final Obf startServer = new Obf("func_71197_b", "i" ); + @Deprecated public static final Obf startServerThread = new Obf("func_71256_s", "B" ); + @Deprecated public static final Obf sendChatMessage = new Obf("func_71165_d", "e" ); + @Deprecated public static final Obf updateFramebufferSize = new Obf("func_147119_ah", "av" ); + @Deprecated public static final Obf framebufferRender = new Obf("func_147615_c", "c" ); + @Deprecated public static final Obf framebufferRenderExt = new Obf("func_178038_a", "a" ); + @Deprecated public static final Obf bindFramebufferTexture = new Obf("func_147612_c", "c" ); + @Deprecated public static final Obf drawChat = new Obf("func_146230_a", "a" ); + @Deprecated public static final Obf clear = new Obf("func_179086_m", "m" ); + @Deprecated public static final Obf renderWorldPass = new Obf("func_175068_a", "a" ); + @Deprecated public static final Obf getProfile = new Obf("func_148256_e", "a" ); + @Deprecated public static final Obf saveScreenshot = new Obf("func_148260_a", "a" ); + @Deprecated public static final Obf isFramebufferEnabled = new Obf("func_148822_b", "i" ); + @Deprecated public static final Obf doRenderEntity = new Obf("func_147939_a", "a" ); + @Deprecated public static final Obf doRender = new Obf("func_76986_a", "a" ); + @Deprecated public static final Obf doRenderShadowAndFire = new Obf("func_76979_b", "b" ); + @Deprecated public static final Obf resize = new Obf("func_71370_a", "a" ); + @Deprecated public static final Obf loadShader = new Obf("func_175069_a", "a" ); + @Deprecated public static final Obf getFOVModifier = new Obf("func_78481_a", "a" ); + @Deprecated public static final Obf setupCameraTransform = new Obf("func_78479_a", "a" ); + @Deprecated public static final Obf loadSoundResource = new Obf("func_147693_a", "a" ); + @Deprecated public static final Obf onBlockClicked = new Obf("func_180784_a", "a" ); + @Deprecated public static final Obf activateBlockOrUseItem = new Obf("func_180236_a", "a" ); + @Deprecated public static final Obf processPlayerBlockPlacement = new Obf("func_147346_a", "a" ); + @Deprecated public static final Obf handleAnimation = new Obf("func_175087_a", "a" ); + @Deprecated public static final Obf processPlayerDigging = new Obf("func_147345_a", "a" ); + @Deprecated public static final Obf updateTimeLightAndEntities = new Obf("func_71190_q", "z" ); + @Deprecated public static final Obf checkThreadAndEnqueue = new Obf("func_180031_a", "a" ); + @Deprecated public static final Obf processPlayer = new Obf("func_147347_a", "a" ); + @Deprecated public static final Obf renderSky = new Obf("func_174976_a", "a" ); + @Deprecated public static final Obf renderCloudsCheck = new Obf("func_180437_a", "a" ); + @Deprecated public static final Obf setupFog = new Obf("func_78468_a", "a" ); + + // CHECKSTYLE:ON + + public static final int MCP = 0; + public static final int SRG = 1; + public static final int OBF = 2; + + private static Properties mcpNames; + + private static final Map obfs = new HashMap(); + + static + { + try + { + for (Field fd : Obf.class.getFields()) + { + if (fd.getType().equals(Obf.class)) + { + Obf.obfs.put(fd.getName(), (Obf)fd.get(null)); + } + } + } + catch (IllegalAccessException ex) {} + } + + /** + * Array of names, indexed by MCP, SRG, OBF constants + */ + public final String[] names; + + /** + * Class, field or method name in unobfuscated (MCP) format + */ + public final String name; + + /** + * Class name in bytecode notation with slashes instead of dots + */ + public final String ref; + + /** + * Class, field or method name in searge format + */ + public final String srg; + + /** + * Class, field or method name in obfuscated (original) format + */ + public final String obf; + + /** + * @param mcpName + */ + protected Obf(String mcpName) + { + this(mcpName, mcpName, mcpName); + } + + /** + * @param seargeName + * @param obfName + */ + protected Obf(String seargeName, String obfName) + { + this(seargeName, obfName, null); + } + + /** + * @param seargeName + * @param obfName + * @param mcpName + */ + protected Obf(String seargeName, String obfName, String mcpName) + { + this.name = mcpName != null ? mcpName : this.getDeobfuscatedName(seargeName); + this.ref = this.name.replace('.', '/'); + this.srg = seargeName; + this.obf = obfName; + + this.names = new String[] { this.name, this.srg, this.obf }; + } + + /** + * @param type + */ + public String getDescriptor(int type) + { + return String.format("L%s;", this.names[type].replace('.', '/')); + } + + /** + * Test whether any of this Obf's dimensions match the supplied name + * + * @param name + */ + public boolean matches(String name) + { + return this.obf.equals(name) || this.srg.equals(name)|| this.name.equals(name); + } + + /** + * Test whether any of this Obf's dimensions match the supplied name or + * ordinal + * + * @param name + * @param ordinal + */ + public boolean matches(String name, int ordinal) + { + if (this.isOrdinal() && ordinal > -1) + { + return this.getOrdinal() == ordinal; + } + + return this.matches(name); + } + + /** + * Returns true if this is an ordinal pointer + */ + public boolean isOrdinal() + { + return false; + } + + /** + * Get the ordinal for this entry + */ + public int getOrdinal() + { + return -1; + } + + @Override + public String toString() + { + return String.format("%s[%s,%s,%s]@%d", this.getClass().getSimpleName(), this.name, this.srg, this.obf, this.getOrdinal()); + } + + /** + * @param seargeName + */ + protected String getDeobfuscatedName(String seargeName) + { + return Obf.getDeobfName(seargeName); + } + + /** + * @param seargeName + */ + static String getDeobfName(String seargeName) + { + if (Obf.mcpNames == null) + { + Obf.mcpNames = new Properties(); + InputStream is = Obf.class.getResourceAsStream("/obfuscation.properties"); + if (is != null) + { + try + { + Obf.mcpNames.load(is); + } + catch (IOException ex) {} + + try + { + is.close(); + } + catch (IOException ex) {} + } + } + + return Obf.mcpNames.getProperty(seargeName, seargeName); + } + + /** + * @param name + */ + public static Obf getByName(String name) + { + return Obf.obfs.get(name); + } + + public static Obf getByName(Class obf, String name) + { + try + { + for (Field fd : obf.getFields()) + { + if (Obf.class.isAssignableFrom(fd.getType())) + { + String fieldName = fd.getName(); + Obf entry = (Obf)fd.get(null); + if (name.equals(fieldName) || name.equals(entry.name)) + { + return entry; + } + } + } + } + catch (Exception ex) {} + + return Obf.getByName(name); + } + + public static String lookupMCPName(String obfName) + { + for (Obf obf : Obf.obfs.values()) + { + if (obfName.equals(obf.obf)) + { + return obf.name; + } + } + + return obfName; + } + + /** + * Ordinal reference, can be passed to some methods which accept an + * {@link Obf} to indicate an offset into a class rather than a named + * reference. + * + * @author Adam Mummery-Smith + */ + public static class Ord extends Obf + { + /** + * Field/method offset + */ + private final int ordinal; + + /** + * @param name Field/method name + * @param ordinal Field/method ordinal + */ + public Ord(String name, int ordinal) + { + super(name); + this.ordinal = ordinal; + } + + /** + * @param ordinal Field ordinal + */ + public Ord(int ordinal) + { + super("ord#" + ordinal); + this.ordinal = ordinal; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.runtime.Obf#isOrdinal() + */ + @Override + public boolean isOrdinal() + { + return true; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.runtime.Obf#getOrdinal() + */ + @Override + public int getOrdinal() + { + return this.ordinal; + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Packets.java b/liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Packets.java new file mode 100644 index 00000000..48765098 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/core/runtime/Packets.java @@ -0,0 +1,321 @@ +package com.mumfrey.liteloader.core.runtime; + +import java.util.HashMap; +import java.util.Map; + +/** + * Packet obfuscation table + * + * @author Adam Mummery-Smith + * TODO Obfuscation 1.8 + */ +public final class Packets extends Obf +{ + /** + * Since we need to catch and deal with the fact that a packet is first + * marshalled across threads via PacketThreadUtil, we will need to know + * which owner object to check against the current thread in order to detect + * when the packet instance is being processed by the main message loop. The + * Context object describes in which context (client or server) that a + * particular packet will be processed in on the receiving end, and + * thus which object to check threading against. + * + * @author Adam Mummery-Smith + */ + public enum Context + { + CLIENT, + SERVER + } + + // CHECKSTYLE:OFF + + private static Map packetMap = new HashMap(); + + public static Packets S08PacketPlayerPosLook = new Packets("net.minecraft.network.play.server.S08PacketPlayerPosLook", "ii", Context.CLIENT); + public static Packets S0EPacketSpawnObject = new Packets("net.minecraft.network.play.server.S0EPacketSpawnObject", "il", Context.CLIENT); + public static Packets S11PacketSpawnExperienceOrb = new Packets("net.minecraft.network.play.server.S11PacketSpawnExperienceOrb", "im", Context.CLIENT); + public static Packets S2CPacketSpawnGlobalEntity = new Packets("net.minecraft.network.play.server.S2CPacketSpawnGlobalEntity", "in", Context.CLIENT); + public static Packets S0FPacketSpawnMob = new Packets("net.minecraft.network.play.server.S0FPacketSpawnMob", "io", Context.CLIENT); + public static Packets S10PacketSpawnPainting = new Packets("net.minecraft.network.play.server.S10PacketSpawnPainting", "ip", Context.CLIENT); + public static Packets S0CPacketSpawnPlayer = new Packets("net.minecraft.network.play.server.S0CPacketSpawnPlayer", "iq", Context.CLIENT); + public static Packets S0BPacketAnimation = new Packets("net.minecraft.network.play.server.S0BPacketAnimation", "ir", Context.CLIENT); + public static Packets S37PacketStatistics = new Packets("net.minecraft.network.play.server.S37PacketStatistics", "is", Context.CLIENT); + public static Packets S25PacketBlockBreakAnim = new Packets("net.minecraft.network.play.server.S25PacketBlockBreakAnim", "it", Context.CLIENT); + public static Packets S35PacketUpdateTileEntity = new Packets("net.minecraft.network.play.server.S35PacketUpdateTileEntity", "iu", Context.CLIENT); + public static Packets S24PacketBlockAction = new Packets("net.minecraft.network.play.server.S24PacketBlockAction", "iv", Context.CLIENT); + public static Packets S23PacketBlockChange = new Packets("net.minecraft.network.play.server.S23PacketBlockChange", "iw", Context.CLIENT); + public static Packets S41PacketServerDifficulty = new Packets("net.minecraft.network.play.server.S41PacketServerDifficulty", "ix", Context.CLIENT); + public static Packets S3APacketTabComplete = new Packets("net.minecraft.network.play.server.S3APacketTabComplete", "iy", Context.CLIENT); + public static Packets S02PacketChat = new Packets("net.minecraft.network.play.server.S02PacketChat", "iz", Context.CLIENT); + public static Packets S22PacketMultiBlockChange = new Packets("net.minecraft.network.play.server.S22PacketMultiBlockChange", "ja", Context.CLIENT); + public static Packets S32PacketConfirmTransaction = new Packets("net.minecraft.network.play.server.S32PacketConfirmTransaction", "jc", Context.CLIENT); + public static Packets S2EPacketCloseWindow = new Packets("net.minecraft.network.play.server.S2EPacketCloseWindow", "jd", Context.CLIENT); + public static Packets S2DPacketOpenWindow = new Packets("net.minecraft.network.play.server.S2DPacketOpenWindow", "je", Context.CLIENT); + public static Packets S30PacketWindowItems = new Packets("net.minecraft.network.play.server.S30PacketWindowItems", "jf", Context.CLIENT); + public static Packets S31PacketWindowProperty = new Packets("net.minecraft.network.play.server.S31PacketWindowProperty", "jg", Context.CLIENT); + public static Packets S2FPacketSetSlot = new Packets("net.minecraft.network.play.server.S2FPacketSetSlot", "jh", Context.CLIENT); + public static Packets S3FPacketCustomPayload = new Packets("net.minecraft.network.play.server.S3FPacketCustomPayload", "ji", Context.CLIENT); + public static Packets S40PacketDisconnect = new Packets("net.minecraft.network.play.server.S40PacketDisconnect", "jj", Context.CLIENT); + public static Packets S19PacketEntityStatus = new Packets("net.minecraft.network.play.server.S19PacketEntityStatus", "jk", Context.CLIENT); + public static Packets S49PacketUpdateEntityNBT = new Packets("net.minecraft.network.play.server.S49PacketUpdateEntityNBT", "jl", Context.CLIENT); + public static Packets S27PacketExplosion = new Packets("net.minecraft.network.play.server.S27PacketExplosion", "jm", Context.CLIENT); + public static Packets S46PacketSetCompressionLevel = new Packets("net.minecraft.network.play.server.S46PacketSetCompressionLevel", "jn", Context.CLIENT); + public static Packets S2BPacketChangeGameState = new Packets("net.minecraft.network.play.server.S2BPacketChangeGameState", "jo", Context.CLIENT); + public static Packets S00PacketKeepAlive = new Packets("net.minecraft.network.play.server.S00PacketKeepAlive", "jp", Context.CLIENT); + public static Packets S21PacketChunkData = new Packets("net.minecraft.network.play.server.S21PacketChunkData", "jq", Context.CLIENT); + public static Packets S26PacketMapChunkBulk = new Packets("net.minecraft.network.play.server.S26PacketMapChunkBulk", "js", Context.CLIENT); + public static Packets S28PacketEffect = new Packets("net.minecraft.network.play.server.S28PacketEffect", "jt", Context.CLIENT); + public static Packets S2APacketParticles = new Packets("net.minecraft.network.play.server.S2APacketParticles", "ju", Context.CLIENT); + public static Packets S29PacketSoundEffect = new Packets("net.minecraft.network.play.server.S29PacketSoundEffect", "jv", Context.CLIENT); + public static Packets S01PacketJoinGame = new Packets("net.minecraft.network.play.server.S01PacketJoinGame", "jw", Context.CLIENT); + public static Packets S34PacketMaps = new Packets("net.minecraft.network.play.server.S34PacketMaps", "jx", Context.CLIENT); + public static Packets S14PacketEntity = new Packets("net.minecraft.network.play.server.S14PacketEntity", "jy", Context.CLIENT); + public static Packets S15PacketEntityRelMove = new Packets("net.minecraft.network.play.server.S14PacketEntity$S15PacketEntityRelMove", "jz", Context.CLIENT); + public static Packets S17PacketEntityLookMove = new Packets("net.minecraft.network.play.server.S14PacketEntity$S17PacketEntityLookMove", "ka", Context.CLIENT); + public static Packets S16PacketEntityLook = new Packets("net.minecraft.network.play.server.S14PacketEntity$S16PacketEntityLook", "kb", Context.CLIENT); + public static Packets S36PacketSignEditorOpen = new Packets("net.minecraft.network.play.server.S36PacketSignEditorOpen", "kc", Context.CLIENT); + public static Packets S39PacketPlayerAbilities = new Packets("net.minecraft.network.play.server.S39PacketPlayerAbilities", "kd", Context.CLIENT); + public static Packets S42PacketCombatEvent = new Packets("net.minecraft.network.play.server.S42PacketCombatEvent", "ke", Context.CLIENT); + public static Packets S38PacketPlayerListItem = new Packets("net.minecraft.network.play.server.S38PacketPlayerListItem", "kh", Context.CLIENT); + public static Packets S0APacketUseBed = new Packets("net.minecraft.network.play.server.S0APacketUseBed", "kl", Context.CLIENT); + public static Packets S13PacketDestroyEntities = new Packets("net.minecraft.network.play.server.S13PacketDestroyEntities", "km", Context.CLIENT); + public static Packets S1EPacketRemoveEntityEffect = new Packets("net.minecraft.network.play.server.S1EPacketRemoveEntityEffect", "kn", Context.CLIENT); + public static Packets S48PacketResourcePackSend = new Packets("net.minecraft.network.play.server.S48PacketResourcePackSend", "ko", Context.CLIENT); + public static Packets S07PacketRespawn = new Packets("net.minecraft.network.play.server.S07PacketRespawn", "kp", Context.CLIENT); + public static Packets S19PacketEntityHeadLook = new Packets("net.minecraft.network.play.server.S19PacketEntityHeadLook", "kq", Context.CLIENT); + public static Packets S44PacketWorldBorder = new Packets("net.minecraft.network.play.server.S44PacketWorldBorder", "kr", Context.CLIENT); + public static Packets S43PacketCamera = new Packets("net.minecraft.network.play.server.S43PacketCamera", "ku", Context.CLIENT); + public static Packets S09PacketHeldItemChange = new Packets("net.minecraft.network.play.server.S09PacketHeldItemChange", "kv", Context.CLIENT); + public static Packets S3DPacketDisplayScoreboard = new Packets("net.minecraft.network.play.server.S3DPacketDisplayScoreboard", "kw", Context.CLIENT); + public static Packets S1CPacketEntityMetadata = new Packets("net.minecraft.network.play.server.S1CPacketEntityMetadata", "kx", Context.CLIENT); + public static Packets S1BPacketEntityAttach = new Packets("net.minecraft.network.play.server.S1BPacketEntityAttach", "ky", Context.CLIENT); + public static Packets S12PacketEntityVelocity = new Packets("net.minecraft.network.play.server.S12PacketEntityVelocity", "kz", Context.CLIENT); + public static Packets S04PacketEntityEquipment = new Packets("net.minecraft.network.play.server.S04PacketEntityEquipment", "la", Context.CLIENT); + public static Packets S1FPacketSetExperience = new Packets("net.minecraft.network.play.server.S1FPacketSetExperience", "lb", Context.CLIENT); + public static Packets S06PacketUpdateHealth = new Packets("net.minecraft.network.play.server.S06PacketUpdateHealth", "lc", Context.CLIENT); + public static Packets S3BPacketScoreboardObjective = new Packets("net.minecraft.network.play.server.S3BPacketScoreboardObjective", "ld", Context.CLIENT); + public static Packets S3EPacketTeams = new Packets("net.minecraft.network.play.server.S3EPacketTeams", "le", Context.CLIENT); + public static Packets S3CPacketUpdateScore = new Packets("net.minecraft.network.play.server.S3CPacketUpdateScore", "lf", Context.CLIENT); + public static Packets S05PacketSpawnPosition = new Packets("net.minecraft.network.play.server.S05PacketSpawnPosition", "lh", Context.CLIENT); + public static Packets S03PacketTimeUpdate = new Packets("net.minecraft.network.play.server.S03PacketTimeUpdate", "li", Context.CLIENT); + public static Packets S45PacketTitle = new Packets("net.minecraft.network.play.server.S45PacketTitle", "lj", Context.CLIENT); + public static Packets S33PacketUpdateSign = new Packets("net.minecraft.network.play.server.S33PacketUpdateSign", "ll", Context.CLIENT); + public static Packets S47PacketPlayerListHeaderFooter = new Packets("net.minecraft.network.play.server.S47PacketPlayerListHeaderFooter", "lm", Context.CLIENT); + public static Packets S0DPacketCollectItem = new Packets("net.minecraft.network.play.server.S0DPacketCollectItem", "ln", Context.CLIENT); + public static Packets S18PacketEntityTeleport = new Packets("net.minecraft.network.play.server.S18PacketEntityTeleport", "lo", Context.CLIENT); + public static Packets S20PacketEntityProperties = new Packets("net.minecraft.network.play.server.S20PacketEntityProperties", "lp", Context.CLIENT); + public static Packets S1DPacketEntityEffect = new Packets("net.minecraft.network.play.server.S1DPacketEntityEffect", "lr", Context.CLIENT); + public static Packets C14PacketTabComplete = new Packets("net.minecraft.network.play.client.C14PacketTabComplete", "lt", Context.SERVER); + public static Packets C01PacketChatMessage = new Packets("net.minecraft.network.play.client.C01PacketChatMessage", "lu", Context.SERVER); + public static Packets C16PacketClientStatus = new Packets("net.minecraft.network.play.client.C16PacketClientStatus", "lv", Context.SERVER); + public static Packets C15PacketClientSettings = new Packets("net.minecraft.network.play.client.C15PacketClientSettings", "lx", Context.SERVER); + public static Packets C0FPacketConfirmTransaction = new Packets("net.minecraft.network.play.client.C0FPacketConfirmTransaction", "ly", Context.SERVER); + public static Packets C11PacketEnchantItem = new Packets("net.minecraft.network.play.client.C11PacketEnchantItem", "lz", Context.SERVER); + public static Packets C0EPacketClickWindow = new Packets("net.minecraft.network.play.client.C0EPacketClickWindow", "ma", Context.SERVER); + public static Packets C0DPacketCloseWindow = new Packets("net.minecraft.network.play.client.C0DPacketCloseWindow", "mb", Context.SERVER); + public static Packets C17PacketCustomPayload = new Packets("net.minecraft.network.play.client.C17PacketCustomPayload", "mc", Context.SERVER); + public static Packets C02PacketUseEntity = new Packets("net.minecraft.network.play.client.C02PacketUseEntity", "md", Context.SERVER); + public static Packets C00PacketKeepAlive = new Packets("net.minecraft.network.play.client.C00PacketKeepAlive", "mf", Context.SERVER); + public static Packets C03PacketPlayer = new Packets("net.minecraft.network.play.client.C03PacketPlayer", "mg", Context.SERVER); + public static Packets C04PacketPlayerPosition = new Packets("net.minecraft.network.play.client.C03PacketPlayer$C04PacketPlayerPosition", "mh", Context.SERVER); + public static Packets C06PacketPlayerPosLook = new Packets("net.minecraft.network.play.client.C03PacketPlayer$C06PacketPlayerPosLook", "mi", Context.SERVER); + public static Packets C05PacketPlayerLook = new Packets("net.minecraft.network.play.client.C03PacketPlayer$C05PacketPlayerLook", "mj", Context.SERVER); + public static Packets C13PacketPlayerAbilities = new Packets("net.minecraft.network.play.client.C13PacketPlayerAbilities", "mk", Context.SERVER); + public static Packets C07PacketPlayerDigging = new Packets("net.minecraft.network.play.client.C07PacketPlayerDigging", "ml", Context.SERVER); + public static Packets C0BPacketEntityAction = new Packets("net.minecraft.network.play.client.C0BPacketEntityAction", "mn", Context.SERVER); + public static Packets C0CPacketInput = new Packets("net.minecraft.network.play.client.C0CPacketInput", "mp", Context.SERVER); + public static Packets C19PacketResourcePackStatus = new Packets("net.minecraft.network.play.client.C19PacketResourcePackStatus", "mq", Context.SERVER); + public static Packets C09PacketHeldItemChange = new Packets("net.minecraft.network.play.client.C09PacketHeldItemChange", "ms", Context.SERVER); + public static Packets C10PacketCreativeInventoryAction = new Packets("net.minecraft.network.play.client.C10PacketCreativeInventoryAction", "mt", Context.SERVER); + public static Packets C12PacketUpdateSign = new Packets("net.minecraft.network.play.client.C12PacketUpdateSign", "mu", Context.SERVER); + public static Packets C0APacketAnimation = new Packets("net.minecraft.network.play.client.C0APacketAnimation", "mv", Context.SERVER); + public static Packets C18PacketSpectate = new Packets("net.minecraft.network.play.client.C18PacketSpectate", "mw", Context.SERVER); + public static Packets C08PacketPlayerBlockPlacement = new Packets("net.minecraft.network.play.client.C08PacketPlayerBlockPlacement", "mx", Context.SERVER); + public static Packets C00Handshake = new Packets("net.minecraft.network.handshake.client.C00Handshake", "mz", Context.SERVER); + public static Packets S02PacketLoginSuccess = new Packets("net.minecraft.network.login.server.S02PacketLoginSuccess", "nd", Context.CLIENT); + public static Packets S01PacketEncryptionRequest = new Packets("net.minecraft.network.login.server.S01PacketEncryptionRequest", "ne", Context.CLIENT); + public static Packets S03PacketEnableCompression = new Packets("net.minecraft.network.login.server.S03PacketEnableCompression", "nf", Context.CLIENT); + public static Packets S00PacketDisconnect = new Packets("net.minecraft.network.login.server.S00PacketDisconnect", "ng", Context.CLIENT); + public static Packets C00PacketLoginStart = new Packets("net.minecraft.network.login.client.C00PacketLoginStart", "ni", Context.SERVER); + public static Packets C01PacketEncryptionResponse = new Packets("net.minecraft.network.login.client.C01PacketEncryptionResponse", "nj", Context.SERVER); + public static Packets S01PacketPong = new Packets("net.minecraft.network.status.server.S01PacketPong", "nn", Context.CLIENT); + public static Packets S00PacketServerInfo = new Packets("net.minecraft.network.status.server.S00PacketServerInfo", "no", Context.CLIENT); + public static Packets C01PacketPing = new Packets("net.minecraft.network.status.client.C01PacketPing", "nw", Context.SERVER); + public static Packets C00PacketServerQuery = new Packets("net.minecraft.network.status.client.C00PacketServerQuery", "nx", Context.SERVER); + + // CHECKSTYLE:ON + + public static final Packets[] packets = new Packets[] { + S08PacketPlayerPosLook, + S0EPacketSpawnObject, + S11PacketSpawnExperienceOrb, + S2CPacketSpawnGlobalEntity, + S0FPacketSpawnMob, + S10PacketSpawnPainting, + S0CPacketSpawnPlayer, + S0BPacketAnimation, + S37PacketStatistics, + S25PacketBlockBreakAnim, + S35PacketUpdateTileEntity, + S24PacketBlockAction, + S23PacketBlockChange, + S41PacketServerDifficulty, + S3APacketTabComplete, + S02PacketChat, + S22PacketMultiBlockChange, + S32PacketConfirmTransaction, + S2EPacketCloseWindow, + S2DPacketOpenWindow, + S30PacketWindowItems, + S31PacketWindowProperty, + S2FPacketSetSlot, + S3FPacketCustomPayload, + S40PacketDisconnect, + S19PacketEntityStatus, + S49PacketUpdateEntityNBT, + S27PacketExplosion, + S46PacketSetCompressionLevel, + S2BPacketChangeGameState, + S00PacketKeepAlive, + S21PacketChunkData, + S26PacketMapChunkBulk, + S28PacketEffect, + S2APacketParticles, + S29PacketSoundEffect, + S01PacketJoinGame, + S34PacketMaps, + S14PacketEntity, + S15PacketEntityRelMove, + S17PacketEntityLookMove, + S16PacketEntityLook, + S36PacketSignEditorOpen, + S39PacketPlayerAbilities, + S42PacketCombatEvent, + S38PacketPlayerListItem, + S0APacketUseBed, + S13PacketDestroyEntities, + S1EPacketRemoveEntityEffect, + S48PacketResourcePackSend, + S07PacketRespawn, + S19PacketEntityHeadLook, + S44PacketWorldBorder, + S43PacketCamera, + S09PacketHeldItemChange, + S3DPacketDisplayScoreboard, + S1CPacketEntityMetadata, + S1BPacketEntityAttach, + S12PacketEntityVelocity, + S04PacketEntityEquipment, + S1FPacketSetExperience, + S06PacketUpdateHealth, + S3BPacketScoreboardObjective, + S3EPacketTeams, + S3CPacketUpdateScore, + S05PacketSpawnPosition, + S03PacketTimeUpdate, + S45PacketTitle, + S33PacketUpdateSign, + S47PacketPlayerListHeaderFooter, + S0DPacketCollectItem, + S18PacketEntityTeleport, + S20PacketEntityProperties, + S1DPacketEntityEffect, + C14PacketTabComplete, + C01PacketChatMessage, + C16PacketClientStatus, + C15PacketClientSettings, + C0FPacketConfirmTransaction, + C11PacketEnchantItem, + C0EPacketClickWindow, + C0DPacketCloseWindow, + C17PacketCustomPayload, + C02PacketUseEntity, + C00PacketKeepAlive, + C03PacketPlayer, + C04PacketPlayerPosition, + C06PacketPlayerPosLook, + C05PacketPlayerLook, + C13PacketPlayerAbilities, + C07PacketPlayerDigging, + C0BPacketEntityAction, + C0CPacketInput, + C19PacketResourcePackStatus, + C09PacketHeldItemChange, + C10PacketCreativeInventoryAction, + C12PacketUpdateSign, + C0APacketAnimation, + C18PacketSpectate, + C08PacketPlayerBlockPlacement, + C00Handshake, + S02PacketLoginSuccess, + S01PacketEncryptionRequest, + S03PacketEnableCompression, + S00PacketDisconnect, + C00PacketLoginStart, + C01PacketEncryptionResponse, + S01PacketPong, + S00PacketServerInfo, + C01PacketPing, + C00PacketServerQuery, + }; + + private static int nextPacketIndex; + + private final String shortName; + + private final int index; + + private final Context context; + + private Packets(String seargeName, String obfName, Context context) + { + super(seargeName, obfName); + + this.shortName = seargeName.substring(Math.max(seargeName.lastIndexOf('.'), seargeName.lastIndexOf('$')) + 1); + this.index = Packets.nextPacketIndex++; + Packets.packetMap.put(this.shortName, this); + this.context = context; + } + + public int getIndex() + { + return this.index; + } + + public String getShortName() + { + return this.shortName; + } + + public Context getContext() + { + return this.context; + } + + public static int indexOf(String packetClassName) + { + for (Packets packet : Packets.packets) + { + if (packet.name.equals(packetClassName) || packet.shortName.equals(packetClassName) || packet.obf.equals(packetClassName)) + { + return packet.index; + } + } + + return -1; + } + + public static int count() + { + return Packets.nextPacketIndex; + } + + /** + * @param name + */ + public static Packets getByName(String name) + { + return Packets.packetMap.get(name); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLaunchWrapper.java b/liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLaunchWrapper.java new file mode 100644 index 00000000..2937d9fa --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLaunchWrapper.java @@ -0,0 +1,47 @@ +package com.mumfrey.liteloader.crashreport; + +import java.util.List; +import java.util.concurrent.Callable; + +import net.minecraft.crash.CrashReport; +import net.minecraft.launchwrapper.IClassTransformer; +import net.minecraft.launchwrapper.Launch; + +public class CallableLaunchWrapper implements Callable +{ + final CrashReport crashReport; + + public CallableLaunchWrapper(CrashReport report) + { + this.crashReport = report; + } + + /* (non-Javadoc) + * @see java.util.concurrent.Callable#call() + */ + @Override + public String call() throws Exception + { + return CallableLaunchWrapper.generateTransformerList(); + } + + /** + * Generates a list of active transformers to display in the crash report + */ + public static String generateTransformerList() + { + final List transformers = Launch.classLoader.getTransformers(); + + StringBuilder sb = new StringBuilder(); + sb.append(transformers.size()); + sb.append(" active transformer(s)"); + + for (IClassTransformer transformer : transformers) + { + sb.append("\n - Transformer: "); + sb.append(transformer.getClass().getName()); + } + + return sb.toString(); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLiteLoaderBrand.java b/liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLiteLoaderBrand.java new file mode 100644 index 00000000..16a0490a --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLiteLoaderBrand.java @@ -0,0 +1,35 @@ +package com.mumfrey.liteloader.crashreport; + +import java.util.concurrent.Callable; + +import net.minecraft.crash.CrashReport; + +import com.mumfrey.liteloader.core.LiteLoader; + +public class CallableLiteLoaderBrand implements Callable +{ + final CrashReport crashReport; + + public CallableLiteLoaderBrand(CrashReport report) + { + this.crashReport = report; + } + + /* (non-Javadoc) + * @see java.util.concurrent.Callable#call() + */ + @Override + public String call() throws Exception + { + String brand = null; + try + { + brand = LiteLoader.getBranding(); + } + catch (Exception ex) + { + brand = "LiteLoader startup failed"; + } + return brand == null ? "Unknown / None" : brand; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLiteLoaderMods.java b/liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLiteLoaderMods.java new file mode 100644 index 00000000..7f482c62 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/crashreport/CallableLiteLoaderMods.java @@ -0,0 +1,33 @@ +package com.mumfrey.liteloader.crashreport; + +import java.util.concurrent.Callable; + +import net.minecraft.crash.CrashReport; + +import com.mumfrey.liteloader.core.LiteLoader; + +public class CallableLiteLoaderMods implements Callable +{ + final CrashReport crashReport; + + public CallableLiteLoaderMods(CrashReport report) + { + this.crashReport = report; + } + + /* (non-Javadoc) + * @see java.util.concurrent.Callable#call() + */ + @Override + public String call() throws Exception + { + try + { + return LiteLoader.getInstance().getLoadedModsList(); + } + catch (Exception ex) + { + return "LiteLoader startup failed"; + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/FastIterable.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/FastIterable.java new file mode 100644 index 00000000..b4ab6cfe --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/FastIterable.java @@ -0,0 +1,29 @@ +package com.mumfrey.liteloader.interfaces; + +/** + * Interface for objects which can return a baked list view of their list + * contents. + * + * @author Adam Mummery-Smith + * + * @param + */ +public interface FastIterable extends Iterable +{ + /** + * Add an entry to the iterable + * + * @param entry + */ + public boolean add(T entry); + + /** + * Return the baked view of all entries + */ + public T all(); + + /** + * Invalidate (force rebake of) the baked entry list + */ + public void invalidate(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/FastIterableDeque.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/FastIterableDeque.java new file mode 100644 index 00000000..ae879f16 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/FastIterableDeque.java @@ -0,0 +1,14 @@ +package com.mumfrey.liteloader.interfaces; + +import java.util.Deque; + +/** + * Deque interface which is FastIterable + * + * @author Adam Mummery-Smith + * + * @param + */ +public interface FastIterableDeque extends FastIterable, Deque +{ +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/Injectable.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/Injectable.java new file mode 100644 index 00000000..c0021b6a --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/Injectable.java @@ -0,0 +1,40 @@ +package com.mumfrey.liteloader.interfaces; + +import java.net.MalformedURLException; +import java.net.URL; + +import com.mumfrey.liteloader.launch.InjectionStrategy; + +import net.minecraft.launchwrapper.LaunchClassLoader; + +/** + * Interface for objects which can be injected into the classpath + * + * @author Adam Mummery-Smith + */ +public interface Injectable +{ + /** + * Get the URL of this injectable resource + * @throws MalformedURLException + */ + public abstract URL getURL() throws MalformedURLException; + + /** + * Returns true if this object has been injected already + */ + public abstract boolean isInjected(); + + /** + * @param classLoader + * @param injectIntoParent + * @return whether the injection was successful or not + * @throws MalformedURLException + */ + public abstract boolean injectIntoClassPath(LaunchClassLoader classLoader, boolean injectIntoParent) throws MalformedURLException; + + /** + * Get the injection strategy for this object + */ + public abstract InjectionStrategy getInjectionStrategy(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/InterfaceRegistry.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/InterfaceRegistry.java new file mode 100644 index 00000000..d325e8fb --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/InterfaceRegistry.java @@ -0,0 +1,20 @@ +package com.mumfrey.liteloader.interfaces; + +import com.mumfrey.liteloader.api.Listener; +import com.mumfrey.liteloader.api.InterfaceProvider; +import com.mumfrey.liteloader.api.LiteAPI; + +/** + * + * @author Adam Mummery-Smith + */ +public interface InterfaceRegistry +{ + public abstract void registerAPI(LiteAPI api); + + public abstract void registerInterface(InterfaceProvider provider, Class interfaceType); + + public abstract void registerInterface(InterfaceProvider provider, Class interfaceType, int priority); + + public abstract void registerInterface(InterfaceProvider provider, Class interfaceType, int priority, boolean exclusive); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/Loadable.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/Loadable.java new file mode 100644 index 00000000..a39db61b --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/Loadable.java @@ -0,0 +1,100 @@ +package com.mumfrey.liteloader.interfaces; + +import java.io.File; + +import com.mumfrey.liteloader.launch.LoaderEnvironment; + +/** + * Interface for things which are loadable, essentially mods and tweaks + * + * @author Adam Mummery-Smith + * + * @param base class type for Comparable so that implementors can specify + * their Comparable type. + */ +public interface Loadable extends Comparable +{ + /** + * Get the target resource + */ + public abstract L getTarget(); + + /** + * Get the name of the loadable (usually the file name) + */ + public abstract String getName(); + + /** + * Get the name to use when displaying this loadable, such as file name, + * identifier or friendly name + */ + public abstract String getDisplayName(); + + /** + * Get the location (path or URL) of this loadable + */ + public abstract String getLocation(); + + /** + * Get the identifier (usually "name" from metadata) of this loadable, used + * as the exclusivity key for mods and also the metadata key + */ + public abstract String getIdentifier(); + + /** + * Get the version specified in the metadata or other location + */ + public abstract String getVersion(); + + /** + * Get the author specified in the metadata + */ + public abstract String getAuthor(); + + /** + * Get the description + */ + public abstract String getDescription(String key); + + /** + * Returns true if this is an external jar containing a tweak rather than a + * mod. + */ + public abstract boolean isExternalJar(); + + /** + * Returns true if this loadable supports being enabled and disabled via the + * GUI. + */ + public abstract boolean isToggleable(); + + /** + * Get whether this loadable is currently enabled in the context of the + * supplied mods list. + * + * @param environment + */ + public abstract boolean isEnabled(LoaderEnvironment environment); + + /** + * Get whether this loadable is a file container + */ + public abstract boolean isFile(); + + /** + * Get whether this loadable is a directory container + */ + public abstract boolean isDirectory(); + + /** + * If isFile or isDirectory return true then this method returns the inner + * File instance, otherwise returns null. + */ + public abstract File toFile(); + + /** + * Get whether this container requires early injection, eg. it contains a + * tweaker, transformer or mixins + */ + public abstract boolean requiresPreInitInjection(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoadableFile.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoadableFile.java new file mode 100644 index 00000000..dfef3c91 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoadableFile.java @@ -0,0 +1,507 @@ +package com.mumfrey.liteloader.interfaces; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import com.google.common.io.Files; +import com.google.common.primitives.Ints; +import com.mumfrey.liteloader.core.api.LoadableModFile; +import com.mumfrey.liteloader.launch.ClassPathUtilities; +import com.mumfrey.liteloader.launch.InjectionStrategy; +import com.mumfrey.liteloader.launch.LiteLoaderTweaker; +import com.mumfrey.liteloader.launch.LoaderEnvironment; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +import net.minecraft.launchwrapper.LaunchClassLoader; + +public class LoadableFile extends File implements TweakContainer +{ + public static final String MFATT_MODTYPE = "ModType"; + public static final String MFATT_TWEAK_CLASS = "TweakClass"; + public static final String MFATT_CLASS_PATH = "Class-Path"; + public static final String MFATT_TWEAK_ORDER = "TweakOrder"; + public static final String MFATT_IMPLEMENTATION_TITLE = "Implementation-Title"; + public static final String MFATT_TWEAK_NAME = "TweakName"; + public static final String MFATT_IMPLEMENTATION_VERSION = "Implementation-Version"; + public static final String MFATT_TWEAK_VERSION = "TweakVersion"; + public static final String MFATT_IMPLEMENTATION_VENDOR = "Implementation-Vendor"; + public static final String MFATT_TWEAK_AUTHOR = "TweakAuthor"; + public static final String MFATT_MIXIN_CONFIGS = "MixinConfigs"; + public static final String MFATT_INJECTION_STRATEGY = "TweakInjectionStrategy"; + + private static final Pattern versionPattern = Pattern.compile("([0-9]+\\.)+[0-9]+([_A-Z0-9]+)?"); + + private static final long serialVersionUID = 1L; + + /** + * True once this file has been injected into the class path + */ + protected boolean injected; + + protected boolean forceInjection; + + /** + * Position to inject the mod file at in the class path, if blank injects at + * the bottom as usual, alternatively the developer can specify "top" to + * inject at the top, "base" to inject above the game jar, or "above: name" + * to inject above a specified other library matching "name". + */ + protected InjectionStrategy injectionStrategy = null; + + protected Set modSystems = new HashSet(); + + /** + * Name of the tweak class + */ + protected String tweakClassName; + + /** + * Priority for this tweaker + */ + protected int tweakPriority = 1000; + + /** + * Class path entries read from jar metadata + */ + protected String[] classPathEntries = null; + + protected String displayName; + + protected String version = "Unknown"; + + protected String author = "Unknown"; + + protected boolean hasEventTransformers; + + /** + * Mixin config resource names + */ + protected Set mixinConfigs = new HashSet(); + + /** + * Create a new tweak container wrapping the specified file + */ + public LoadableFile(File parent) + { + super(parent.getAbsolutePath()); + this.displayName = this.getName(); + this.guessVersionFromName(); + this.readJarMetaData(); + } + + /** + * ctor for subclasses + */ + protected LoadableFile(LoadableFile file) + { + super(file.getAbsolutePath()); + this.displayName = this.getName(); + this.forceInjection = file.forceInjection; + this.assignJarMetaData(file); + } + + /** + * ctor for subclasses + */ + protected LoadableFile(String pathname) + { + super(pathname); + this.displayName = this.getName(); + this.readJarMetaData(); + } + + private void guessVersionFromName() + { + Matcher versionPatternMatcher = LoadableFile.versionPattern.matcher(this.getName()); + while (versionPatternMatcher.find()) + { + this.version = versionPatternMatcher.group(); + } + } + + protected void assignJarMetaData(LoadableFile file) + { + this.modSystems = file.modSystems; + this.tweakClassName = file.tweakClassName; + this.classPathEntries = file.classPathEntries; + this.tweakPriority = file.tweakPriority; + this.displayName = file.displayName; + this.version = file.version; + this.author = file.author; + this.injectionStrategy = file.injectionStrategy; + } + + /** + * Search for tweaks in this file + */ + protected void readJarMetaData() + { + JarFile jar = null; + + if (this.isDirectory()) + { + return; + } + + try + { + jar = new JarFile(this); + if (jar.getManifest() != null) + { + LiteLoaderLogger.info("Inspecting jar metadata in '%s'", this.getName()); + Attributes mfAttributes = jar.getManifest().getMainAttributes(); + + String mfAttmodSystemList = mfAttributes.getValue(LoadableFile.MFATT_MODTYPE); + String mfAttTweakClass = mfAttributes.getValue(LoadableFile.MFATT_TWEAK_CLASS); + String mfAttClassPath = mfAttributes.getValue(LoadableFile.MFATT_CLASS_PATH); + String mfAttTweakOrder = mfAttributes.getValue(LoadableFile.MFATT_TWEAK_ORDER); + String mfAttDisplayName = mfAttributes.getValue(LoadableFile.MFATT_IMPLEMENTATION_TITLE); + String mfAttTweakName = mfAttributes.getValue(LoadableFile.MFATT_TWEAK_NAME); + String mfAttVersion = mfAttributes.getValue(LoadableFile.MFATT_IMPLEMENTATION_VERSION); + String mfAttTweakVersion = mfAttributes.getValue(LoadableFile.MFATT_TWEAK_VERSION); + String mfAttAuthor = mfAttributes.getValue(LoadableFile.MFATT_IMPLEMENTATION_VENDOR); + String mfAttTweakAuthor = mfAttributes.getValue(LoadableFile.MFATT_TWEAK_AUTHOR); + String mfAttMixinConfigs = mfAttributes.getValue(LoadableFile.MFATT_MIXIN_CONFIGS); + String mfAttInjectionStrategy = mfAttributes.getValue(LoadableFile.MFATT_INJECTION_STRATEGY); + + if (mfAttmodSystemList != null) + { + for (String modSystem : mfAttmodSystemList.split(",")) + { + modSystem = modSystem.trim(); + if (modSystem.length() > 0) + { + this.modSystems.add(modSystem); + } + } + } + + this.tweakClassName = mfAttTweakClass; + if (this.tweakClassName != null && mfAttClassPath != null) + { + this.classPathEntries = mfAttClassPath.split(" "); + } + + if (mfAttTweakOrder != null) + { + Integer tweakOrder = Ints.tryParse(mfAttTweakOrder); + if (tweakOrder != null) + { + this.tweakPriority = tweakOrder.intValue(); + } + } + + if (mfAttDisplayName != null) this.displayName = mfAttDisplayName; + if (mfAttTweakName != null) this.displayName = mfAttTweakName; + if (mfAttVersion != null) this.version = mfAttVersion; + if (mfAttTweakVersion != null) this.version = mfAttTweakVersion; + if (mfAttAuthor != null) this.author = mfAttAuthor; + if (mfAttTweakAuthor != null) this.author = mfAttTweakAuthor; + + if (mfAttMixinConfigs != null) + { + for (String config : mfAttMixinConfigs.split(",")) + { + this.mixinConfigs.add(config); + } + } + + this.injectionStrategy = InjectionStrategy.parseStrategy(mfAttInjectionStrategy, InjectionStrategy.TOP); + } + } + catch (Exception ex) + { + LiteLoaderLogger.warning("Could not parse jar metadata in '%s'", this); + } + finally + { + try + { + if (jar != null) jar.close(); + } + catch (IOException ex) {} + } + } + + public Set getModSystems() + { + return Collections.unmodifiableSet(this.modSystems); + } + + @Override + public File getTarget() + { + return this; + } + + @Override + public File toFile() + { + return this; + } + + @Override + public String getLocation() + { + return this.getAbsolutePath(); + } + + @Override + public URL getURL() throws MalformedURLException + { + return this.toURI().toURL(); + } + + @Override + public String getIdentifier() + { + return this.getName().toLowerCase(); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ITweakContainer#hasTweakClass() + */ + @Override + public boolean hasTweakClass() + { + return this.tweakClassName != null; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ITweakContainer#getTweakClassName() + */ + @Override + public String getTweakClassName() + { + return this.tweakClassName; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.TweakContainer#getTweakPriority() + */ + @Override + public int getTweakPriority() + { + return this.tweakPriority; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ITweakContainer#getClassPathEntries() + */ + @Override + public String[] getClassPathEntries() + { + return this.classPathEntries; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ITweakContainer#hasClassTransformers() + */ + @Override + public boolean hasClassTransformers() + { + return false; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.ITweakContainer + * #getClassTransformerClassNames() + */ + @Override + public List getClassTransformerClassNames() + { + return Collections.emptyList(); + } + + @Override + public boolean hasMixins() + { + return this.mixinConfigs.size() > 0; + } + + @Override + public Set getMixinConfigs() + { + return this.mixinConfigs; + } + + @Override + public boolean hasEventTransformers() + { + return this.hasEventTransformers; + } + + public void onEventsInjected() + { + this.hasEventTransformers = true; + } + + public boolean isInjectionForced() + { + return this.forceInjection; + } + + public void setForceInjection(boolean forceInjection) + { + this.forceInjection = forceInjection; + } + + @Override + public boolean requiresPreInitInjection() + { + return this.hasTweakClass() || this.hasClassTransformers() || this.hasMixins(); + } + + @Override + public boolean isInjected() + { + return this.injected; + } + + @Override + public boolean injectIntoClassPath(LaunchClassLoader classLoader, boolean injectIntoParent) throws MalformedURLException + { + if (!this.injected) + { + this.injected = true; + + boolean isOnClassPath = ClassPathUtilities.isJarOnClassPath(this, classLoader); + if (!this.forceInjection && isOnClassPath) + { + LiteLoaderLogger.info("%s already exists on the classpath, skipping injection", this); + return false; + } + + ClassPathUtilities.injectIntoClassPath(classLoader, this.getURL(), this.getInjectionStrategy()); + + if (injectIntoParent) + { + LiteLoaderTweaker.addURLToParentClassLoader(this.getURL()); + } + + return true; + + } + + return false; + } + + @Override + public InjectionStrategy getInjectionStrategy() + { + return this.injectionStrategy; + } + + @Override + public String getDisplayName() + { + return this.displayName != null ? this.displayName : this.getName(); + } + + @Override + public String getVersion() + { + return this.version; + } + + @Override + public String getAuthor() + { + return this.author; + } + + @Override + public String getDescription(String key) + { + return ""; + } + + @Override + public boolean isExternalJar() + { + return true; + } + + @Override + public boolean isToggleable() + { + return false; + } + + @Override + public boolean isEnabled(LoaderEnvironment environment) + { + return environment.getEnabledModsList().isEnabled(environment.getProfile(), this.getIdentifier()); + } + + @Override + public String toString() + { + return this.getLocation(); + } + + /** + * @param name + * @param charset + */ + public String getFileContents(String name, Charset charset) + { + return LoadableFile.getFileContents(this, name, charset); + } + + /** + * @param parent + * @param name + * @param charset + */ + public static String getFileContents(File parent, String name, Charset charset) + { + try + { + if (parent.isDirectory()) + { + File file = new File(parent, name); + if (file.isFile()) + { + return Files.toString(file, charset); + } + } + else + { + String content = null; + ZipFile zipFile = new ZipFile(parent); + ZipEntry zipEntry = zipFile.getEntry(name); + if (zipEntry != null) + { + try + { + content = LoadableModFile.zipEntryToString(zipFile, zipEntry); + } + catch (IOException ex) {} + } + + zipFile.close(); + return content; + } + } + catch (IOException ex) + { + ex.printStackTrace(); + } + + return null; + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoadableMod.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoadableMod.java new file mode 100644 index 00000000..19a36b1d --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoadableMod.java @@ -0,0 +1,355 @@ +package com.mumfrey.liteloader.interfaces; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.mumfrey.liteloader.launch.InjectionStrategy; +import com.mumfrey.liteloader.launch.LoaderEnvironment; + +import net.minecraft.launchwrapper.LaunchClassLoader; + +/** + * Interface for containers which can be loaded as mods + * + * @author Adam Mummery-Smith + * + * @param base class type for Comparable so that implementors can specify + * their Comparable type + */ +public interface LoadableMod extends Loadable, Injectable +{ + static final String METADATA_FILENAME = "litemod.json"; + + /** + * Get the mod systems declared in the jar metadata + */ + public abstract Set getModSystems(); + + /** + * Get the name of the mod + */ + public abstract String getModName(); + + /** + * Get the target loader version for this mod + */ + public abstract String getTargetVersion(); + + /** + * Get the revision number for this mod + */ + public abstract float getRevision(); + + /** + * Get whether this mod's metadata is valid + */ + public abstract boolean hasValidMetaData(); + + /** + * Get whether this mod has any dependencies + */ + public abstract boolean hasDependencies(); + + /** + * Get this mod's list of dependencies + */ + public abstract Set getDependencies(); + + /** + * Callback to notify the container that it's missing a specific dependency + */ + public abstract void registerMissingDependency(String dependency); + + /** + * Get this mod's list of missing dependencies + */ + public abstract Set getMissingDependencies(); + + /** + * Get this mod's list of required APIs + */ + public abstract Set getRequiredAPIs(); + + /** + * Callback to notify the container that it's missing a specific required + * API + */ + public abstract void registerMissingAPI(String identifier); + + /** + * Get this mod's list of missing APIs + */ + public abstract Set getMissingAPIs(); + + /** + * Get the specified metadata value and return the default value if not + * present + * + * @param metaKey metadata key + * @param defaultValue metadata value + */ + public abstract String getMetaValue(String metaKey, String defaultValue); + + /** + * Get the mod metadata key set + */ + public abstract Set getMetaDataKeys(); + + /** + * Returns true if this mod can be added as a resource pack + */ + public abstract boolean hasResources(); + + /** + * Get all class names in this container + */ + public abstract List getContainedClassNames(); + + /** + * Callback from the enumerator, whenever a mod is registered to this + * container + */ + public abstract void addContainedMod(String modName); + + /** + * Container returned instead of null when a mod does not actually have a + * container or a container is requested for a mod which doesn't exist. + */ + public static final LoadableMod NONE = new EmptyModContainer(); + + /** + * Mod container for a mod which doesn't have a container + * + * @author Adam Mummery-Smith + */ + public class EmptyModContainer implements LoadableMod + { + private static final ImmutableSet EMPTY_SET = ImmutableSet.of(); + + EmptyModContainer() {} + + @Override + public File getTarget() + { + return null; + } + + @Override + public Set getModSystems() + { + return EmptyModContainer.EMPTY_SET; + } + + @Override + public String getName() + { + return "Unknown"; + } + + @Override + public String getDisplayName() + { + return "Unknown"; + } + + @Override + public String getLocation() + { + return "."; + } + + @Override + public String getIdentifier() + { + return "Unknown"; + } + + @Override + public String getVersion() + { + return "Unknown"; + } + + @Override + public String getAuthor() + { + return "Unknown"; + } + + @Override + public String getDescription(String key) + { + return ""; + } + + @Override + public boolean isExternalJar() + { + return false; + } + + @Override + public boolean isToggleable() + { + return false; + } + + @Override + public boolean isEnabled(LoaderEnvironment environment) + { + return true; + } + + @Override + public boolean isFile() + { + return false; + } + + @Override + public boolean isDirectory() + { + return false; + } + + @Override + public File toFile() + { + return null; + } + + @Override + public int compareTo(File other) + { + return 0; + } + + @Override + public URL getURL() throws MalformedURLException + { + throw new MalformedURLException("Attempted to get the URL of an empty mod"); + } + + @Override + public boolean isInjected() + { + return false; + } + + @Override + public boolean injectIntoClassPath(LaunchClassLoader classLoader, boolean injectIntoParent) throws MalformedURLException + { + return false; + } + + @Override + public InjectionStrategy getInjectionStrategy() + { + return null; + } + + @Override + public String getModName() + { + return "Unknown"; + } + + @Override + public String getTargetVersion() + { + return ""; + } + + @Override + public float getRevision() + { + return 0; + } + + @Override + public boolean hasValidMetaData() + { + return false; + } + + @Override + public String getMetaValue(String metaKey, String defaultValue) + { + return defaultValue; + } + + @Override + public Set getMetaDataKeys() + { + return Collections.emptySet(); + } + + @Override + public boolean hasResources() + { + return false; + } + + @Override + public boolean hasDependencies() + { + return false; + } + + @Override + public Set getDependencies() + { + return Collections.emptySet(); + } + + @Override + public void registerMissingDependency(String dependency) + { + } + + @Override + public Set getMissingDependencies() + { + return Collections.emptySet(); + } + + @Override + public Set getRequiredAPIs() + { + return Collections.emptySet(); + } + + @Override + public void registerMissingAPI(String identifier) + { + } + + @Override + public Set getMissingAPIs() + { + return Collections.emptySet(); + } + + @Override + public List getContainedClassNames() + { + return Collections.emptyList(); + } + + @Override + public void addContainedMod(String modName) + { + } + + @Override + public boolean requiresPreInitInjection() + { + return false; + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoaderEnumerator.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoaderEnumerator.java new file mode 100644 index 00000000..c8538389 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/LoaderEnumerator.java @@ -0,0 +1,97 @@ +package com.mumfrey.liteloader.interfaces; + +import java.util.Collection; +import java.util.Map; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.core.ModInfo; + +/** + * Interface for the enumerator + * + * @author Adam Mummery-Smith + */ +public interface LoaderEnumerator extends ModularEnumerator +{ + /** + * Perform pre-init tasks (container discovery) + */ + public abstract void onPreInit(); + + /** + * Perform init tasks (injection and mod discovery) + */ + public abstract void onInit(); + + /** + * Check API requirements for the supplied container + * + * @param container + */ + public abstract boolean checkAPIRequirements(LoadableMod container); + + /** + * Check intra-mod dependencies for the supplied container + * + * @param base + */ + public abstract boolean checkDependencies(LoadableMod base); + + /** + * Inflect mod identifier from the supplied mod class + * + * @param modClass + */ + public abstract String getIdentifier(Class modClass); + + /** + * Get the container which the specified mod is loaded from + * + * @param modClass + */ + public abstract LoadableMod getContainer(Class modClass); + + /** + * Get the container for the specified mod identifier + * + * @param identifier + */ + public abstract LoadableMod getContainer(String identifier); + + /** + * Get all containers identified at discover-time as disabled + */ + public abstract Collection>> getDisabledContainers(); + + /** + * Get all bad containers + */ + public abstract Collection>> getBadContainers(); + + /** + * @param modClass + * @param metaDataKey + * @param defaultValue + */ + public abstract String getModMetaData(Class modClass, String metaDataKey, String defaultValue); + + /** + * Get the total number of mods to load + */ + public abstract int modsToLoadCount(); + + /** + * Get all mods to load + */ + public abstract Collection>> getModsToLoad(); + + /** + * Get all tweakers which were injected + */ + public abstract Collection>> getInjectedTweaks(); + + /** + * Get the shared modlist data + */ + public abstract Map> getSharedModList(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/MixinContainer.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/MixinContainer.java new file mode 100644 index 00000000..868cb22f --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/MixinContainer.java @@ -0,0 +1,42 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.mumfrey.liteloader.interfaces; + +import java.util.Set; + +public interface MixinContainer extends Loadable, Injectable +{ + + /** + * Get whether this container has any mixins + */ + public abstract boolean hasMixins(); + + /** + * Get this mod's list of mixin configs + */ + public abstract Set getMixinConfigs(); + +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/ModularEnumerator.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/ModularEnumerator.java new file mode 100644 index 00000000..5f94fded --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/ModularEnumerator.java @@ -0,0 +1,56 @@ +package com.mumfrey.liteloader.interfaces; + +import java.io.File; + +import com.mumfrey.liteloader.api.EnumeratorModule; +import com.mumfrey.liteloader.api.EnumeratorPlugin; +import com.mumfrey.liteloader.core.ModInfo; + +/** + * Interface for the mod enumerator + * + * @author Adam Mummery-Smith + */ +public interface ModularEnumerator +{ + /** + * Register a pluggable module into the enumerator + * + * @param module + */ + public abstract void registerModule(EnumeratorModule module); + + /** + * Register a plugin into the enumerator + * + * @param plugin + */ + public abstract void registerPlugin(EnumeratorPlugin plugin); + + /** + * @param container + */ + public abstract boolean registerModContainer(LoadableMod container); + + /** + * @param container + * @param reason + */ + public abstract void registerBadContainer(Loadable container, String reason); + + /** + * @param container + */ + public abstract boolean registerTweakContainer(TweakContainer container); + + /** + * @param container + * @param registerContainer + */ + public abstract void registerModsFrom(LoadableMod container, boolean registerContainer); + + /** + * @param mod + */ + public abstract void registerMod(ModInfo> mod); +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/ObjectFactory.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/ObjectFactory.java new file mode 100644 index 00000000..7356de9b --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/ObjectFactory.java @@ -0,0 +1,45 @@ +package com.mumfrey.liteloader.interfaces; + +import net.minecraft.server.MinecraftServer; + +import com.mumfrey.liteloader.common.GameEngine; +import com.mumfrey.liteloader.core.ClientPluginChannels; +import com.mumfrey.liteloader.core.LiteLoaderEventBroker; +import com.mumfrey.liteloader.core.PacketEvents; +import com.mumfrey.liteloader.core.ServerPluginChannels; +import com.mumfrey.liteloader.permissions.PermissionsManagerClient; +import com.mumfrey.liteloader.permissions.PermissionsManagerServer; +import com.mumfrey.liteloader.util.Input; + +/** + * Factory for generating loader managament objects based on the environment + * + * @author Adam Mummery-Smith + * + * @param Type of the client runtime, "Minecraft" on client and null + * on the server + * @param Type of the server runtime, "IntegratedServer" on the client + * "MinecraftServer" on the server + */ +public interface ObjectFactory +{ + public abstract LiteLoaderEventBroker getEventBroker(); + + public abstract PacketEvents getPacketEventBroker(); + + public abstract Input getInput(); + + public abstract GameEngine getGameEngine(); + + public abstract PanelManager getPanelManager(); + + public abstract ClientPluginChannels getClientPluginChannels(); + + public abstract ServerPluginChannels getServerPluginChannels(); + + public abstract PermissionsManagerClient getClientPermissionManager(); + + public abstract PermissionsManagerServer getServerPermissionManager(); + + public abstract void preBeginGame(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/PanelManager.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/PanelManager.java new file mode 100644 index 00000000..b9303b42 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/PanelManager.java @@ -0,0 +1,86 @@ +package com.mumfrey.liteloader.interfaces; + +import com.mumfrey.liteloader.api.PostRenderObserver; +import com.mumfrey.liteloader.api.TickObserver; +import com.mumfrey.liteloader.core.LiteLoaderMods; +import com.mumfrey.liteloader.modconfig.ConfigManager; + +/** + * Interface for the liteloader panel manager, abstracted because we don't have + * the class GuiScreen on the server. + * + * @author Adam Mummery-Smith + * + * @param GuiScreen class, must be generic because we don't have + * GuiScreen on the server side + */ +public interface PanelManager extends TickObserver, PostRenderObserver +{ + /** + * @param mods + * @param configManager + */ + public abstract void init(LiteLoaderMods mods, ConfigManager configManager); + + /** + * + */ + public abstract void onStartupComplete(); + + /** + * Hide the LiteLoader tab + */ + public abstract void hideTab(); + + /** + * Set the LiteLoader tab's visibility + */ + public abstract void setTabVisible(boolean show); + + /** + * Get whether the LiteLoader tab is visible + */ + public abstract boolean isTabVisible(); + + /** + * Set whether the LiteLoader tab should remain expanded + */ + public abstract void setTabAlwaysExpanded(boolean expand); + + /** + * Get whether the LiteLoader tab should remain expanded + */ + public abstract boolean isTabAlwaysExpanded(); + + /** + * Display the LiteLoader panel + * + * @param parentScreen Parent screen to display the panel on top of + */ + public abstract void displayLiteLoaderPanel(TParentScreen parentScreen); + + /** + * Get the number of startup errors + */ + public abstract int getStartupErrorCount(); + + /** + * Get the number of critical startup errors + */ + public abstract int getCriticalErrorCount(); + + /** + * Set the current notification text + */ + public abstract void setNotification(String notification); + + /** + * Set whether "force update" is enabled + */ + public abstract void setForceUpdateEnabled(boolean forceUpdate); + + /** + * Get whether "force update" is enabled + */ + public abstract boolean isForceUpdateEnabled(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/TweakContainer.java b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/TweakContainer.java new file mode 100644 index 00000000..86ec510d --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/interfaces/TweakContainer.java @@ -0,0 +1,47 @@ +package com.mumfrey.liteloader.interfaces; + +import java.util.List; + +/** + * Interface for loadables which can contain tweaks and transformers + * + * @author Adam Mummery-Smith + */ +public interface TweakContainer extends MixinContainer +{ + /** + * Get whether this tweak container has a defined tweak class in its + * metadata. + */ + public abstract boolean hasTweakClass(); + + /** + * Get the tweak class name defined in the metadata + */ + public abstract String getTweakClassName(); + + /** + * Get the priority value for this tweak + */ + public abstract int getTweakPriority(); + + /** + * Get classpath entries defined in the metadata + */ + public abstract String[] getClassPathEntries(); + + /** + * Get whether this container defines any transformer classes + */ + public abstract boolean hasClassTransformers(); + + /** + * Get class transformers defined in the metadata + */ + public abstract List getClassTransformerClassNames(); + + /** + * True if this container defines event transformers via JSON + */ + public abstract boolean hasEventTransformers(); +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/ClassPathUtilities.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/ClassPathUtilities.java new file mode 100644 index 00000000..272d671f --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/ClassPathUtilities.java @@ -0,0 +1,478 @@ +package com.mumfrey.liteloader.launch; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Stack; +import java.util.jar.JarFile; + +import com.mumfrey.liteloader.launch.InjectionStrategy.InjectionPosition; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +import net.minecraft.launchwrapper.Launch; +import net.minecraft.launchwrapper.LaunchClassLoader; + +/** + * Nasty horrible reflection hacks to do nasty things with the classpath + * + * @author Adam Mummery-Smith + */ +public abstract class ClassPathUtilities +{ + /** + * URLClassPath + */ + private static Class clURLClassPath; + + /** + * URLClassLoader::ucp -> instance of URLClassPath + */ + private static Field ucp; + + /** + * URLClassPath::urls -> instance of Stack + */ + private static Field classPathURLs; + + /** + * URLClassPath::path -> instance of ArrayList + */ + private static Field classPathPath; + + /** + * URLClassPath::lmap -> instance of HashMap + */ + private static Field classPathLoaderMap; + + /** + * URLClassPath::loaders -> instance of ArrayList + */ + private static Field classPathLoaderList; + + private static boolean canInject; + + private static boolean canTerminate; + + static + { + try + { + ClassPathUtilities.clURLClassPath = Class.forName("sun.misc.URLClassPath"); + + ClassPathUtilities.ucp = URLClassLoader.class.getDeclaredField("ucp"); + ClassPathUtilities.ucp.setAccessible(true); + + ClassPathUtilities.classPathURLs = ClassPathUtilities.clURLClassPath.getDeclaredField("urls"); + ClassPathUtilities.classPathURLs.setAccessible(true); + ClassPathUtilities.classPathPath = ClassPathUtilities.clURLClassPath.getDeclaredField("path"); + ClassPathUtilities.classPathPath.setAccessible(true); + ClassPathUtilities.classPathLoaderMap = ClassPathUtilities.clURLClassPath.getDeclaredField("lmap"); + ClassPathUtilities.classPathLoaderMap.setAccessible(true); + ClassPathUtilities.classPathLoaderList = ClassPathUtilities.clURLClassPath.getDeclaredField("loaders"); + ClassPathUtilities.classPathLoaderList.setAccessible(true); + ClassPathUtilities.canInject = true; + } + catch (Throwable th) + { + LiteLoaderLogger.severe(th, "ClassPathUtilities: Error initialising ClassPathUtilities, special class path injection disabled"); + th.printStackTrace(); + } + } + + /** + * Injects a URL into the classpath based on the specified injection + * strategy. + * + * @param classLoader + * @param url + */ + public static void injectIntoClassPath(URLClassLoader classLoader, URL url, InjectionStrategy strategy) + { + if (strategy == null || strategy.getPosition() == null) + { + ClassPathUtilities.addURL(classLoader, url); + return; + } + + if (strategy.getPosition() == InjectionPosition.Top) + { + ClassPathUtilities.injectIntoClassPath(classLoader, url); + } + else if (strategy.getPosition() == InjectionPosition.Base) + { + ClassPathUtilities.injectIntoClassPath(classLoader, url, LiteLoaderTweaker.getJarUrl()); + } + else if (strategy.getPosition() == InjectionPosition.Above) + { + String[] params = strategy.getParams(); + if (params.length > 0) + { + ClassPathUtilities.injectIntoClassPath(classLoader, url, params[0]); + } + } + else + { + ClassPathUtilities.addURL(classLoader, url); + } + } + + /** + * Injects a URL into the classpath at the TOP of the stack + * + * @param classLoader + * @param url + */ + public static void injectIntoClassPath(URLClassLoader classLoader, URL url) + { + ClassPathUtilities.injectIntoClassPath(classLoader, url, (URL)null); + } + + /** + * Injects a URL into the classpath at the TOP of the stack + * + * @param classLoader + * @param url + * @param above + */ + @SuppressWarnings({ "unchecked" }) + public static void injectIntoClassPath(URLClassLoader classLoader, URL url, URL above) + { + if (ClassPathUtilities.canInject) + { + LiteLoaderLogger.info("ClassPathUtilities: attempting to inject %s into %s", url, classLoader.getClass().getSimpleName()); + + try + { + Object classPath = ClassPathUtilities.ucp.get(classLoader); + + Stack urls = (Stack)ClassPathUtilities.classPathURLs.get(classPath); + ArrayList path = (ArrayList)ClassPathUtilities.classPathPath.get(classPath); + + synchronized (urls) + { + if (!path.contains(url)) + { + urls.add(url); + + if (above == null) + { + path.add(0, url); + } + else + { + for (int pos = path.size() - 1; pos > 0; pos--) + { + if (above.equals(path.get(pos))) + { + path.add(pos, url); + } + } + } + } + } + } + catch (Exception ex) + { + LiteLoaderLogger.warning("ClassPathUtilities: failed to inject %s", url); + } + } + + ClassPathUtilities.addURL(classLoader, url); + } + + /** + * @param classLoader + * @param url + * @param above + */ + public static void injectIntoClassPath(URLClassLoader classLoader, URL url, String above) + { + above = above.trim().toLowerCase(); + if (above.length() < 1) return; + + for (URL classPathUrl : classLoader.getURLs()) + { + if (classPathUrl.toString().toLowerCase().contains(above)) + { + ClassPathUtilities.injectIntoClassPath(classLoader, url, classPathUrl); + return; + } + } + } + + /** + * @param classLoader + * @param url + */ + public static void addURL(URLClassLoader classLoader, URL url) + { + if (classLoader instanceof LaunchClassLoader) + { + ((LaunchClassLoader)classLoader).addURL(url); + } + else + { + try + { + Method mAddUrl = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + mAddUrl.setAccessible(true); + mAddUrl.invoke(classLoader, url); + } + catch (Exception ex) {} + } + } + + /** + * Is the specified jar on the game launch classpath + * + * @param jarFile + */ + public static boolean isJarOnClassPath(File jarFile) + { + URLClassLoader classLoader = (URLClassLoader)Launch.class.getClassLoader(); + return ClassPathUtilities.isJarOnClassPath(jarFile, classLoader); + } + + /** + * @param jarFile + * @param classLoader + */ + public static boolean isJarOnClassPath(File jarFile, URLClassLoader classLoader) + { + try + { + String jarURL = jarFile.toURI().toURL().toString(); + + URL[] classPath = classLoader.getURLs(); + for (URL classPathEntry : classPath) + { + if (classPathEntry.toString().equals(jarURL)) + { + return true; + } + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + + return false; + } + + /** + * Gets the file containing the specified resource + * + * @param contextClass + * @param resource + */ + public static File getPathToResource(Class contextClass, String resource) + { + URL res = contextClass.getResource(resource); + if (res == null) return null; + + boolean returnParent = true; + String jarPath = res.toString(); + if (jarPath.startsWith("jar:") && jarPath.indexOf('!') > -1) + { + jarPath = jarPath.substring(4, jarPath.indexOf('!')); + returnParent = false; + } + + if (jarPath.startsWith("file:")) + { + try + { + File targetFile = new File(new URI(jarPath)); + return returnParent ? targetFile.getParentFile() : targetFile; + } + catch (URISyntaxException ex) + { + // derp + } + } + + return null; + } + + /** + * @param contextClass + * @param resource + */ + public static boolean deleteClassPathJarContaining(Class contextClass, String resource) + { + File jarFile = ClassPathUtilities.getPathToResource(contextClass, resource); + if (jarFile != null && jarFile.exists() && jarFile.isFile() && jarFile.getName().endsWith(".jar")) + { + return ClassPathUtilities.deleteClassPathJar(jarFile.getName()); + } + + return false; + } + + /** + * @param jarFileName + */ + public static boolean deleteClassPathJar(String jarFileName) + { + try + { + // First try to find the jar reference in the class loaders + JarFile jar = ClassPathUtilities.getJarFromClassLoader(Launch.classLoader, jarFileName, false); + JarFile parentJar = ClassPathUtilities.getJarFromClassLoader((URLClassLoader)Launch.class.getClassLoader(), jarFileName, false); + + if (jar != null && parentJar != null && jar.getName().equals(parentJar.getName())) + { + final JarDeletionHandler jarDeletionHandler = new JarDeletionHandler(); + + JarFile jarInClassLoader = ClassPathUtilities.getJarFromClassLoader(Launch.classLoader, jarFileName, true); + JarFile jarInParentClassLoader = ClassPathUtilities.getJarFromClassLoader((URLClassLoader)Launch.class.getClassLoader(), + jarFileName, true); + + File jarFileInClassLoader = new File(jarInClassLoader.getName()); + File jarFileInParentClassLoader = new File(jarInParentClassLoader.getName()); + + jarDeletionHandler.setPaths(jarInClassLoader, jarInParentClassLoader, jarFileInClassLoader, jarFileInParentClassLoader); + + try + { + Boolean deleted = AccessController.doPrivileged(jarDeletionHandler); + ClassPathUtilities.canTerminate |= deleted; + return deleted; + } + catch (PrivilegedActionException ex) + { + ex.printStackTrace(); + } + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + + return false; + } + + public static void terminateRuntime(int status) + { + if (ClassPathUtilities.canTerminate) + { + System.exit(status); + } + else + { + throw new IllegalStateException(); + } + } + + /** + * @param classLoader + * @param fileName + * @param removeFromClassPath + * @throws MalformedURLException + */ + @SuppressWarnings("unchecked") + private static JarFile getJarFromClassLoader(URLClassLoader classLoader, String fileName, boolean removeFromClassPath) + throws MalformedURLException + { + JarFile jar = null; + + try + { + Object classPath = ClassPathUtilities.ucp.get(classLoader); + Map loaderMap = (Map)ClassPathUtilities.classPathLoaderMap.get(classPath); + + Iterator iter = loaderMap.entrySet().iterator(); + while (iter.hasNext()) + { + Entry loaderEntry = (Entry)iter.next(); + + String url = loaderEntry.getKey(); + + if (url.endsWith(fileName)) + { + Object loader = loaderEntry.getValue(); + Field jarField = loader.getClass().getDeclaredField("jar"); + jarField.setAccessible(true); + + jar = (JarFile)jarField.get(loader); + + if (removeFromClassPath) + { + jarField.set(loader, null); + + Stack urls = (Stack)ClassPathUtilities.classPathURLs.get(classPath); + ArrayList path = (ArrayList)ClassPathUtilities.classPathPath.get(classPath); + ArrayList loaders = (ArrayList)ClassPathUtilities.classPathLoaderList.get(classPath); + + loaders.remove(loader); + iter.remove(); + + URL jarURL = new URL(url); + urls.remove(jarURL); + path.remove(jarURL); + } + } + } + } + catch (IllegalArgumentException ex) {} + catch (SecurityException ex) {} + catch (IllegalAccessException ex) {} + catch (NoSuchFieldException ex) + { + ex.printStackTrace(); + } + + return jar; + } +} + +class JarDeletionHandler implements PrivilegedExceptionAction +{ + JarFile jarInClassLoader, jarInParentClassLoader; + File jarFileInClassLoader, jarFileInParentClassLoader; + + void setPaths(JarFile jarInClassLoader, JarFile jarInParentClassLoader, File jarFileInClassLoader, File jarFileInParentClassLoader) + { + this.jarInClassLoader = jarInClassLoader; + this.jarInParentClassLoader = jarInParentClassLoader; + this.jarFileInClassLoader = jarFileInClassLoader; + this.jarFileInParentClassLoader = jarFileInParentClassLoader; + } + + @Override + public Boolean run() throws Exception + { + this.jarInClassLoader.close(); + this.jarInParentClassLoader.close(); + + try + { + Thread.sleep(5000); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + + boolean deletedJarFile = this.jarFileInClassLoader.delete(); + boolean deletedParentJarFile = this.jarFileInParentClassLoader.delete(); + + System.err.println("deletedJarFile=" + deletedJarFile + " deletedParentJarFile=" + deletedParentJarFile); + + return Boolean.valueOf(deletedJarFile || deletedParentJarFile); + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/ClassTransformerManager.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/ClassTransformerManager.java new file mode 100644 index 00000000..c0c9b161 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/ClassTransformerManager.java @@ -0,0 +1,235 @@ +package com.mumfrey.liteloader.launch; + +import java.lang.reflect.Field; +import java.util.*; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; + +import net.minecraft.launchwrapper.IClassTransformer; +import net.minecraft.launchwrapper.LaunchClassLoader; +import net.minecraft.launchwrapper.LogWrapper; + +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +/** + * Manages injection of required and optional transformers + * + * @author Adam Mummery-Smith + */ +public class ClassTransformerManager +{ + /** + * Once the game is started we can no longer inject transformers + */ + private boolean gameStarted; + + /** + * Transformers to inject after preInit but before the game starts, + * necessary for anything that needs to be downstream of forge. + */ + private Set downstreamTransformers = new LinkedHashSet(); + + /** + * Transformers passed into the constructor which are required and must be + * injected upstream. + */ + private final List requiredTransformers; + + /** + * Transformers successfully injected by us + */ + private final Set injectedTransformers = new LinkedHashSet(); + + /** + * Catalogue of transformer startup failures + */ + private final Map> transformerStartupErrors = new HashMap>(); + + private Logger attachedLog; + + private String pendingTransformer; + + class ThrowableObserver extends AbstractAppender + { + public ThrowableObserver() + { + super("Throwable Observer", null, null); + this.start(); + } + + @Override + public void append(LogEvent event) + { + ClassTransformerManager.this.observeThrowable(event.getThrown()); + } + } + + /** + * @param requiredTransformers + */ + public ClassTransformerManager(List requiredTransformers) + { + this.requiredTransformers = requiredTransformers; + + this.appendObserver(); + } + + private void appendObserver() + { + try + { + Field fLogger = LogWrapper.class.getDeclaredField("myLog"); + fLogger.setAccessible(true); + this.attachedLog = (Logger)fLogger.get(LogWrapper.log); + if (this.attachedLog instanceof org.apache.logging.log4j.core.Logger) + { + ((org.apache.logging.log4j.core.Logger)this.attachedLog).addAppender(new ThrowableObserver()); + } + } + catch (Exception ex) + { + LiteLoaderLogger.warning("Failed to append ThrowableObserver to LogWrapper, transformer startup exceptions may not be logged"); + } + } + + /** + * @param transformerClass + */ + public boolean injectTransformer(String transformerClass) + { + if (!this.gameStarted) + { + this.downstreamTransformers.add(transformerClass); + return true; + } + + return false; + } + + /** + * @param transformerClasses + */ + public boolean injectTransformers(Collection transformerClasses) + { + if (!this.gameStarted) + { + this.downstreamTransformers.addAll(transformerClasses); + return true; + } + + return false; + } + + /** + * @param transformerClasses + */ + public boolean injectTransformers(String[] transformerClasses) + { + if (!this.gameStarted) + { + this.downstreamTransformers.addAll(Arrays.asList(transformerClasses)); + return true; + } + + return false; + } + + /** + * @param classLoader + */ + void injectUpstreamTransformers(LaunchClassLoader classLoader) + { + for (String requiredTransformerClassName : this.requiredTransformers) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Injecting required class transformer '%s'", requiredTransformerClassName); + this.injectTransformer(classLoader, requiredTransformerClassName); + } + } + + /** + * @param classLoader + */ + void injectDownstreamTransformers(LaunchClassLoader classLoader) + { + this.gameStarted = true; + + if (this.downstreamTransformers.size() > 0) + { + LiteLoaderLogger.info("Injecting downstream transformers"); + } + + for (String transformerClassName : this.downstreamTransformers) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Injecting additional class transformer class '%s'", transformerClassName); + this.injectTransformer(classLoader, transformerClassName); + } + + this.downstreamTransformers.clear(); + } + + private synchronized void injectTransformer(LaunchClassLoader classLoader, String transformerClassName) + { + try + { + // Assign pendingTransformer so that logged errors during transformer init can be put in the map + this.pendingTransformer = transformerClassName; + + // Register the transformer + classLoader.registerTransformer(transformerClassName); + + // Unassign pending transformer now init is completed + this.pendingTransformer = null; + + // Check whether the transformer was successfully injected, look for it in the transformer list + if (this.findTransformer(classLoader, transformerClassName) != null) + { + this.injectedTransformers.add(transformerClassName); + } + } + catch (Throwable th) + { + LiteLoaderLogger.severe(th, "Error injecting class transformer class %s", transformerClassName); + } + } + + public void observeThrowable(Throwable th) + { + if (th != null && this.pendingTransformer != null) + { + List transformerErrors = this.transformerStartupErrors.get(this.pendingTransformer); + if (transformerErrors == null) + { + transformerErrors = new ArrayList(); + this.transformerStartupErrors.put(this.pendingTransformer, transformerErrors); + } + transformerErrors.add(th); + } + } + + private IClassTransformer findTransformer(LaunchClassLoader classLoader, String transformerClassName) + { + for (IClassTransformer transformer : classLoader.getTransformers()) + { + if (transformer.getClass().getName().equals(transformerClassName)) + { + return transformer; + } + } + + return null; + } + + public Set getInjectedTransformers() + { + return Collections.unmodifiableSet(this.injectedTransformers); + } + + public List getTransformerStartupErrors(String transformerClassName) + { + List errorList = this.transformerStartupErrors.get(transformerClassName); + return errorList != null ? Collections.unmodifiableList(errorList) : null; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/GameEnvironment.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/GameEnvironment.java new file mode 100644 index 00000000..7482e754 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/GameEnvironment.java @@ -0,0 +1,28 @@ +package com.mumfrey.liteloader.launch; + +import java.io.File; + +public interface GameEnvironment +{ + /** + * Get the game directory, this is the root directory of the game profile + * specified by the user in the launcher. + */ + public abstract File getGameDirectory(); + + /** + * Get the assets directory + */ + public abstract File getAssetsDirectory(); + + /** + * Get the active profile name + */ + public abstract String getProfile(); + + /** + * Get the "mods" folder, used to get the base path for enumerators and + * config for legacy mods. + */ + public abstract File getModsFolder(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/InjectionStrategy.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/InjectionStrategy.java new file mode 100644 index 00000000..1dee4627 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/InjectionStrategy.java @@ -0,0 +1,145 @@ +package com.mumfrey.liteloader.launch; + +/** + * Encapsulates a strategy for injecting a URL into the classpath + * + * @author Adam Mummery-Smith + */ +public final class InjectionStrategy +{ + /** + * Defines a position for a classpath injection strategy + * + * @author Adam Mummery-Smith + */ + public enum InjectionPosition + { + /** + * Inject the URL at the bottom (end) of the classpath, lowest priority + * - this is the default. + */ + Bottom, + + /** + * Inject the URL at the base of the classpath (directly above the + * minecraft jar but below all other libs). + */ + Base, + + /** + * Inject the URL at the top (start) of the classpath, highest priority + * above all other libs. + */ + Top, + + /** + * Inject the URL above the entry which matches the URL defined by param + */ + Above; + + /** + * Parse an InjectionPosition from the specified "injectAt" string + * + * @param injectAt + */ + public static InjectionPosition parsePosition(String injectAt) + { + if ("top".equalsIgnoreCase(injectAt)) return InjectionPosition.Top; + if ("base".equalsIgnoreCase(injectAt)) return InjectionPosition.Base; + if (injectAt != null && injectAt.toLowerCase().startsWith("above:")) return InjectionPosition.Above; + return InjectionPosition.Bottom; + } + + /** + * Parse InjectionPosition params from the specified "injectAt" string + * + * @param injectAt + */ + public String[] parseParams(String injectAt) + { + if (this == InjectionPosition.Above && injectAt != null) return injectAt.substring(6).split(","); + return null; + } + } + + /** + * Top strategy + */ + public static final InjectionStrategy TOP = new InjectionStrategy(InjectionPosition.Top, null); + + /** + * Default strategy + */ + public static final InjectionStrategy DEFAULT = new InjectionStrategy(InjectionPosition.Bottom, null); + + /** + * Position for this strategy + */ + private final InjectionPosition position; + + /** + * Params for the strategy (if supported by the specified position) + */ + private final String[] params; + + /** + * Private constructor because strategy should be created from a string + * using parseStrategy() + * + * @param injectAt + */ + private InjectionStrategy(String injectAt) + { + this.position = InjectionPosition.parsePosition(injectAt); + this.params = this.position.parseParams(injectAt); + } + + /** + * Private constructor for the pre-defined public strategies TOP and DEFAULT + * + * @param position + * @param params + */ + private InjectionStrategy(InjectionPosition position, String[] params) + { + this.position = position; + this.params = params; + } + + /** + * Get the position + */ + public InjectionPosition getPosition() + { + return this.position; + } + + /** + * Get the parameters + */ + public String[] getParams() + { + return this.params; + } + + /** + * Parse an injection strategy from the specified injectAt string + * + * @param injectAt + */ + public static InjectionStrategy parseStrategy(String injectAt) + { + return InjectionStrategy.parseStrategy(injectAt, null); + } + + /** + * Parse an injection strategy from the specified injectAt string + * + * @param injectAt + */ + public static InjectionStrategy parseStrategy(String injectAt, InjectionStrategy defaultStrategy) + { + if (injectAt == null) return defaultStrategy; + return new InjectionStrategy(injectAt); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/InvalidTransformerException.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/InvalidTransformerException.java new file mode 100644 index 00000000..83a40965 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/InvalidTransformerException.java @@ -0,0 +1,25 @@ +package com.mumfrey.liteloader.launch; + +/** + * Exception thrown from the NonDelegatingClassLoader if a transformer tries to + * access a class outside of the classes that are allowed for that transformer. + * + * @author Adam Mummery-Smith + */ +public class InvalidTransformerException extends ClassNotFoundException +{ + private static final long serialVersionUID = 6723030540814568734L; + + private final String accessedClass; + + public InvalidTransformerException(String accessedClass) + { + super("Tried to access " + accessedClass); + this.accessedClass = accessedClass; + } + + public String getAccessedClass() + { + return this.accessedClass; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTransformer.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTransformer.java new file mode 100644 index 00000000..a8c12ca7 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTransformer.java @@ -0,0 +1,65 @@ +package com.mumfrey.liteloader.launch; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.ClassTransformer; + +public class LiteLoaderTransformer extends ClassTransformer +{ + private static final String LITELOADER_TWEAKER_CLASS = LiteLoaderTweaker.class.getName().replace('.', '/'); + + private static final String METHOD_PRE_BEGIN_GAME = "preBeginGame"; + + @Override + public byte[] transform(String name, String transformedName, byte[] basicClass) + { + if (basicClass == null) return basicClass; + + if (Obf.MinecraftMain.name.equals(transformedName)) + { + return this.transformMain(basicClass); + } + else if (Obf.Blocks.obf.equals(transformedName) + || Obf.Blocks.name.equals(transformedName) + || Obf.Items.obf.equals(transformedName) + || Obf.Items.name.equals(transformedName)) + { + return this.stripFinalModifiers(basicClass); + } + + return basicClass; + } + + private byte[] transformMain(byte[] basicClass) + { + ClassNode classNode = this.readClass(basicClass, true); + + for (MethodNode method : classNode.methods) + { + if ("main".equals(method.name)) + { + method.instructions.insert(new MethodInsnNode(Opcodes.INVOKESTATIC, LiteLoaderTransformer.LITELOADER_TWEAKER_CLASS, + LiteLoaderTransformer.METHOD_PRE_BEGIN_GAME, "()V", false)); + } + } + + return this.writeClass(classNode); + } + + private byte[] stripFinalModifiers(byte[] basicClass) + { + ClassNode classNode = this.readClass(basicClass, true); + + for (FieldNode field : classNode.fields) + { + field.access = field.access & ~Opcodes.ACC_FINAL; + } + + return this.writeClass(classNode); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTweaker.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTweaker.java new file mode 100644 index 00000000..9c6c58ec --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTweaker.java @@ -0,0 +1,692 @@ +package com.mumfrey.liteloader.launch; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.spongepowered.asm.launch.MixinBootstrap; + +import net.minecraft.launchwrapper.ITweaker; +import net.minecraft.launchwrapper.Launch; +import net.minecraft.launchwrapper.LaunchClassLoader; + +import com.google.common.base.Preconditions; +import com.mumfrey.liteloader.launch.LoaderEnvironment.EnvironmentType; +import com.mumfrey.liteloader.transformers.event.EventInfo; +import com.mumfrey.liteloader.util.SortableValue; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +/** + * LiteLoader tweak class + * + * @author Adam Mummery-Smith + */ +public class LiteLoaderTweaker implements ITweaker +{ + public static final int ENV_TYPE_CLIENT = 0; + public static final int ENV_TYPE_DEDICATEDSERVER = 1; + + // TODO Version - 1.8 + public static final String VERSION = "1.8"; + + protected static final String bootstrapClassName = "com.mumfrey.liteloader.core.LiteLoaderBootstrap"; + + /** + * Loader startup state + * + * @author Adam Mummery-Smith + */ + enum StartupState + { + PREPARE, + PREINIT, + BEGINGAME, + INIT, + POSTINIT, + DONE; + + /** + * Current state + */ + private static StartupState currentState = StartupState.PREPARE.gotoState(); + + /** + * Whether this state is active + */ + private boolean inState; + + /** + * Whether this state is completed (can go to next state) + */ + private boolean completed; + + /** + * Get whether this state is completed + */ + public boolean isCompleted() + { + return this.completed; + } + + /** + * Get whether the tweaker is currrently in this state + */ + public boolean isInState() + { + return this.inState; + } + + /** + * Go to the next state, checks whether can move to the next state + * (previous state is marked completed) first + */ + public StartupState gotoState() + { + for (StartupState otherState : StartupState.values()) + { + if (otherState.isInState() && otherState != this) + { + if (otherState.canGotoState(this)) + { + otherState.leaveState(); + } + else + { + String message = String.format("Cannot go to state <%s> as %s %s", this.name(), otherState, + otherState.getNextState() == this ? "" : "and expects \"" + otherState.getNextState().name() + "\" instead"); + throw new IllegalStateException(message, LiteLoaderLogger.getLastThrowable()); + } + } + } + + LiteLoaderLogger.clearLastThrowable(); + StartupState.currentState = this; + + this.inState = true; + this.completed = false; + + return this; + } + + /* (non-Javadoc) + * @see java.lang.Enum#toString() + */ + @Override + public String toString() + { + return String.format("<%s> is %s %s", this.name(), + this.inState ? "[ACTIVE]" : "[INACTIVE]", + this.completed ? "and [COMPLETED]" : "but [INCOMPLETE]"); + } + + /** + * + */ + public void leaveState() + { + this.inState = false; + } + + /** + * + */ + public void completed() + { + if (!this.inState || this.completed) + { + String message = String.format("Attempted to complete state %s but the state is already completed or is not active", this.name()); + throw new IllegalStateException(message, LiteLoaderLogger.getLastThrowable()); + } + + this.completed = true; + } + + /** + * Get the state which follows this state + */ + private StartupState getNextState() + { + return this.ordinal() < StartupState.values().length - 1 ? StartupState.values()[this.ordinal() + 1] : StartupState.DONE; + } + + /** + * @param next + */ + public boolean canGotoState(StartupState next) + { + if (this.inState && next == this.getNextState()) + { + return this.completed; + } + + return !this.inState; + } + + /** + * Get the current state + */ + public static StartupState getCurrent() + { + return StartupState.currentState; + } + } + + /** + * Singleton instance, mainly for delegating from injected callbacks which + * need a static method to call. + */ + protected static LiteLoaderTweaker instance; + + /** + * Approximate location of the minecraft jar, used for "base" injection + * position in ClassPathUtilities. + */ + protected static URL jarUrl; + + /** + * "Order" value for inserted tweakers, used as disambiguating sort criteria + * for injected tweakers which have the same priority. + */ + protected int tweakOrder = 0; + + /** + * All tweakers, used to avoid injecting duplicates + */ + protected Set allCascadingTweaks = new HashSet(); + + /** + * Sorted list of tweakers, used to sort tweakers before injecting + */ + protected Set> sortedCascadingTweaks = new TreeSet>(); + + /** + * True if this is the primary tweak, not known until at least PREJOINGAME + */ + protected boolean isPrimary; + + /** + * Startup environment information, used to store info about the current + * startup in one place, also handles parsing command line arguments. + */ + protected StartupEnvironment env; + + /** + * Loader bootstrap object + */ + protected LoaderBootstrap bootstrap; + + /** + * Transformer manager + */ + protected ClassTransformerManager transformerManager; + + /* (non-Javadoc) + * @see net.minecraft.launchwrapper.ITweaker + * #acceptOptions(java.util.List, java.io.File, java.io.File, + * java.lang.String) + */ + @Override + public void acceptOptions(List args, File gameDirectory, File assetsDirectory, String profile) + { + LiteLoaderTweaker.injectTweakClass("org.spongepowered.asm.launch.MixinTweaker"); + + Launch.classLoader.addClassLoaderExclusion("org.apache."); + Launch.classLoader.addClassLoaderExclusion("com.google.common."); + Launch.classLoader.addClassLoaderExclusion("org.objectweb.asm."); + LiteLoaderTweaker.instance = this; + + this.onPrepare(args, gameDirectory, assetsDirectory, profile); + + this.onPreInit(); + } + + /* (non-Javadoc) + * @see net.minecraft.launchwrapper.ITweaker + * #injectIntoClassLoader( + * net.minecraft.launchwrapper.LaunchClassLoader) + */ + @Override + public void injectIntoClassLoader(LaunchClassLoader classLoader) + { +// classLoader.addClassLoaderExclusion("com.mumfrey.liteloader.core.runtime.Obf"); +// classLoader.addClassLoaderExclusion("com.mumfrey.liteloader.core.runtime.Packets"); + + this.transformerManager.injectUpstreamTransformers(classLoader); + + for (String transformerClassName : this.bootstrap.getRequiredDownstreamTransformers()) + { + LiteLoaderLogger.info("Queuing required class transformer '%s'", transformerClassName); + this.transformerManager.injectTransformer(transformerClassName); + } + } + + /* (non-Javadoc) + * @see net.minecraft.launchwrapper.ITweaker#getLaunchTarget() + */ + @Override + public String getLaunchTarget() + { + this.isPrimary = true; + this.onPreBeginGame(); + + return "net.minecraft.client.main.Main"; + } + + /* (non-Javadoc) + * @see net.minecraft.launchwrapper.ITweaker#getLaunchArguments() + */ + @Override + public String[] getLaunchArguments() + { + return this.env.getLaunchArguments(); + } + + /** + * Return true if this is the primary tweaker + */ + public boolean isPrimary() + { + return this.isPrimary; + } + + /** + * Get the class transformer manager + */ + public ClassTransformerManager getTransformerManager() + { + return this.transformerManager; + } + + /** + * @param args + * @param gameDirectory + * @param assetsDirectory + * @param profile + */ + private void onPrepare(List args, File gameDirectory, File assetsDirectory, String profile) + { + LiteLoaderLogger.info(Verbosity.REDUCED, "Bootstrapping LiteLoader " + LiteLoaderTweaker.VERSION); + + try + { + this.initEnvironment(args, gameDirectory, assetsDirectory, profile); + + this.bootstrap = this.spawnBootstrap(LiteLoaderTweaker.bootstrapClassName, Launch.classLoader); + + this.transformerManager = new ClassTransformerManager(this.bootstrap.getRequiredTransformers()); + + StartupState.PREPARE.completed(); + } + catch (Throwable th) + { + LiteLoaderLogger.severe(th, "Error during LiteLoader PREPARE: %s %s", th.getClass().getName(), th.getMessage()); + } + } + + /** + * Do the first stage of loader startup, which enumerates mod sources and + * finds tweakers. + */ + private void onPreInit() + { + StartupState.PREINIT.gotoState(); + + try + { + MixinBootstrap.init(); + + this.bootstrap.preInit(Launch.classLoader, true, this.env.getModFilterList()); + + this.injectDiscoveredTweakClasses(); + StartupState.PREINIT.completed(); + } + catch (Throwable th) + { + LiteLoaderLogger.severe(th, "Error during LiteLoader PREINIT: %s %s", th.getClass().getName(), th.getMessage()); + } + } + + /** + * + */ + private void onPreBeginGame() + { + if (StartupState.BEGINGAME.isCompleted()) + { + return; + } + + StartupState.BEGINGAME.gotoState(); + try + { + this.transformerManager.injectDownstreamTransformers(Launch.classLoader); + this.bootstrap.preBeginGame(); + MixinBootstrap.addProxy(); + StartupState.BEGINGAME.completed(); + } + catch (Throwable th) + { + LiteLoaderLogger.severe(th, "Error during LiteLoader BEGINGAME: %s %s", th.getClass().getName(), th.getMessage()); + } + } + + /** + * Do the second stage of loader startup + */ + private void onInit() + { + StartupState.INIT.gotoState(); + + try + { + this.bootstrap.init(); + StartupState.INIT.completed(); + } + catch (Throwable th) + { + LiteLoaderLogger.severe(th, "Error during LiteLoader INIT: %s %s", th.getClass().getName(), th.getMessage()); + } + } + + /** + * Do the second stage of loader startup + */ + private void onPostInit() + { + StartupState.POSTINIT.gotoState(); + + try + { + this.bootstrap.postInit(); + StartupState.POSTINIT.completed(); + + StartupState.DONE.gotoState(); + } + catch (Throwable th) + { + LiteLoaderLogger.severe(th, "Error during LiteLoader POSTINIT: %s %s", th.getClass().getName(), th.getMessage()); + } + } + + /** + * Set up the startup environment + * + * @param args + * @param gameDirectory + * @param assetsDirectory + * @param profile + */ + private void initEnvironment(List args, File gameDirectory, File assetsDirectory, String profile) + { + this.env = this.spawnStartupEnvironment(args, gameDirectory, assetsDirectory, profile); + + URL[] urls = Launch.classLoader.getURLs(); + LiteLoaderTweaker.jarUrl = urls[urls.length - 1]; // probably? + } + + /** + * Injects discovered tweak classes + */ + private void injectDiscoveredTweakClasses() + { + if (this.sortedCascadingTweaks.size() > 0) + { + if (StartupState.getCurrent() != StartupState.PREINIT || !StartupState.PREINIT.isInState()) + { + LiteLoaderLogger.warning("Failed to inject cascaded tweak classes because preInit is already complete"); + return; + } + + LiteLoaderLogger.info("Injecting cascaded tweakers..."); + + List tweakClasses = LiteLoaderTweaker.getTweakClasses(); + List tweakers = LiteLoaderTweaker.getTweakers(); + if (tweakClasses != null && tweakers != null) + { + for (SortableValue tweak : this.sortedCascadingTweaks) + { + String tweakClass = tweak.getValue(); + LiteLoaderLogger.info(Verbosity.REDUCED, "Injecting tweak class %s with priority %d", tweakClass, tweak.getPriority()); + LiteLoaderTweaker.injectTweakClass(tweakClass, tweakClasses, tweakers); + } + } + + // Clear sortedTweaks but not allTweaks + this.sortedCascadingTweaks.clear(); + } + } + + private static boolean injectTweakClass(String tweakClass) + { + List tweakClasses = LiteLoaderTweaker.getTweakClasses(); + List tweakers = LiteLoaderTweaker.getTweakers(); + return LiteLoaderTweaker.injectTweakClass(tweakClass, tweakClasses, tweakers); + } + + /** + * @param tweakClass + * @param tweakClasses + * @param tweakers + */ + private static boolean injectTweakClass(String tweakClass, List tweakClasses, List tweakers) + { + if (tweakClasses.contains(tweakClass)) + { + return false; + } + + for (ITweaker existingTweaker : tweakers) + { + if (tweakClass.equals(existingTweaker.getClass().getName())) + { + return false; + } + } + + tweakClasses.add(tweakClass); + return true; + } + + /** + * @param tweakClass + * @param priority + */ + public boolean addCascadedTweaker(String tweakClass, int priority) + { + if (tweakClass != null && !this.allCascadingTweaks.contains(tweakClass)) + { + if (this.getClass().getName().equals(tweakClass)) + { + return false; + } + + if (LiteLoaderTweaker.isTweakAlreadyEnqueued(tweakClass)) + { + return false; + } + + this.allCascadingTweaks.add(tweakClass); + this.sortedCascadingTweaks.add(new SortableValue(priority, this.tweakOrder++, tweakClass)); + return true; + } + + return false; + } + + /** + * The bootstrap object has to be spawned using reflection for obvious + * reasons + * + * @param bootstrapClassName + * @param classLoader + */ + protected LoaderBootstrap spawnBootstrap(String bootstrapClassName, ClassLoader classLoader) + { + if (!StartupState.PREPARE.isInState()) + { + throw new IllegalStateException("spawnBootstrap is not valid outside PREPARE"); + } + + try + { + @SuppressWarnings("unchecked") + Class bootstrapClass = (Class)Class.forName(bootstrapClassName, false, classLoader); + Constructor bootstrapCtor = bootstrapClass.getDeclaredConstructor(StartupEnvironment.class, ITweaker.class); + bootstrapCtor.setAccessible(true); + + return bootstrapCtor.newInstance(this.env, this); + } + catch (Throwable th) + { + throw new RuntimeException(th); + } + } + + /** + * @param args + * @param gameDirectory + * @param assetsDirectory + * @param profile + */ + protected StartupEnvironment spawnStartupEnvironment(List args, File gameDirectory, File assetsDirectory, String profile) + { + return new StartupEnvironment(args, gameDirectory, assetsDirectory, profile) + { + @Override + public void registerCoreAPIs(List apisToLoad) + { + apisToLoad.add(0, "com.mumfrey.liteloader.client.api.LiteLoaderCoreAPIClient"); + } + + @Override + public int getEnvironmentTypeId() + { + return LiteLoaderTweaker.ENV_TYPE_CLIENT; + } + }; + } + + /** + * Get the game jar url (probably) + */ + public static URL getJarUrl() + { + return LiteLoaderTweaker.jarUrl; + } + + /** + * @param url URL to add + */ + public static boolean addURLToParentClassLoader(URL url) + { + if (StartupState.getCurrent() == StartupState.PREINIT && StartupState.PREINIT.isInState()) + { + try + { + URLClassLoader classLoader = (URLClassLoader)Launch.class.getClassLoader(); + Method mAddUrl = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + mAddUrl.setAccessible(true); + mAddUrl.invoke(classLoader, url); + + return true; + } + catch (Exception ex) + { + LiteLoaderLogger.warning(ex, "addURLToParentClassLoader failed: %s", ex.getMessage()); + } + } + + return false; + } + + /** + * @param clazz + */ + private static boolean isTweakAlreadyEnqueued(String clazz) + { + List tweakClasses = LiteLoaderTweaker.getTweakClasses(); + List tweakers = LiteLoaderTweaker.getTweakers(); + + if (tweakClasses != null) + { + for (String tweakClass : tweakClasses) + { + if (tweakClass.equals(clazz)) return true; + } + } + + if (tweakers != null) + { + for (ITweaker tweaker : tweakers) + { + if (tweaker.getClass().getName().equals(clazz)) return true; + } + } + + return false; + } + + @SuppressWarnings("unchecked") + private static List getTweakClasses() + { + return Preconditions.>checkNotNull((List)Launch.blackboard.get("TweakClasses"), "TweakClasses"); + } + + @SuppressWarnings("unchecked") + private static List getTweakers() + { + return Preconditions.>checkNotNull((List)Launch.blackboard.get("Tweaks"), "Tweaks"); + } + + /** + * Get whether to enable the loading bar for minecraft startup + */ + public static boolean loadingBarEnabled() + { + LoaderProperties properties = LiteLoaderTweaker.instance.bootstrap.getProperties(); + return properties != null && properties.getBooleanProperty("loadingbar"); + } + + /** + * Callback from the "Main" class, do the PREBEGINGAME steps (inject + * "downstream" transformers) + */ + public static void preBeginGame() + { + LiteLoaderTweaker.instance.onPreBeginGame(); + } + + /** + * Callback from Minecraft::startGame() do early mod initialisation + */ + public static void init() + { + LiteLoaderTweaker.instance.onInit(); + } + + /** + * Callback from Minecraft::startGame() do late mod initialisation + */ + public static void postInit() + { + LiteLoaderTweaker.instance.onPostInit(); + } + + public static void dedicatedServerInit(EventInfo e) + { + LiteLoaderTweaker.instance.onInit(); + LiteLoaderTweaker.instance.onPostInit(); + } + + public static EnvironmentType getEnvironmentType() + { + return LiteLoaderTweaker.instance.bootstrap.getEnvironment().getType(); + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTweakerServer.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTweakerServer.java new file mode 100644 index 00000000..d88e5821 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LiteLoaderTweakerServer.java @@ -0,0 +1,42 @@ +package com.mumfrey.liteloader.launch; + +import java.io.File; +import java.util.List; + +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +public class LiteLoaderTweakerServer extends LiteLoaderTweaker +{ + public LiteLoaderTweakerServer() + { + LiteLoaderLogger.verbosity = Verbosity.REDUCED; + } + + @Override + protected StartupEnvironment spawnStartupEnvironment(List args, File gameDirectory, File assetsDirectory, String profile) + { + return new StartupEnvironment(args, gameDirectory, assetsDirectory, profile) + { + @Override + public void registerCoreAPIs(List apisToLoad) + { + apisToLoad.add(0, "com.mumfrey.liteloader.server.api.LiteLoaderCoreAPIServer"); + } + + @Override + public int getEnvironmentTypeId() + { + return LiteLoaderTweaker.ENV_TYPE_DEDICATEDSERVER; + } + }; + } + + @Override + public String getLaunchTarget() + { + super.getLaunchTarget(); + + return "net.minecraft.server.MinecraftServer"; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderBootstrap.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderBootstrap.java new file mode 100644 index 00000000..99e81283 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderBootstrap.java @@ -0,0 +1,48 @@ +package com.mumfrey.liteloader.launch; + +import java.util.List; + +import net.minecraft.launchwrapper.LaunchClassLoader; + +/** + * Interface for the loader bootstrap, this is loaded in the parent classloader + * for convenience otherwise it would be necessary to call the initialisation + * functions using reflection which just gets boring very quickly. + * + * @author Adam Mummery-Smith + */ +public interface LoaderBootstrap +{ + /** + * Pre-init, perform mod file discovery and initial setup (eg. logger, + * properties) + * + * @param classLoader + * @param loadTweaks + * @param modsToLoad + */ + public abstract void preInit(LaunchClassLoader classLoader, boolean loadTweaks, List modsToLoad); + + /** + * + */ + public abstract void preBeginGame(); + + /** + * Init, create the loader instance and load mods + */ + public abstract void init(); + + /** + * Post-init, initialise loaded mods + */ + public abstract void postInit(); + + public abstract List getRequiredTransformers(); + + public abstract List getRequiredDownstreamTransformers(); + + public abstract LoaderEnvironment getEnvironment(); + + public abstract LoaderProperties getProperties(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderEnvironment.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderEnvironment.java new file mode 100644 index 00000000..f0feb77e --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderEnvironment.java @@ -0,0 +1,83 @@ +package com.mumfrey.liteloader.launch; + +import java.io.File; + +import com.mumfrey.liteloader.api.manager.APIAdapter; +import com.mumfrey.liteloader.api.manager.APIProvider; +import com.mumfrey.liteloader.core.EnabledModsList; +import com.mumfrey.liteloader.core.LiteLoaderVersion; +import com.mumfrey.liteloader.interfaces.LoaderEnumerator; + +/** + * The Loader Environment, contains accessors for getting information about the + * current Loader session such as the game directories, profile, and API + * management classes. + * + * Launch namespace, so loaded by the AppClassLoader + * + * @author Adam Mummery-Smith + */ +public interface LoaderEnvironment extends GameEnvironment +{ + public enum EnvironmentType + { + CLIENT, + DEDICATEDSERVER + } + + public abstract EnvironmentType getType(); + + /** + * Get the API Adapter, the API Adapter provides functionality for working + * with all loaded APIs. + */ + public abstract APIAdapter getAPIAdapter(); + + /** + * Get the API Provider, the API Provider contains API instances for the + * current session. + */ + public abstract APIProvider getAPIProvider(); + + /** + * The enabled mods list is a serialisable class which contains information + * about which mods are enabled/disabled. + */ + public abstract EnabledModsList getEnabledModsList(); + + /** + * The enumerator manages mod container and class discovery + */ + public abstract LoaderEnumerator getEnumerator(); + + /** + * Get the version-specific mods folder + */ + public abstract File getVersionedModsFolder(); + + /** + * Get the configuration base folder + */ + public abstract File getConfigBaseFolder(); + + /** + * Get the version-agnostic mod config folder + */ + public abstract File getCommonConfigFolder(); + + /** + * Get the version-specific config folder + */ + public abstract File getVersionedConfigFolder(); + + /** + * Inflect a versioned configuration path for a specific version + * + * @param version + */ + public abstract File inflectVersionedConfigPath(LiteLoaderVersion version); + + public abstract boolean addCascadedTweaker(String tweakClass, int priority); + + public abstract ClassTransformerManager getTransformerManager(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderProperties.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderProperties.java new file mode 100644 index 00000000..c31409d0 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/LoaderProperties.java @@ -0,0 +1,103 @@ +package com.mumfrey.liteloader.launch; + +/** + * Interface for the object which will manage loader properties (internal and + * volatile). + * + * @author Adam Mummery-Smith + */ +public interface LoaderProperties +{ + /** + * True if the "load tweaks" option is enabled and enumerator modules + */ + public abstract boolean loadTweaksEnabled(); + + /** + * Get the mod pack branding from the non-volatile store + */ + public abstract String getBranding(); + + /** + * Set a boolean property in the properties file + * + * @param propertyName + * @param value + */ + public abstract void setBooleanProperty(String propertyName, boolean value); + + /** + * Get a boolean property from the properties file + * + * @param propertyName + */ + public abstract boolean getBooleanProperty(String propertyName); + + /** + * Get a boolean property but write and return the supplied default value if + * the property doesn't exist + * + * @param propertyName + * @param defaultValue + */ + public abstract boolean getAndStoreBooleanProperty(String propertyName, boolean defaultValue); + + /** + * Set an integer property in the properties file + * + * @param propertyName + * @param value + */ + public abstract void setIntegerProperty(String propertyName, int value); + + /** + * Get an integer property from the properties file + * + * @param propertyName + */ + public abstract int getIntegerProperty(String propertyName); + + /** + * Get an integer property but write and return the supplied default value + * if the property doesn't exist + * + * @param propertyName + * @param defaultValue + */ + public abstract int getAndStoreIntegerProperty(String propertyName, int defaultValue); + + /** + * Get a stored mod revision number from the properties file + * + * @param modKey + */ + public abstract int getLastKnownModRevision(String modKey); + + /** + * Store a mod revision number in the properties file + * + * @param modKey + */ + public abstract void storeLastKnownModRevision(String modKey); + + /** + * Write the properties to disk + */ + public abstract void writeProperties(); + + // General properties + public static final String OPTION_SOUND_MANAGER_FIX = "soundManagerFix"; + public static final String OPTION_MOD_INFO_SCREEN = "modInfoScreen"; + public static final String OPTION_NO_HIDE_TAB = "tabAlwaysExpanded"; + public static final String OPTION_BRAND = "brand"; + public static final String OPTION_LOADING_BAR = "loadingbar"; + public static final String OPTION_FORCE_UPDATE = "allowForceUpdate"; + public static final String OPTION_UPDATE_CHECK_INTR = "updateCheckInterval"; + public static final String OPTION_JINPUT_DISABLE = "disableJInput"; + + // Enumerator properties + public static final String OPTION_SEARCH_MODS = "search.mods"; + public static final String OPTION_SEARCH_CLASSPATH = "search.classpath"; + public static final String OPTION_SEARCH_JARFILES = "search.jarfiles"; + public static final String OPTION_FORCE_INJECTION = "forceInjection"; +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/NonDelegatingClassLoader.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/NonDelegatingClassLoader.java new file mode 100644 index 00000000..b6bc2aec --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/NonDelegatingClassLoader.java @@ -0,0 +1,145 @@ +package com.mumfrey.liteloader.launch; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashSet; +import java.util.Set; + +/** + * ClassLoader which only allows whitelisted classes to be loaded, used to + * pre-load packet transformer classes to ensure that they don't reference any + * external classes. + * + * @author Adam Mummery-Smith + */ +public class NonDelegatingClassLoader extends URLClassLoader +{ + /** + * Class names which we can load with this loader + */ + private final Set validClassNames = new HashSet(); + + /** + * Packages which we can load with this loader + */ + private final Set validPackages = new HashSet(); + + /** + * Class names which will be forcibly delegated to the parent ClassLoader + */ + private final Set delegatedClassNames = new HashSet(); + + /** + * Package names which will be forcibly delegated to the parent ClassLoader + */ + private final Set delegatedPackages = new HashSet(); + + private final ClassLoader parent; + + private boolean valid = true; + + private String invalidClassName = null; + + NonDelegatingClassLoader(URL[] urls, ClassLoader parent) + { + super(urls, null); + + this.parent = parent; + + this.validClassNames.add("java.lang.Object"); + this.validPackages.add("java."); + } + + public boolean isValid() + { + return this.valid; + } + + public String getInvalidClassName() + { + return this.invalidClassName; + } + + public void reset() + { + this.valid = true; + this.invalidClassName = null; + } + + public void addValidClassName(String className) + { + this.validClassNames.add(className); + } + + public void addValidPackage(String packageName) + { + if (!packageName.endsWith(".")) packageName += "."; + this.validPackages.add(packageName); + } + + public void addDelegatedClassName(String className) + { + this.delegatedClassNames.add(className); + this.validClassNames.add(className); + } + + public void addDelegatedPackage(String packageName) + { + if (!packageName.endsWith(".")) packageName += "."; + this.delegatedPackages.add(packageName); + this.validPackages.add(packageName); + } + + public Class addAndLoadClass(String name) throws ClassNotFoundException + { + this.reset(); + this.addValidClassName(name); + return this.loadClass(name); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException + { + if (this.parent != null) + { + if (this.delegatedClassNames.contains(name)) + { + return this.parent.loadClass(name); + } + + for (String delegatedPackage : this.delegatedPackages) + { + if (name.startsWith(delegatedPackage)) + { + return this.parent.loadClass(name); + } + } + } + + return super.loadClass(name); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException + { + if (name == null) return null; + + if (this.validClassNames.contains(name)) + { + return super.findClass(name); + } + + for (String validPackage : this.validPackages) + { + if (name.startsWith(validPackage)) + { + return super.findClass(name); + } + } + + this.valid = false; + this.invalidClassName = name; + + return super.findClass(name); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/launch/StartupEnvironment.java b/liteloader/src/main/java/com/mumfrey/liteloader/launch/StartupEnvironment.java new file mode 100644 index 00000000..23429e81 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/launch/StartupEnvironment.java @@ -0,0 +1,235 @@ +package com.mumfrey.liteloader.launch; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import net.minecraft.launchwrapper.Launch; +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.NonOptionArgumentSpec; +import joptsimple.OptionParser; +import joptsimple.OptionSet; + +/** + * Container for startup environment state which also parses the command line + * options. + * + * @author Adam Mummery-Smith + */ +public abstract class StartupEnvironment implements GameEnvironment +{ + private List singularLaunchArgs = new ArrayList(); + private Map launchArgs; + + private ArgumentAcceptingOptionSpec modsDirOption; + private ArgumentAcceptingOptionSpec modsOption; + private ArgumentAcceptingOptionSpec apisOption; + private NonOptionArgumentSpec unparsedOptions; + private OptionSet parsedOptions; + + private final File gameDirectory; + private final File assetsDirectory; + private final String profile; + + public StartupEnvironment(List args, File gameDirectory, File assetsDirectory, String profile) + { + this.gameDirectory = gameDirectory; + this.assetsDirectory = assetsDirectory; + this.profile = profile; + + this.initArgs(args); + } + + public abstract void registerCoreAPIs(List apisToLoad); + + public abstract int getEnvironmentTypeId(); + + /** + * @param args + */ + @SuppressWarnings("unchecked") + public void initArgs(List args) + { + // Get the launchArgs map from the blackboard, or create it if it's not there + this.launchArgs = (Map)Launch.blackboard.get("launchArgs"); + if (this.launchArgs == null) + { + this.launchArgs = new HashMap(); + Launch.blackboard.put("launchArgs", this.launchArgs); + } + + // Parse liteloader options using joptsimple + this.parseOptions(args.toArray(new String[args.size()])); + + // Parse out the arguments ourself because joptsimple doesn't really provide a good way to + // add arguments to the unparsed argument list after parsing + this.parseArgs(this.parsedOptions.valuesOf(this.unparsedOptions)); + + // Put required arguments to the blackboard if they don't already exist there + this.provideRequiredArgs(); + } + + private void parseOptions(String[] args) + { + OptionParser optionParser = new OptionParser(); + optionParser.allowsUnrecognizedOptions(); + + this.modsOption = optionParser.accepts("mods", "Comma-separated list of mods to load") + .withRequiredArg().ofType(String.class).withValuesSeparatedBy(','); + this.apisOption = optionParser.accepts("api", "Additional API classes to load") + .withRequiredArg().ofType(String.class); + this.modsDirOption = optionParser.accepts("modsDir", "Path to 'mods' folder to use instead of default") + .withRequiredArg().ofType(String.class); + + this.unparsedOptions = optionParser.nonOptions(); + this.parsedOptions = optionParser.parse(args); + } + + private void parseArgs(List args) + { + String classifier = null; + + for (String arg : args) + { + if (arg.startsWith("-")) + { + if (classifier != null) + { + this.addClassifiedArg(classifier, ""); + classifier = null; + } + else if (arg.contains("=")) + { + this.addClassifiedArg(arg.substring(0, arg.indexOf('=')), arg.substring(arg.indexOf('=') + 1)); + } + else + { + classifier = arg; + } + } + else + { + if (classifier != null) + { + this.addClassifiedArg(classifier, arg); + classifier = null; + } + else + { + this.singularLaunchArgs.add(arg); + } + } + } + + if (classifier != null) this.singularLaunchArgs.add(classifier); + } + + public void addClassifiedArg(String classifiedArg, String arg) + { + this.launchArgs.put(classifiedArg, arg); + } + + public void provideRequiredArgs() + { + if (this.launchArgs.get("--version") == null) + { + this.addClassifiedArg("--version", LiteLoaderTweaker.VERSION); + } + + if (this.launchArgs.get("--gameDir") == null && this.gameDirectory != null) + { + this.addClassifiedArg("--gameDir", this.gameDirectory.getAbsolutePath()); + } + + if (this.launchArgs.get("--assetsDir") == null && this.assetsDirectory != null) + { + this.addClassifiedArg("--assetsDir", this.assetsDirectory.getAbsolutePath()); + } + } + + public String[] getLaunchArguments() + { + List args = new ArrayList(); + + for (String singularArg : this.singularLaunchArgs) + args.add(singularArg); + + for (Entry launchArg : this.launchArgs.entrySet()) + { + args.add(launchArg.getKey().trim()); + args.add(launchArg.getValue().trim()); + } + + this.singularLaunchArgs.clear(); + this.launchArgs.clear(); + + return args.toArray(new String[args.size()]); + } + + /** + * Get the mod filter list + */ + public List getModFilterList() + { + return (this.parsedOptions.has(this.modsOption)) ? this.modsOption.values(this.parsedOptions) : null; + } + + /** + * Get API classes to load + */ + public List getAPIsToLoad() + { + List apisToLoad = new ArrayList(); + this.registerCoreAPIs(apisToLoad); + if (this.parsedOptions.has(this.apisOption)) + { + apisToLoad.addAll(this.apisOption.values(this.parsedOptions)); + } + + return apisToLoad; + } + + public File getOptionalDirectory(File baseDirectory, ArgumentAcceptingOptionSpec option, String defaultDir) + { + if (this.parsedOptions.has(option)) + { + String path = option.value(this.parsedOptions); + File dir = new File(path); + if (dir.isAbsolute()) + { + return dir; + } + + return new File(baseDirectory, path); + } + + return new File(baseDirectory, defaultDir); + } + + @Override + public File getGameDirectory() + { + return this.gameDirectory; + } + + @Override + public File getAssetsDirectory() + { + return this.assetsDirectory; + } + + @Override + public String getProfile() + { + return this.profile; + } + + @Override + public File getModsFolder() + { + return this.getOptionalDirectory(this.gameDirectory, this.modsDirOption, "mods"); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/messaging/Message.java b/liteloader/src/main/java/com/mumfrey/liteloader/messaging/Message.java new file mode 100644 index 00000000..6e86d5c5 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/messaging/Message.java @@ -0,0 +1,184 @@ +package com.mumfrey.liteloader.messaging; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import com.google.common.collect.ImmutableMap; + +/** + * Class used to encapsulate a MessageBus message + * + * @author Adam Mummery-Smith + */ +public class Message +{ + /** + * Regex for matching valid channels + */ + private static final Pattern channelPattern = Pattern.compile("^[a-z0-9]([a-z0-9_\\-]*[a-z0-9])?:[a-z0-9]([a-z0-9_\\-]*[a-z0-9])?$", + Pattern.CASE_INSENSITIVE); + + private final String channel, replyChannel; + private final Messenger sender; + private final Map payload; + + Message(String channel, Object value, Messenger sender) + { + this(channel, value, sender, null); + } + + Message(String channel, Object value, Messenger sender, String replyChannel) + { + Message.validateChannel(channel); + + this.channel = channel; + this.payload = ImmutableMap.of("value", value); + this.sender = sender; + this.replyChannel = replyChannel; + } + + Message(String channel, Map payload, Messenger sender) + { + this(channel, payload, sender, null); + } + + Message(String channel, Map payload, Messenger sender, String replyChannel) + { + Message.validateChannel(channel); + + this.channel = channel; + this.payload = payload != null ? ImmutableMap.copyOf(payload) : ImmutableMap.of(); + this.sender = sender; + this.replyChannel = replyChannel; + } + + /** + * Get the channel (fully qualified) that this message was sent on + */ + public String getChannel() + { + return this.channel; + } + + /** + * Get the channel category for this message + */ + public String getCategory() + { + return this.channel.substring(0, this.channel.indexOf(':')); + } + + /** + * Get the specified reply channel (if any) for this message - may return + * null + */ + public String getReplyChannel() + { + return this.replyChannel; + } + + /** + * Get the message sender (if any) for this message - may return null + */ + public Messenger getSender() + { + return this.sender; + } + + /** + * Get the message payload + */ + public Map getPayload() + { + return this.payload; + } + + /** + * Check if this message is on the specified channel + * + * @param channel Full name of the channel to check against (case sensitive) + */ + public boolean isChannel(String channel) + { + return this.channel.equals(channel); + } + + /** + * Check if this message has the specified category + * + * @param category + */ + public boolean isCategory(String category) + { + return this.getCategory().equals(category); + } + + /** + * Get (and implicit cast) a value from this message's payload + * + * @param key + */ + @SuppressWarnings("unchecked") + public T get(String key) + { + return (T)this.payload.get(key); + } + + @SuppressWarnings("unchecked") + public T get(String key, T defaultValue) + { + Object value = this.payload.get(key); + if (value != null) + { + return (T)value; + } + return defaultValue; + } + + /** + * Gets the payload with the key "value", which is used with messages + * constructed using a string-only payload. + */ + public T getValue() + { + return this.get("value"); + } + + public static void validateChannel(String channel) throws IllegalArgumentException + { + if (channel == null) + { + throw new IllegalArgumentException("Channel name cannot be null"); + } + + if (!Message.isValidChannel(channel)) + { + throw new IllegalArgumentException("'" + channel + "' is not a valid channel name"); + } + } + + public static boolean isValidChannel(String channel) + { + return Message.channelPattern.matcher(channel).matches(); + } + + /** + * Build a KV map from interleaved keys and values, convenience function + * + * @param args + */ + public static Map buildMap(Object... args) + { + Map payload = new HashMap(); + for (int i = 0; i < args.length - 1; i += 2) + { + if (args[i] instanceof String) + { + payload.put((String)args[i], args[i + 1]); + } + } + + return payload; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/messaging/MessageBus.java b/liteloader/src/main/java/com/mumfrey/liteloader/messaging/MessageBus.java new file mode 100644 index 00000000..6c26412b --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/messaging/MessageBus.java @@ -0,0 +1,269 @@ +package com.mumfrey.liteloader.messaging; + +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.mumfrey.liteloader.api.InterfaceProvider; +import com.mumfrey.liteloader.api.Listener; +import com.mumfrey.liteloader.core.InterfaceRegistrationDelegate; +import com.mumfrey.liteloader.core.event.HandlerList; +import com.mumfrey.liteloader.interfaces.FastIterable; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Intra-mod messaging bus, allows mods to send arbitrary notifications to each + * other without having to create an explicit dependency or resort to reflection + * + * @author Adam Mummery-Smith + */ +public final class MessageBus implements InterfaceProvider +{ + /** + * Singleton + */ + private static MessageBus instance; + + /** + * Messengers subscribed to each channel + */ + private final Map> messengers = new HashMap>(); + + /** + * Pending messages dispatched pre-startup + */ + private final Deque messageQueue = new LinkedList(); + + private boolean enableMessaging = false; + + private MessageBus() + { + } + + /** + * Get the singleton instance + */ + public static MessageBus getInstance() + { + if (MessageBus.instance == null) + { + MessageBus.instance = new MessageBus(); + } + + return MessageBus.instance; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#getListenerBaseType() + */ + @Override + public Class getListenerBaseType() + { + return Listener.class; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider + * #registerInterfaces( + * com.mumfrey.liteloader.core.InterfaceRegistrationDelegate) + */ + @Override + public void registerInterfaces(InterfaceRegistrationDelegate delegate) + { + delegate.registerInterface(Messenger.class); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.api.InterfaceProvider#initProvider() + */ + @Override + public void initProvider() + { + } + + /** + * + */ + public void onStartupComplete() + { + this.enableMessaging = true; + + while (this.messageQueue.size() > 0) + { + Message msg = this.messageQueue.pop(); + this.dispatchMessage(msg); + } + } + + public void registerMessenger(Messenger messenger) + { + List messageChannels = messenger.getMessageChannels(); + if (messageChannels == null) + { + LiteLoaderLogger.warning("Listener %s returned a null channel list for getMessageChannels(), " + + "this could indicate a problem with the listener", messenger.getName()); + return; + } + + for (String channel : messageChannels) + { + if (channel != null && Message.isValidChannel(channel)) + { + LiteLoaderLogger.info("Listener %s is registering MessageBus channel %s", messenger.getName(), channel); + this.getMessengerList(channel).add(messenger); + } + else + { + LiteLoaderLogger.warning("Listener %s tried to register invalid MessageBus channel %s", messenger.getName(), channel); + } + } + } + + /** + * @param message + */ + private void sendMessage(Message message) + { + if (this.enableMessaging) + { + this.dispatchMessage(message); + return; + } + + this.messageQueue.push(message); + } + + /** + * @param message + */ + private void dispatchMessage(Message message) + { + try + { + FastIterable messengerList = this.messengers.get(message.getChannel()); + if (messengerList != null) + { + messengerList.all().receiveMessage(message); + } + } + catch (StackOverflowError err) + { + // A listener tried to reply on the same channel and ended up calling itself + throw new RuntimeException("Stack overflow encountered dispatching message on channel '" + + message.getChannel() + "'. Did you reply to yourself?"); + } + } + + /** + * Get messengers for the specified channel + * + * @param channel + */ + private FastIterable getMessengerList(String channel) + { + FastIterable messengerList = this.messengers.get(channel); + if (messengerList == null) + { + messengerList = new HandlerList(Messenger.class); + this.messengers.put(channel, messengerList); + } + + return messengerList; + } + + /** + * Send an empty message on the specified channel, this is useful for + * messages which are basically just notifications. + * + * @param channel + */ + public static void send(String channel) + { + Message message = new Message(channel, null, null); + MessageBus.getInstance().sendMessage(message); + } + + /** + * Send a message with a value on the specified channel + * + * @param channel + * @param value + */ + public static void send(String channel, String value) + { + Message message = new Message(channel, value, null); + MessageBus.getInstance().sendMessage(message); + } + + /** + * Send a message with a value on the specified channel from the specified + * sender. + * + * @param channel + * @param value + * @param sender + */ + public static void send(String channel, String value, Messenger sender) + { + Message message = new Message(channel, value, sender); + MessageBus.getInstance().sendMessage(message); + } + + /** + * Send a message with a value on the specified channel from the specified + * sender. + * + * @param channel + * @param value + * @param sender + * @param replyChannel + */ + public static void send(String channel, String value, Messenger sender, String replyChannel) + { + Message message = new Message(channel, value, sender, replyChannel); + MessageBus.getInstance().sendMessage(message); + } + + /** + * Send a message with a supplied payload on the specified channel + * + * @param channel + * @param payload + */ + public static void send(String channel, Map payload) + { + Message message = new Message(channel, payload, null); + MessageBus.getInstance().sendMessage(message); + } + + /** + * Send a message with a supplied payload on the specified channel from the + * specified sender. + * + * @param channel + * @param payload + * @param sender + */ + public static void send(String channel, Map payload, Messenger sender) + { + Message message = new Message(channel, payload, sender); + MessageBus.getInstance().sendMessage(message); + } + + /** + * Send a message with a supplied payload on the specified channel from the + * specified sender. + * + * @param channel + * @param payload + * @param sender + * @param replyChannel + */ + public static void send(String channel, Map payload, Messenger sender, String replyChannel) + { + Message message = new Message(channel, payload, sender, replyChannel); + MessageBus.getInstance().sendMessage(message); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/messaging/Messenger.java b/liteloader/src/main/java/com/mumfrey/liteloader/messaging/Messenger.java new file mode 100644 index 00000000..4793c3e8 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/messaging/Messenger.java @@ -0,0 +1,62 @@ +package com.mumfrey.liteloader.messaging; + +import java.util.List; + +import com.mumfrey.liteloader.api.Listener; + +/** + * Interface for listeners that want to receive (or send) + * + * @author Adam Mummery-Smith + */ +public interface Messenger extends Listener +{ + /** + *

      Get listening channels for this Messenger. Channel names must follow + * the format:

      + * + * {category}:{channel} + * + *

      where both {category} and {channel} are + * alpha-numeric identifiers which can contain underscore or dash but must + * begin and end with only alpha-numeric characters: for example the + * following channel names are valid: + * + *

        + *
      • foo:bar
      • + *
      • foo-bar:baz
      • + *
      • foo-bar:baz_derp
      • + *
      + * + *

      The following are invalid:

      + * + *
        + *
      • foo
      • + *
      • foo_:bar
      • + *
      • _foo:bar
      • + *
      + * + *

      In general, your listener should listen on channels all beginning with + * the same category, which may match your mod id. Channel names and + * categories are case-sensitive.

      + * + * @return List of channels to listen on + */ + public abstract List getMessageChannels(); + + /** + * Called when a message matching a channel you have elected to listen on is + * dispatched by any agent. WARNING this method is called if you + * dispatch a message on a channel you are listening to, thus you should + * avoid replying on channels you are listening to unless you + * specifically filter messages based on their sender: + * + * if (message.getSender() == this) return; + * + *

      Messages may have a null sender or payload but will never have a null + * channel.

      + * + * @param message + */ + public abstract void receiveMessage(Message message); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/AdvancedExposable.java b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/AdvancedExposable.java new file mode 100644 index 00000000..602ae24c --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/AdvancedExposable.java @@ -0,0 +1,39 @@ +package com.mumfrey.liteloader.modconfig; + +import java.io.File; + +import com.google.gson.GsonBuilder; + +/** + * Interface for Exposables which want a finer degree of control over the + * serialisation process. + * + * @author Adam Mummery-Smith + */ +public interface AdvancedExposable extends Exposable +{ + /** + * Allows this object to configure the GsonBuilder prior to the construction + * of the Gson instance. Use this callback to (for example) register custom + * type adapters or set other Gson options such as pretty printing. + * + * @param gsonBuilder + */ + public abstract void setupGsonSerialiser(GsonBuilder gsonBuilder); + + /** + * Allows this object to specify an alternative configuration file to the + * one determined by the writer, either return null or return configFile to + * keep the original setting, or return a new File object to set the + * location for the config file. If you specify an alternative location, you + * are responsible for ensuring that the location exists and is writable. + * + * @param configFile Default config file, generated by the ExposableOptions + * for this Exposable + * @param configFileLocation Default config file location, from the config + * strategy + * @param defaultFileName Default cfg file name, from the ExposableOptions + * @return config file location to return, return null to use the default + */ + public abstract File getConfigFile(File configFile, File configFileLocation, String defaultFileName); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigManager.java b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigManager.java new file mode 100644 index 00000000..914f74a5 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigManager.java @@ -0,0 +1,234 @@ +package com.mumfrey.liteloader.modconfig; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.google.common.base.Strings; +import com.google.common.collect.Maps; +import com.google.common.io.Files; +import com.mumfrey.liteloader.Configurable; +import com.mumfrey.liteloader.LiteMod; + +/** + * Registry where we keep the mod config panel classes and config file writers + * + * @author Adam Mummery-Smith + */ +public class ConfigManager +{ + /** + * Mod config panel classes + */ + private Map, Class> configPanels = Maps.newHashMap(); + + /** + * Mod config writers + */ + private Map configWriters = new HashMap(); + + /** + * List of config writers, for faster iteration in onTick + */ + private List configWriterList = new LinkedList(); + + /** + * Register a mod, adds the config panel class to the map if the mod + * implements Configurable + */ + public void registerMod(LiteMod mod) + { + if (mod instanceof Configurable) + { + Class panelClass = ((Configurable)mod).getConfigPanelClass(); + if (panelClass != null) this.configPanels.put(mod.getClass(), panelClass); + } + + this.registerExposable(mod, null, false); + } + + /** + * @param exposable + * @param fallbackFileName + * @param ignoreMissingConfigAnnotation + */ + public void registerExposable(Exposable exposable, String fallbackFileName, boolean ignoreMissingConfigAnnotation) + { + ExposableOptions options = exposable.getClass().getAnnotation(ExposableOptions.class); + if (options != null) + { + if (fallbackFileName == null) fallbackFileName = options.filename(); + this.initConfigWriter(exposable, fallbackFileName, options.strategy(), options.aggressive()); + } + else if (ignoreMissingConfigAnnotation) + { + this.initConfigWriter(exposable, fallbackFileName, ConfigStrategy.Versioned, false); + } + } + + /** + * Create a config writer instance for the specified mod + * + * @param exposable + * @param fileName + * @param strategy + */ + private void initConfigWriter(Exposable exposable, String fileName, ConfigStrategy strategy, boolean aggressive) + { + if (this.configWriters.containsKey(exposable)) + { + return; + } + + if (Strings.isNullOrEmpty(fileName)) + { + fileName = exposable.getClass().getSimpleName().toLowerCase(); + + if (fileName.startsWith("litemod")) + { + fileName = fileName.substring(7); + } + } + + ExposableConfigWriter configWriter = ExposableConfigWriter.create(exposable, strategy, fileName, aggressive); + if (configWriter != null) + { + this.configWriters.put(exposable, configWriter); + this.configWriterList.add(configWriter); + } + } + + /** + * If the specified mod has a versioned config strategy, attempt to copy the + * config. + * + * @param mod + * @param newConfigPath + * @param oldConfigPath + */ + public void migrateModConfig(LiteMod mod, File newConfigPath, File oldConfigPath) + { + if (this.configWriters.containsKey(mod)) + { + ExposableConfigWriter writer = this.configWriters.get(mod); + if (writer.isVersioned()) + { + File newConfigFile = writer.getConfigFile(); + File legacyConfigFile = new File(oldConfigPath, newConfigFile.getName()); + + if (legacyConfigFile.exists() && !newConfigFile.exists()) + { + try + { + Files.copy(legacyConfigFile, newConfigFile); + } + catch (IOException ex) + { + ex.printStackTrace(); + } + } + } + } + } + + /** + * Check whether a config panel is available for the specified class + * + * @param modClass + */ + public boolean hasPanel(Class modClass) + { + return modClass != null && this.configPanels.containsKey(modClass); + } + + /** + * Instance a new config panel for the specified mod class if one is + * available. + * + * @param modClass + */ + public ConfigPanel getPanel(Class modClass) + { + if (modClass != null && this.configPanels.containsKey(modClass)) + { + try + { + return this.configPanels.get(modClass).newInstance(); + } + catch (InstantiationException ex) {} + catch (IllegalAccessException ex) {} + + // If instantiation fails, remove the panel + this.configPanels.remove(modClass); + } + + return null; + } + + /** + * Initialise the config writer for the specified mod + * + * @param exposable + */ + public void initConfig(Exposable exposable) + { + if (this.configWriters.containsKey(exposable)) + { + this.configWriters.get(exposable).init(); + } + } + + /** + * Invalidate the specified mod config, cause it to be written to disk or + * scheduled for writing if it has been written recently. + * + * @param exposable + */ + public void invalidateConfig(Exposable exposable) + { + if (this.configWriters.containsKey(exposable)) + { + this.configWriters.get(exposable).invalidate(); + } + } + + /** + * Tick all of the configuration writers, handles latent writes for + * anti-hammer strategy. + */ + public void onTick() + { + for (ExposableConfigWriter writer : this.configWriterList) + { + writer.onTick(); + } + } + + /** + * Force all mod configs to be flushed to disk + */ + public void syncConfig() + { + for (ExposableConfigWriter writer : this.configWriterList) + { + writer.sync(); + } + } + + /** + * @param exposable + */ + public static ConfigStrategy getConfigStrategy(Exposable exposable) + { + ExposableOptions options = exposable.getClass().getAnnotation(ExposableOptions.class); + if (options != null) + { + return options.strategy(); + } + + return ConfigStrategy.Unversioned; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigPanel.java b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigPanel.java new file mode 100644 index 00000000..ee09e7b0 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigPanel.java @@ -0,0 +1,95 @@ +package com.mumfrey.liteloader.modconfig; + + +/** + * Interface for mod config panels to implement + * + * @author Adam Mummery-Smith + */ +public interface ConfigPanel +{ + /** + * Panels should return the text to display at the top of the config panel + * window. + */ + public abstract String getPanelTitle(); + + /** + * Get the height of the content area for scrolling purposes, return -1 to + * disable scrolling. + */ + public abstract int getContentHeight(); + + /** + * Called when the panel is displayed, initialise the panel (read settings, + * etc) + * + * @param host panel host + */ + public abstract void onPanelShown(ConfigPanelHost host); + + /** + * Called when the window is resized whilst the panel is active + * + * @param host panel host + */ + public abstract void onPanelResize(ConfigPanelHost host); + + /** + * Called when the panel is closed, panel should save settings + */ + public abstract void onPanelHidden(); + + /** + * Called every tick + */ + public abstract void onTick(ConfigPanelHost host); + + /** + * Draw the configuration panel + * + * @param host + * @param mouseX + * @param mouseY + * @param partialTicks + */ + public abstract void drawPanel(ConfigPanelHost host, int mouseX, int mouseY, float partialTicks); + + /** + * Called when a mouse button is pressed + * + * @param host + * @param mouseX + * @param mouseY + * @param mouseButton + */ + public abstract void mousePressed(ConfigPanelHost host, int mouseX, int mouseY, int mouseButton); + + /** + * Called when a mouse button is released + * + * @param host + * @param mouseX + * @param mouseY + * @param mouseButton + */ + public abstract void mouseReleased(ConfigPanelHost host, int mouseX, int mouseY, int mouseButton); + + /** + * Called when the mouse is moved + * + * @param host + * @param mouseX + * @param mouseY + */ + public abstract void mouseMoved(ConfigPanelHost host, int mouseX, int mouseY); + + /** + * Called when a key is pressed + * + * @param host + * @param keyChar + * @param keyCode + */ + public abstract void keyPressed(ConfigPanelHost host, char keyChar, int keyCode); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigPanelHost.java b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigPanelHost.java new file mode 100644 index 00000000..096e9810 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigPanelHost.java @@ -0,0 +1,42 @@ +package com.mumfrey.liteloader.modconfig; + +import com.mumfrey.liteloader.LiteMod; + +/** + * Interface for object which can host configuration panels + * + * @author Adam Mummery-Smith + */ +public interface ConfigPanelHost +{ + /** + * Get the mod instance which owns the panel + */ + public abstract TModClass getMod(); + + /** + * Get the width of the configuration panel area + */ + public abstract int getWidth(); + + /** + * Get the height of the configuration panel area + */ + public abstract int getHeight(); + + /** + * Notify the panel host that the panel wishes to close + */ + public abstract void close(); + + /** + * Notify the panel host that the panel wishes to advance to the next panel + */ +// public abstract void next(); + + /** + * Notify the panel host that the panel wishes to go back to the previous + * panel. + */ +// public abstract void previous(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigStrategy.java b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigStrategy.java new file mode 100644 index 00000000..450c5217 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ConfigStrategy.java @@ -0,0 +1,33 @@ +package com.mumfrey.liteloader.modconfig; + +import java.io.File; + +import com.mumfrey.liteloader.core.LiteLoader; + +/** + * Configuration management strategy + * + * @author Adam Mummery-Smith + */ +public enum ConfigStrategy +{ + /** + * Use the unversioned "common" config folder + */ + Unversioned, + + /** + * Use the versioned config folder + */ + Versioned; + + public File getFileForStrategy(String fileName) + { + if (this == ConfigStrategy.Versioned) + { + return new File(LiteLoader.getConfigFolder(), fileName); + } + + return new File(LiteLoader.getCommonConfigFolder(), fileName); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/Exposable.java b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/Exposable.java new file mode 100644 index 00000000..5133fbad --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/Exposable.java @@ -0,0 +1,10 @@ +package com.mumfrey.liteloader.modconfig; + +/** + * Base interface for objects which can support the ExposeConfig annotations + * + * @author Adam Mummery-Smith + */ +public interface Exposable +{ +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ExposableConfigWriter.java b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ExposableConfigWriter.java new file mode 100644 index 00000000..f44badf2 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ExposableConfigWriter.java @@ -0,0 +1,305 @@ +package com.mumfrey.liteloader.modconfig; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; + +/** + * Manages serialisation of exposable properties to a JSON config file via Gson + * + * @author Adam Mummery-Smith + */ +public final class ExposableConfigWriter implements InstanceCreator +{ + /** + * Minimum number of milliseconds that must elapse between writes + */ + private static final long ANTI_HAMMER_DELAY = 1000L; + + /** + * Exposable instance which we will serialise exposed properties + */ + private final Exposable exposable; + + /** + * JSON file to write to + */ + private final File configFile; + + /** + * True if this is a versioned config strategy + */ + private final boolean versioned; + + /** + * Disable anti-hammer and always save when requested + */ + private final boolean aggressive; + + /** + * Gson instance + */ + private final Gson gson; + + /** + * True if a config write has been requested but anti-hammer has prevented + * the write from occurring. + */ + private volatile boolean dirty = false; + + /** + * Last time the config was written, used for anti-hammer + */ + private volatile long lastWrite = 0L; + + /** + * It's possible that writes may be requested from different threads, lock + * object to prevent cross-thread derp. + */ + private Object readWriteLock = new Object(); + + /** + * @param exposable + * @param configFile + */ + private ExposableConfigWriter(Exposable exposable, File configFile, boolean versioned, boolean aggressive) + { + this.exposable = exposable; + this.configFile = configFile; + this.versioned = versioned; + this.aggressive = aggressive; + + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.setPrettyPrinting(); + gsonBuilder.serializeNulls(); + gsonBuilder.excludeFieldsWithoutExposeAnnotation(); + gsonBuilder.registerTypeAdapter(exposable.getClass(), this); + + if (this.exposable instanceof AdvancedExposable) + { + ((AdvancedExposable)this.exposable).setupGsonSerialiser(gsonBuilder); + } + + this.gson = gsonBuilder.create(); + } + + /** + * Get the config file underlying this writer + */ + File getConfigFile() + { + return this.configFile; + } + + /** + * Returns true if this writer is using a versioned strategy + */ + boolean isVersioned() + { + return this.versioned; + } + + /** + * Returns true if anti-hammer is disabled for this writer + */ + public boolean isAggressive() + { + return this.aggressive; + } + + /** + * Returns true if this writer has been invalidated but not yet been flushed + * to disk. + */ + boolean isDirty() + { + return this.dirty; + } + + /* (non-Javadoc) + * @see com.google.gson.InstanceCreator + * #createInstance(java.lang.reflect.Type) + */ + @Override + public Exposable createInstance(Type type) + { + return this.exposable; + } + + /** + * Initialise the config, reads from file and writes the initial config file + * if not present. + */ + void init() + { + // Read the config + this.read(); + + // If the config doesn't exist yet, seed the config + if (!this.configFile.exists()) + { + this.write(); + } + } + + /** + * Read the config from the file + */ + void read() + { + synchronized (this.readWriteLock) + { + if (this.configFile.exists()) + { + FileReader reader = null; + + try + { + reader = new FileReader(this.configFile); + + // Normally GSON would produce a new object by calling the default constructor, but we + // trick it into deserialising properties on the existing object instance by implementing + // an InstanceCreator which just returns the object instance which we already have + this.gson.fromJson(reader, this.exposable.getClass()); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + finally + { + try + { + if (reader != null) + { + reader.close(); + } + } + catch (IOException ex) + { + ex.printStackTrace(); + } + } + } + } + } + + /** + * Write the config to the file + */ + void write() + { + synchronized (this.readWriteLock) + { + FileWriter writer = null; + try + { + writer = new FileWriter(this.configFile); + this.gson.toJson(this.exposable, writer); + + this.dirty = false; + this.lastWrite = System.currentTimeMillis(); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + finally + { + try + { + if (writer != null) + { + writer.close(); + } + } + catch (IOException ex) + { + ex.printStackTrace(); + } + } + } + } + + /** + * Write the config to file, respecting anti-hammer and queuing the write if + * not enough time has elapsed. + */ + void invalidate() + { + if (!this.aggressive) + { + long sinceLastWrite = System.currentTimeMillis() - this.lastWrite; + if (sinceLastWrite < ANTI_HAMMER_DELAY) + { + this.dirty = true; + return; + } + } + + this.write(); + } + + /** + * Handle latent writes if the config was previously invalidated + */ + void onTick() + { + if (!this.aggressive && this.dirty) + { + long sinceLastWrite = System.currentTimeMillis() - this.lastWrite; + if (sinceLastWrite >= ANTI_HAMMER_DELAY) + { + this.write(); + } + } + } + + /** + * Force a write if dirty + */ + void sync() + { + if (this.dirty || this.aggressive) + { + this.write(); + } + } + + /** + * Factory method which creates and intialises a new ExposableConfigWriter + * for the specified exposable object and strategy. + * + * @param exposable + * @param strategy + * @param fileName + */ + static ExposableConfigWriter create(Exposable exposable, ConfigStrategy strategy, String fileName, boolean aggressive) + { + if (!fileName.toLowerCase().endsWith(".json")) + { + fileName = fileName + ".json"; + } + + File configFile = strategy.getFileForStrategy(fileName); + + if (exposable instanceof AdvancedExposable) + { + File customConfigFile = ((AdvancedExposable)exposable).getConfigFile(configFile, configFile.getParentFile(), fileName); + if (customConfigFile != null) + { + configFile = customConfigFile; + } + } + + ExposableConfigWriter writer = new ExposableConfigWriter(exposable, configFile, strategy == ConfigStrategy.Versioned, aggressive); + + return writer; + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ExposableOptions.java b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ExposableOptions.java new file mode 100644 index 00000000..03268c7f --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/modconfig/ExposableOptions.java @@ -0,0 +1,32 @@ +package com.mumfrey.liteloader.modconfig; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation which can be a applied to mod classes to indicate that members + * decorated with the Gson Expose annotation should be serialised with Gson. + * + * @author Adam Mummery-Smith + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExposableOptions +{ + /** + * Configuration strategy to use + */ + ConfigStrategy strategy() default ConfigStrategy.Unversioned; + + /** + * Config file name, if not specified the mod class name is used + */ + String filename() default ""; + + /** + * Set to true to disable write anti-hammer for config file + */ + boolean aggressive() default false; +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/permissions/LocalPermissions.java b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/LocalPermissions.java new file mode 100644 index 00000000..27f8a67c --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/LocalPermissions.java @@ -0,0 +1,24 @@ +package com.mumfrey.liteloader.permissions; + + +public class LocalPermissions implements Permissions +{ + @Override + public boolean getPermissionSet(String permission) + { + return true; + } + + @Override + public boolean getHasPermission(String permission) + { + return true; + } + + @Override + public boolean getHasPermission(String permission, boolean defaultValue) + { + return defaultValue; + } + +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissibleAllMods.java b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissibleAllMods.java new file mode 100644 index 00000000..44a80f81 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissibleAllMods.java @@ -0,0 +1,70 @@ +package com.mumfrey.liteloader.permissions; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import com.mumfrey.liteloader.Permissible; + +public class PermissibleAllMods implements Permissible +{ + private Set permissibles = new HashSet(); + + public void addPermissible(Permissible permissible) + { + this.permissibles.add(permissible); + } + + @Override + public String getName() + { + return "All Mods"; + } + + @Override + public String getVersion() + { + return "0.0"; + } + + @Override + public void init(File configPath) + { + } + + @Override + public void upgradeSettings(String version, File configPath, File oldConfigPath) + { + } + + @Override + public String getPermissibleModName() + { + return "all"; + } + + @Override + public float getPermissibleModVersion() + { + return 0.0F; + } + + @Override + public void registerPermissions(PermissionsManagerClient permissionsManager) + { + } + + @Override + public void onPermissionsCleared(PermissionsManager manager) + { + for (Permissible permissible : this.permissibles) + permissible.onPermissionsCleared(manager); + } + + @Override + public void onPermissionsChanged(PermissionsManager manager) + { + for (Permissible permissible : this.permissibles) + permissible.onPermissionsChanged(manager); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/permissions/Permission.java b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/Permission.java new file mode 100644 index 00000000..e0f07ac6 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/Permission.java @@ -0,0 +1,219 @@ +package com.mumfrey.liteloader.permissions; + +import java.util.Hashtable; +import java.util.Map; + +/** + * Class which represents a permission node + * + * @author Adam Mummery-Smith + */ +public class Permission +{ + /** + * True if this node is the root of the permission tree + */ + private final boolean isRootNode; + + /** + * True if this node is a wildcard node (can match any query) + */ + private final boolean isWildcardNode; + + /** + * Node name + */ + private final String nodeName; + + /** + * Nodes which are children of this node + */ + private final Map childNodes = new Hashtable(); + + /** + * Node value + */ + private boolean value; + + /** + * Create a new root node + */ + public Permission() + { + this.isRootNode = true; + this.isWildcardNode = false; + this.value = true; + this.nodeName = "root"; + } + + /** + * Create a new child node with the specified name and value + * + * @param permissionName Name of the permission node + * @param value Initial value for the permission node + */ + public Permission(String permissionName, boolean value) + { + this.isRootNode = false; + this.isWildcardNode = "*".equals(permissionName); + this.value = value; + this.nodeName = permissionName; + } + + /** + * Create a new child node with the specified name + * + * @param permissionName Name of the permission node + */ + public Permission(String permissionName) + { + this(permissionName, true); + } + + /** + * Get whether this node is a root node (read only) + */ + public boolean isRoot() + { + return this.isRootNode; + } + + /** + * Get whether this node is a wildcard node + */ + public boolean isWildcard() + { + return this.isWildcardNode; + } + + /** + * Get the name of this permission node + */ + public String getName() + { + return this.nodeName; + } + + /** + * Get the value of this permission node + */ + public boolean getValue() + { + return this.value; + } + + /** + * Set the value of this permission node + * + * @param newValue + */ + public void setValue(boolean newValue) + { + this.value = newValue; + } + + /** + * Get the specified node name + * + * @param name + */ + public Permission getPermission(String name) + { + Permission fallback = (this.isWildcardNode) ? this : null; + + if (name.indexOf('.') > -1) + { + String head = name.substring(0, name.indexOf('.')); + String tail = name.substring(name.indexOf('.') + 1); + + Permission child = this.getPermission(head); + + if (child != null) + { + return child.getPermission(tail); + } + } + else if (this.childNodes.containsKey(name)) + { + return this.childNodes.get(name); + } + + for (Permission childPermission : this.childNodes.values()) + { + if (childPermission.isWildcard()) + { + return childPermission; + } + } + + return fallback; + } + + /** + * Set the specified node name + * + * @param name + */ + public Permission setPermission(String name) + { + return this.setPermission(name, true); + } + + /** + * Set the specified permission to the specified value + * + * @param name + * @param value + */ + public Permission setPermission(String name, boolean value) + { + if (name.indexOf('.') > -1) + { + String head = name.substring(0, name.indexOf('.')); + String tail = name.substring(name.indexOf('.') + 1); + + Permission child = this.setPermission(head, false); + return child.setPermission(tail, value); + } + + Permission child = this.getPermission(name); + + if (child == null || child.isWildcard()) + { + child = new Permission(name, value); + this.childNodes.put(child.getName(), child); + } + else + { + child.setValue(value | child.value); + } + + return child; + } + + /** + * Sets a permission and also explicitly sets the permission value, this + * allows negated permissions to be set. + * + * @param name + * @param value + */ + public Permission setPermissionAndValue(String name, boolean value) + { + Permission permission = this.setPermission(name, value); + permission.setValue(value); + return permission; + } + + /** + * Check whether the specified permission is set + * + * @param name + * @param value + */ + public boolean isSet(String name, boolean value) + { + Permission child = this.getPermission(name); + return child == null ? value : child.getValue(); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/permissions/Permissions.java b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/Permissions.java new file mode 100644 index 00000000..a70fa531 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/Permissions.java @@ -0,0 +1,38 @@ +package com.mumfrey.liteloader.permissions; + +/** + * Represents a set of permissions assigned by an authority + * + * @author Adam Mummery-Smith + */ +public interface Permissions +{ + /** + * Returns true if the specified permission is set in this permission + * container. + * + * @param permission Name of the permission to test for + * @return True if the permission exists in this set + */ + public abstract boolean getPermissionSet(String permission); + + /** + * Returns true if the authority says we have this permission or false if + * the permission is denied or not set. + * + * @param permission Name of the permission to test for + */ + public abstract boolean getHasPermission(String permission); + + /** + * Returns true if the authority says we have this permission or if the + * permission is not specified by the authority returns the default value. + * + * @param permission Name of the permission to test for + * @param defaultValue Value to return if the permission is NOT specified by + * the authority + * @return State of the authority permission or default value if not + * specified + */ + public abstract boolean getHasPermission(String permission, boolean defaultValue); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManager.java b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManager.java new file mode 100644 index 00000000..00376a43 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManager.java @@ -0,0 +1,51 @@ +package com.mumfrey.liteloader.permissions; + +import com.mumfrey.liteloader.Permissible; +import com.mumfrey.liteloader.common.GameEngine; + +/** + * Interface for permissions manager implementations + * + * @author Adam Mummery-Smith + */ +public interface PermissionsManager +{ + /** + * Get the underlying permissions node for this manager for the specified + * mod + * + * @param mod Mod to fetch permissions for + */ + public abstract Permissions getPermissions(Permissible mod); + + /** + * Get the time the permissions for the specified mod were last updated + * + * @param mod Mod to check for + * @return Timestamp when the permissions were last updated + */ + public abstract Long getPermissionUpdateTime(Permissible mod); + + /** + * Handler for tick event + * + * @param engine + * @param partialTicks + * @param inGame + */ + public abstract void onTick(GameEngine engine, float partialTicks, boolean inGame); + + /** + * Register a new event listener, the registered object will receive + * callbacks for permissions events + * + * @param permissible + */ + public abstract void registerPermissible(Permissible permissible); + + /** + * Perform any necessary validation to check for a tamper condition, can and + * should be called from as many places as possible + */ + public abstract void tamperCheck(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManagerClient.java b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManagerClient.java new file mode 100644 index 00000000..c4b90886 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManagerClient.java @@ -0,0 +1,526 @@ +package com.mumfrey.liteloader.permissions; + +import io.netty.buffer.Unpooled; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import net.eq2online.permissions.ReplicatedPermissionsContainer; +import net.minecraft.network.INetHandler; +import net.minecraft.network.PacketBuffer; +import net.minecraft.network.play.server.S01PacketJoinGame; + +import com.mumfrey.liteloader.LiteMod; +import com.mumfrey.liteloader.Permissible; +import com.mumfrey.liteloader.PluginChannelListener; +import com.mumfrey.liteloader.common.GameEngine; +import com.mumfrey.liteloader.core.ClientPluginChannels; +import com.mumfrey.liteloader.core.PluginChannels.ChannelPolicy; + +/** + * This class manages permissions on the client, it is a singleton class which + * can manage permissions for multiple client mods. It manages the client/server + * communication used to replicate permissions and serves as a hub for + * permissions objects which keep track of the permissions available on the + * client. + * + * @author Adam Mummery-Smith + */ +public final class PermissionsManagerClient implements PermissionsManager, PluginChannelListener +{ + /** + * Singleton instance of the client permissions manager + */ + private static PermissionsManagerClient instance; + + /** + * Permissions permissible which is a proxy for permissions that are common + * to all mods. + */ + private static Permissible allMods = new PermissibleAllMods(); + + /** + * Minecraft instance + */ + private GameEngine engine; + + /** + * List of registered client mods supporting permissions + */ + private Map registeredClientMods = new HashMap(); + + /** + * List of registered client permissions, grouped by mod + */ + private Map> registeredClientPermissions = new HashMap>(); + + /** + * Objects which listen to events generated by this object + */ + private Set permissibles = new HashSet(); + + /** + * Local permissions, used when server permissions are not available + */ + private LocalPermissions localPermissions = new LocalPermissions(); + + /** + * Server permissions, indexed by mod + */ + private Map serverPermissions = new HashMap(); + + /** + * Last time onTick was called, used to detect tamper condition if no ticks + * are being received. + */ + private long lastTickTime = System.currentTimeMillis(); + + /** + * Delay counter for when joining a server + */ + private int pendingRefreshTicks = 0; + + private int menuTicks = 0; + + /** + * Get a reference to the singleton instance of the client permissions + * manager. + */ + public static PermissionsManagerClient getInstance() + { + if (instance == null) + { + instance = new PermissionsManagerClient(); + } + + return instance; + } + + /** + * Private .ctor, for singleton pattern + */ + private PermissionsManagerClient() + { + this.registerClientMod("all", allMods); + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.PermissionsManager + * #getPermissions(java.lang.String) + */ + @Override + public Permissions getPermissions(Permissible mod) + { + if (mod == null) mod = allMods; + String modName = mod.getPermissibleModName(); + + ServerPermissions modPermissions = this.serverPermissions.get(modName); + return modPermissions != null ? modPermissions : this.localPermissions; + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.PermissionsManager + * #getPermissionUpdateTime(java.lang.String) + */ + @Override + public Long getPermissionUpdateTime(Permissible mod) + { + if (mod == null) mod = allMods; + String modName = mod.getPermissibleModName(); + + ServerPermissions modPermissions = this.serverPermissions.get(modName); + return modPermissions != null ? modPermissions.getReplicationTime() : 0; + } + + /** + * Register a new mod, if permissible + * + * @param mod + */ + public void registerMod(LiteMod mod) + { + if (mod instanceof Permissible) + { + this.registerPermissible((Permissible)mod); + } + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.PermissionsManager + * #registerListener(net.eq2online.permissions.PermissionsListener) + */ + @Override + public void registerPermissible(Permissible permissible) + { + if (!this.permissibles.contains(permissible) && permissible.getPermissibleModName() != null) + { + this.registerClientMod(permissible.getPermissibleModName(), permissible); + permissible.registerPermissions(this); + } + + this.permissibles.add(permissible); + } + + /** + * Register a new client mod with this manager + * + * @param modName Mod name + * @param mod Mod instance + */ + private void registerClientMod(String modName, Permissible mod) + { + if (this.registeredClientMods.containsKey(modName)) + { + throw new IllegalArgumentException("Cannot register mod \"" + modName + + "\"! The mod was already registered with the permissions manager."); + } + + this.registeredClientMods.put(modName, mod); + this.registeredClientPermissions.put(mod, new TreeSet()); + } + + public void onJoinGame(INetHandler netHandler, S01PacketJoinGame joinGamePacket) + { + this.clearServerPermissions(); + this.scheduleRefresh(); + } + + /** + * Schedule a permissions refresh + */ + public void scheduleRefresh() + { + this.pendingRefreshTicks = 2; + } + + /** + * Clears the current replicated server permissions + */ + protected void clearServerPermissions() + { + this.serverPermissions.clear(); + + for (Permissible permissible : this.permissibles) + permissible.onPermissionsCleared(this); + } + + /** + * Send permission query packets to the server for all registered mods + */ + protected void sendPermissionQueries() + { + for (Permissible mod : this.registeredClientMods.values()) + this.sendPermissionQuery(mod); + } + + /** + * Send a permission query packet to the server for the specified mod. You + * do not need to call this method because it is issued automatically by the + * client permissions manager when connecting to a new server. However you + * can call use this method to "force" a refresh of permissions when needed. + * + * @param mod mod to send a query packet for + */ + public void sendPermissionQuery(Permissible mod) + { + String modName = mod.getPermissibleModName(); + + if (this.engine != null && this.engine.isClient() && this.engine.isInGame()) + { + if (!this.registeredClientMods.containsValue(mod)) + { + throw new IllegalArgumentException("The specified mod \"" + modName + "\" was not registered with the permissions system"); + } + + Float modVersion = mod.getPermissibleModVersion(); + Set modPermissions = this.registeredClientPermissions.get(mod); + + if (modPermissions != null) + { + ReplicatedPermissionsContainer query = new ReplicatedPermissionsContainer(modName, modVersion, modPermissions); + + if (!query.modName.equals("all") || query.permissions.size() > 0) + { + byte[] data = query.getBytes(); + PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); + buffer.writeBytes(data); + ClientPluginChannels.sendMessage(ReplicatedPermissionsContainer.CHANNEL, buffer, ChannelPolicy.DISPATCH_ALWAYS); + } + } + } + else + { + this.serverPermissions.remove(modName); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.permissions.PermissionsManager + * #onTick(net.minecraft.client.Minecraft, float, boolean) + */ + @Override + public void onTick(GameEngine engine, float partialTicks, boolean inGame) + { + this.engine = engine; + this.lastTickTime = System.currentTimeMillis(); + + if (this.pendingRefreshTicks > 0) + { + this.pendingRefreshTicks--; + + if (this.pendingRefreshTicks == 0 && inGame) + { + this.sendPermissionQueries(); + return; + } + } + + for (Map.Entry modPermissions : this.serverPermissions.entrySet()) + { + if (!modPermissions.getValue().isValid()) + { + modPermissions.getValue().notifyRefreshPending(); + this.sendPermissionQuery(this.registeredClientMods.get(modPermissions.getKey())); + } + } + + if (inGame) this.menuTicks = 0; else this.menuTicks++; + + if (this.menuTicks == 200) + { + this.clearServerPermissions(); + } + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.PermissionsManager#tamperCheck() + */ + @Override + public void tamperCheck() + { + if (System.currentTimeMillis() - this.lastTickTime > 60000L) + { + throw new IllegalStateException("Client permissions manager was not ticked for 60 seconds, tamper."); + } + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.PermissionsManager + * #onCustomPayload(java.lang.String, int, byte[]) + */ + @Override + public void onCustomPayload(String channel, PacketBuffer data) + { + if (channel.equals(ReplicatedPermissionsContainer.CHANNEL) && !this.engine.isSinglePlayer()) + { + ServerPermissions modPermissions = null; + try + { + modPermissions = new ServerPermissions(data); + } + catch (Exception ex) {} + + if (modPermissions != null && modPermissions.getModName() != null) + { + this.serverPermissions.put(modPermissions.getModName(), modPermissions); + + Permissible permissible = this.registeredClientMods.get(modPermissions.getModName()); + if (permissible != null) permissible.onPermissionsChanged(this); + } + } + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.PermissionsManager#getChannels() + */ + @Override + public List getChannels() + { + return Arrays.asList(new String[] { ReplicatedPermissionsContainer.CHANNEL }); + } + + /** + * Register a permission for all mods, the permission will be prefixed with + * "mod.all." to provide a common namespace for client mods when + * permissions are replicated to the server. + * + * @param permission + */ + public void registerPermission(String permission) + { + this.registerModPermission(allMods, permission); + } + + /** + * Register a permission for the specified mod, the permission will be + * prefixed with "mod.." to provide a common namespace for + * client mods when permissions are replicated to the server. + * + * @param mod + * @param permission + */ + public void registerModPermission(Permissible mod, String permission) + { + if (mod == null) mod = allMods; + String modName = mod.getPermissibleModName(); + + if (!this.registeredClientMods.containsValue(mod)) + { + throw new IllegalArgumentException("Cannot register a mod permission for mod \"" + modName + + "\"! The mod was not registered with the permissions manager."); + } + + permission = formatModPermission(modName, permission); + + Set modPermissions = this.registeredClientPermissions.get(mod); + if (modPermissions != null && !modPermissions.contains(permission)) + { + modPermissions.add(permission); + } + } + + /** + * Get the value of the specified permission for all mods. + * + * @param permission Permission to check for + */ + public boolean getPermission(String permission) + { + return this.getModPermission(allMods, permission); + } + + /** + * Get the value of the specified permission for all mods and return the + * default value if the permission is not set. + * + * @param permission Permission to check for + * @param defaultValue Value to return if the permission is not set + */ + public boolean getPermission(String permission, boolean defaultValue) + { + return this.getModPermission(allMods, permission, defaultValue); + } + + /** + * Get the value of the specified permission for the specified mod. The + * permission will be prefixed with "mod.." in keeping + * with registerModPermission as a convenience. + * + * @param mod + * @param permission + */ + public boolean getModPermission(Permissible mod, String permission) + { + if (mod == null) mod = PermissionsManagerClient.allMods; + permission = formatModPermission(mod.getPermissibleModName(), permission); + Permissions permissions = this.getPermissions(mod); + + if (permissions != null) + { + return permissions.getHasPermission(permission); + } + + return true; + } + + /** + * Get the value of the specified permission for the specified mod. The + * permission will be prefixed with "mod.." in keeping + * with registerModPermission as a convenience. + * + * @param modName + * @param permission + */ + public boolean getModPermission(String modName, String permission) + { + Permissible mod = this.registeredClientMods.get(modName); + return mod != null ? this.getModPermission(mod, permission) : false; + } + + /** + * Get the value of the specified permission for the specified mod. The + * permission will be prefixed with "mod.." in keeping + * with registerModPermission as a convenience. If the permission does not + * exist, the specified default value will be returned. + * + * @param mod + * @param permission + * @param defaultValue + */ + public boolean getModPermission(Permissible mod, String permission, boolean defaultValue) + { + if (mod == null) mod = allMods; + permission = formatModPermission(mod.getPermissibleModName(), permission); + Permissions permissions = this.getPermissions(mod); + + if (permissions != null && permissions.getPermissionSet(permission)) + { + return permissions.getHasPermission(permission); + } + + return defaultValue; + } + + /** + * Get the value of the specified permission for the specified mod. The + * permission will be prefixed with "mod.." in keeping + * with registerModPermission as a convenience. + * + * @param modName + * @param permission + */ + public boolean getModPermission(String modName, String permission, boolean defaultValue) + { + Permissible mod = this.registeredClientMods.get(modName); + return mod != null ? this.getModPermission(mod, permission, defaultValue) : defaultValue; + } + + /** + * @param modName + * @param permission + */ + protected static String formatModPermission(String modName, String permission) + { + return String.format("mod.%s.%s", modName, permission); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.LiteMod#getName() + */ + @Override + public String getName() + { + // Stub for PluginChannelListener interface + return null; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.LiteMod#getVersion() + */ + @Override + public String getVersion() + { + // Stub for PluginChannelListener interface + return null; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.LiteMod#init() + */ + @Override + public void init(File configPath) + { + // Stub for PluginChannelListener interface + } + + @Override + public void upgradeSettings(String version, File configPath, File oldConfigPath) + { + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManagerServer.java b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManagerServer.java new file mode 100644 index 00000000..6ebe04c9 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/PermissionsManagerServer.java @@ -0,0 +1,66 @@ +package com.mumfrey.liteloader.permissions; + +import java.util.List; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.PacketBuffer; + +import com.mumfrey.liteloader.Permissible; +import com.mumfrey.liteloader.ServerPluginChannelListener; +import com.mumfrey.liteloader.common.GameEngine; + +/** + * TODO implementation + * + * @author Adam Mummery-Smith + */ +public class PermissionsManagerServer implements PermissionsManager, ServerPluginChannelListener +{ + public PermissionsManagerServer() + { + } + + @Override + public String getName() + { + return null; + } + + @Override + public void onCustomPayload(EntityPlayerMP sender, String channel, PacketBuffer data) + { + } + + @Override + public Permissions getPermissions(Permissible mod) + { + return null; + } + + @Override + public Long getPermissionUpdateTime(Permissible mod) + { + return null; + } + + @Override + public void onTick(GameEngine engine, float partialTicks, boolean inGame) + { + } + + @Override + public List getChannels() + { + return null; + } + + @Override + public void registerPermissible(Permissible permissible) + { + } + + @Override + public void tamperCheck() + { + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/permissions/ReplicatedPermissions.java b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/ReplicatedPermissions.java new file mode 100644 index 00000000..8d9a2279 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/ReplicatedPermissions.java @@ -0,0 +1,32 @@ +package com.mumfrey.liteloader.permissions; + +/** + * Represents a set of permissions assigned by a remote authority such as a + * server. + * + * @author Adam Mummery-Smith + */ +public interface ReplicatedPermissions extends Permissions +{ + /** + * Get the time that this object was received from the remote authority + */ + public abstract long getReplicationTime(); + + /** + * Return true if this permissions object is valid (within cache period) + */ + public abstract boolean isValid(); + + /** + * Forcibly invalidate this permission container, forces update at the next + * opportunity. + */ + public abstract void invalidate(); + + /** + * Temporarily forces the permissions object to be valid to prevent repeated + * revalidation. + */ + public abstract void notifyRefreshPending(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/permissions/ServerPermissions.java b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/ServerPermissions.java new file mode 100644 index 00000000..a94933f1 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/permissions/ServerPermissions.java @@ -0,0 +1,154 @@ +package com.mumfrey.liteloader.permissions; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.eq2online.permissions.ReplicatedPermissionsContainer; +import net.minecraft.network.PacketBuffer; + + +/** + * Replicated permissions implementation + * + * @author Adam Mummery-Smith + */ +public class ServerPermissions implements ReplicatedPermissions +{ + /** + * Pattern for recognising valid permissions in the server feed + */ + private static final Pattern permissionPattern = Pattern.compile("^([\\+\\-])(([a-z0-9]+\\.)*[a-z0-9\\*]+)$", Pattern.CASE_INSENSITIVE); + + protected String modName; + + /** + * Root permission node + */ + protected Permission permissions = new Permission(); + + /** + * Time the permissions were updated + */ + protected long createdTime = 0L; + + /** + * Expiry time of the current data cache + */ + protected long validUntil = 0L; + + /** + * Time to cache server responses by default + */ + protected long cacheTime = 10L * 60L * 1000L; // 10 minutes + + /** + * Time to wait when refreshing server permissions before trying again + */ + protected long refreshTime = 15L * 1000L; // 15 seconds + + /** + * @param data + */ + public ServerPermissions(PacketBuffer data) + { + this.createdTime = System.currentTimeMillis(); + this.validUntil = this.createdTime + this.cacheTime; + + ReplicatedPermissionsContainer response = ReplicatedPermissionsContainer.fromPacketBuffer(data); + + if (response != null) + { + response.sanitise(); + + this.modName = response.modName; + this.validUntil = System.currentTimeMillis() + response.remoteCacheTimeSeconds * 1000L; + + for (String permissionString : response.permissions) + { + Matcher permissionMatcher = permissionPattern.matcher(permissionString); + + if (permissionMatcher.matches()) + { + String name = permissionMatcher.group(2); + boolean value = permissionMatcher.group(1).equals("+"); + + this.permissions.setPermissionAndValue(name, value); + } + } + } + } + + /** + * Get the permissible mod name + */ + public String getModName() + { + return this.modName; + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.Permissions#getPermissionSet( + * java.lang.String) + */ + @Override + public boolean getPermissionSet(String permission) + { + return this.permissions.getPermission(permission) != null; + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.Permissions#getHasPermission( + * java.lang.String) + */ + @Override + public boolean getHasPermission(String permission) + { + Permission perm = this.permissions.getPermission(permission); + return perm != null && perm.getValue(); + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.Permissions#getHasPermission( + * java.lang.String, boolean) + */ + @Override + public boolean getHasPermission(String permission, boolean defaultValue) + { + Permission perm = this.permissions.getPermission(permission); + + return perm != null ? perm.getValue() : defaultValue; + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.ReplicatedPermissions#getReplicationTime() + */ + @Override + public long getReplicationTime() + { + return this.createdTime; + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.ReplicatedPermissions#isValid() + */ + @Override + public boolean isValid() + { + return System.currentTimeMillis() < this.validUntil; + } + + /* (non-Javadoc) + * @see net.eq2online.permissions.ReplicatedPermissions#invalidate() + */ + @Override + public void invalidate() + { + this.validUntil = 0L; + } + + @Override + public void notifyRefreshPending() + { + this.validUntil = System.currentTimeMillis() + this.refreshTime; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/AppendInsns.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/AppendInsns.java new file mode 100644 index 00000000..79aca23c --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/AppendInsns.java @@ -0,0 +1,19 @@ +package com.mumfrey.liteloader.transformers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation which instructs the ClassOverlayTransformer to append instructions + * from the annotated method to the target method. + * + * @author Adam Mummery-Smith + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface AppendInsns +{ + public String value() default(""); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ByteCodeUtilities.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ByteCodeUtilities.java new file mode 100644 index 00000000..3446f509 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ByteCodeUtilities.java @@ -0,0 +1,864 @@ +package com.mumfrey.liteloader.transformers; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import net.minecraft.launchwrapper.IClassTransformer; +import net.minecraft.launchwrapper.Launch; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.BasicValue; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.SimpleVerifier; + +import com.mumfrey.liteloader.core.runtime.Obf; + +/** + * Utility methods for working with bytecode using ASM + * + * @author Adam Mummery-Smith + */ +public abstract class ByteCodeUtilities +{ + private static Map> calculatedLocalVariables = new HashMap>(); + + private ByteCodeUtilities() {} + + /** + * Replace all constructor invocations for the target class in the supplied + * classNode with invocations of the replacement class. + * + * @param classNode Class to search in + * @param target Target type + * @param replacement Replacement type + */ + public static void replaceConstructors(ClassNode classNode, Obf target, Obf replacement) + { + for (MethodNode method : classNode.methods) + { + ByteCodeUtilities.replaceConstructors(method, target, replacement); + } + } + + /** + * Replace all constructor invocations for the target class in the supplied + * method with invocations of the replacement class. + * + * @param method Method to look in + * @param target Target type + * @param replacement Replacement type + */ + public static void replaceConstructors(MethodNode method, Obf target, Obf replacement) + { + Iterator iter = method.instructions.iterator(); + while (iter.hasNext()) + { + AbstractInsnNode insn = iter.next(); + if (insn.getOpcode() == Opcodes.NEW) + { + TypeInsnNode typeInsn = (TypeInsnNode)insn; + if (target.obf.equals(typeInsn.desc) || target.ref.equals(typeInsn.desc)) + { + typeInsn.desc = replacement.ref; + } + } + else if (insn instanceof MethodInsnNode && insn.getOpcode() == Opcodes.INVOKESPECIAL) + { + MethodInsnNode methodInsn = (MethodInsnNode)insn; + if ((target.obf.equals(methodInsn.owner) || target.ref.equals(methodInsn.owner)) && "".equals(methodInsn.name)) + { + methodInsn.owner = replacement.ref; + } + } + } + } + + /** + * Injects appropriate LOAD opcodes into the supplied InsnList appropriate + * for each entry in the args array starting at pos. + * + * @param args Argument types + * @param insns Instruction List to inject into + * @param pos Start position + */ + public static void loadArgs(Type[] args, InsnList insns, int pos) + { + ByteCodeUtilities.loadArgs(args, insns, pos, -1); + } + + /** + * Injects appropriate LOAD opcodes into the supplied InsnList appropriate + * for each entry in the args array starting at start and ending at end. + * + * @param args Argument types + * @param insns Instruction List to inject into + * @param start Start position + * @param end End position + */ + public static void loadArgs(Type[] args, InsnList insns, int start, int end) + { + int pos = start; + + for (Type type : args) + { + insns.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), pos)); + pos += type.getSize(); + if (end >= start && pos >= end) return; + } + } + + /** + * Injects appropriate LOAD opcodes into the supplied InsnList for each + * entry in the supplied locals array starting at pos. + * + * @param locals Local types (can contain nulls for uninitialised, TOP, or + * RETURN values in locals) + * @param insns Instruction List to inject into + * @param pos Start position + */ + public static void loadLocals(Type[] locals, InsnList insns, int pos) + { + for (; pos < locals.length; pos++) + { + if (locals[pos] != null) + { + insns.add(new VarInsnNode(locals[pos].getOpcode(Opcodes.ILOAD), pos)); + } + } + } + + /** + * Get the first variable index in the supplied method which is not an + * argument or "this" reference, this corresponds to the size of the + * arguments passed in to the method plus an extra spot for "this" if the + * method is non-static. + * + * @param method MethodNode to inspect + * @return first available local index which is NOT used by a method + * argument or "this" + */ + public static int getFirstNonArgLocalIndex(MethodNode method) + { + return ByteCodeUtilities.getFirstNonArgLocalIndex(Type.getArgumentTypes(method.desc), (method.access & Opcodes.ACC_STATIC) == 0); + } + + /** + * Get the first non-arg variable index based on the supplied arg array and + * whether to include the "this" reference, this corresponds to the size of + * the arguments passed in to the method plus an extra spot for "this" is + * specified. + * + * @param args Method arguments + * @param includeThis Whether to include a slot for "this" (generally true + * for all non-static methods) + * @return first available local index which is NOT used by a method + * argument or "this" + */ + public static int getFirstNonArgLocalIndex(Type[] args, boolean includeThis) + { + return ByteCodeUtilities.getArgsSize(args) + (includeThis ? 1 : 0); + } + + /** + * Get the size of the specified args array in local variable terms (eg. + * doubles and longs take two spaces). + * + * @param args Method argument types as array + * @return size of the specified arguments array in terms of stack slots + */ + public static int getArgsSize(Type[] args) + { + int size = 0; + + for (Type type : args) + { + size += type.getSize(); + } + + return size; + } + + /** + * Attempts to identify available locals at an arbitrary point in the + * bytecode specified by node. + * + *

      This method builds an approximate view of the locals available at an + * arbitrary point in the bytecode by examining the following features in + * the bytecode:

      + * + *
        + *
      • Any available stack map frames
      • + *
      • STORE opcodes
      • + *
      • The local variable table
      • + *
      + * + *

      Inference proceeds by walking the bytecode from the start of the + * method looking for stack frames and STORE opcodes. When either of these + * is encountered, an attempt is made to cross-reference the values in the + * stack map or STORE opcode with the value in the local variable table + * which covers the code range. Stack map frames overwrite the entire + * simulated local variable table with their own value types, STORE opcodes + * overwrite only the local slot to which they pertain. Values in the + * simulated locals array are spaced according to their size (unlike the + * representation in FrameNode) and this TOP, NULL and UNINTITIALIZED_THIS + * opcodes will be represented as null values in the simulated frame.

      + * + *

      This code does not currently simulate the prescribed JVM behaviour + * where overwriting the second slot of a DOUBLE or LONG actually + * invalidates the DOUBLE or LONG stored in the previous location, so we + * have to hope (for now) that this behaviour isn't emitted by the compiler + * or any upstream transformers. I may have to re-think this strategy if + * this situation is encountered in the wild.

      + * + * @param classNode ClassNode containing the method, used to initialise the + * implicit "this" reference in simple methods with no stack frames + * @param method MethodNode to explore + * @param node Node indicating the position at which to determine the locals + * state. The locals will be enumerated UP TO the specified node, so + * bear in mind that if the specified node is itself a STORE opcode, + * then we will be looking at the state of the locals PRIOR to its + * invocation + * @return A sparse array containing a view (hopefully) of the locals at the + * specified location + */ + public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode method, AbstractInsnNode node) + { + LocalVariableNode[] frame = new LocalVariableNode[method.maxLocals]; + + // Initialise implicit "this" reference in non-static methods + if ((method.access & Opcodes.ACC_STATIC) == 0) + { + frame[0] = new LocalVariableNode("this", classNode.name, null, null, null, 0); + } + + for (Iterator iter = method.instructions.iterator(); iter.hasNext();) + { + AbstractInsnNode insn = iter.next(); + if (insn instanceof FrameNode) + { + FrameNode frameNode = (FrameNode)insn; + + // localPos tracks the location in the frame node's locals list, which doesn't leave space for TOP entries + for (int localPos = 0, framePos = 0; framePos < frame.length; framePos++, localPos++) + { + // Get the local at the current position in the FrameNode's locals list + final Object localType = (localPos < frameNode.local.size()) ? frameNode.local.get(localPos) : null; + + if (localType instanceof String) // String refers to a reference type + { + frame[framePos] = ByteCodeUtilities.getLocalVariableAt(classNode, method, node, framePos); + } + else if (localType instanceof Integer) // Integer refers to a primitive type or other marker + { + boolean isMarkerType = localType == Opcodes.UNINITIALIZED_THIS || localType == Opcodes.TOP || localType == Opcodes.NULL; + boolean is32bitValue = localType == Opcodes.INTEGER || localType == Opcodes.FLOAT; + boolean is64bitValue = localType == Opcodes.DOUBLE || localType == Opcodes.LONG; + if (isMarkerType) + { + frame[framePos] = null; + } + else if (is32bitValue || is64bitValue) + { + frame[framePos] = ByteCodeUtilities.getLocalVariableAt(classNode, method, node, framePos); + + if (is64bitValue) + { + framePos++; + frame[framePos] = null; // TOP + } + } + else + { + throw new RuntimeException("Unrecognised locals opcode " + localType + " in locals array at position " + localPos + " in " + + classNode.name + "." + method.name + method.desc); + } + } + else if (localType == null) + { + frame[framePos] = null; + } + else + { + throw new RuntimeException("Invalid value " + localType + " in locals array at position " + localPos + " in " + classNode.name + + "." + method.name + method.desc); + } + } + } + else if (insn instanceof VarInsnNode) + { + VarInsnNode varNode = (VarInsnNode)insn; + frame[varNode.var] = ByteCodeUtilities.getLocalVariableAt(classNode, method, node, varNode.var); + } + else if (insn == node) + { + break; + } + } + + return frame; + } + + /** + * Attempts to locate the appropriate entry in the local variable table for + * the specified local variable index at the location specified by node. + * + * @param classNode Containing class + * @param method Method + * @param node Instruction defining the location to get the local variable + * table at + * @param var Local variable index + * @return a LocalVariableNode containing information about the local + * variable at the specified location in the specified local slot + */ + public static LocalVariableNode getLocalVariableAt(ClassNode classNode, MethodNode method, AbstractInsnNode node, int var) + { + LocalVariableNode localVariableNode = null; + + int pos = method.instructions.indexOf(node); + + List localVariables = ByteCodeUtilities.getLocalVariableTable(classNode, method); + for (LocalVariableNode local : localVariables) + { + if (local.index != var) continue; + int start = method.instructions.indexOf(local.start); + int end = method.instructions.indexOf(local.end); + if (localVariableNode == null || start < pos && end > pos) + { + localVariableNode = local; + } + } + + return localVariableNode; + } + + /** + * Fetches or generates the local variable table for the specified method. + * Since Mojang strip the local variable table as part of the obfuscation + * process, we need to generate the local variable table when running + * obfuscated. We cache the generated tables so that we only need to do the + * relatively expensive calculation once per method we encounter. + * + * @param classNode Containing class + * @param method Method + */ + public static List getLocalVariableTable(ClassNode classNode, MethodNode method) + { + if (method.localVariables.isEmpty()) + { + String signature = String.format("%s.%s%s", classNode.name, method.name, method.desc); + + List localVars = ByteCodeUtilities.calculatedLocalVariables.get(signature); + if (localVars != null) + { + return localVars; + } + + localVars = ByteCodeUtilities.generateLocalVariableTable(classNode, method); + ByteCodeUtilities.calculatedLocalVariables.put(signature, localVars); + return localVars; + } + + return method.localVariables; + } + + /** + * Use ASM Analyzer to generate the local variable table for the specified + * method. + * + * @param classNode Containing class + * @param method Method + */ + public static List generateLocalVariableTable(ClassNode classNode, MethodNode method) + { + List interfaces = null; + if (classNode.interfaces != null) + { + interfaces = new ArrayList(); + for (String iface : classNode.interfaces) + { + interfaces.add(Type.getObjectType(iface)); + } + } + + Type objectType = null; + if (classNode.superName != null) + { + objectType = Type.getObjectType(classNode.superName); + } + + // Use Analyzer to generate the bytecode frames + Analyzer analyzer = new Analyzer(new SimpleVerifier(Type.getObjectType(classNode.name), + objectType, interfaces, false)); + try + { + analyzer.analyze(classNode.name, method); + } + catch (AnalyzerException ex) + { + ex.printStackTrace(); + } + + // Get frames from the Analyzer + Frame[] frames = analyzer.getFrames(); + + // Record the original size of hte method + int methodSize = method.instructions.size(); + + // List of LocalVariableNodes to return + List localVariables = new ArrayList(); + + LocalVariableNode[] localNodes = new LocalVariableNode[method.maxLocals]; // LocalVariableNodes for current frame + BasicValue[] locals = new BasicValue[method.maxLocals]; // locals in previous frame, used to work out what changes between frames + LabelNode[] labels = new LabelNode[methodSize]; // Labels to add to the method, for the markers + + // Traverse the frames and work out when locals begin and end + for (int i = 0; i < methodSize; i++) + { + Frame f = frames[i]; + if (f == null) continue; + LabelNode label = null; + + for (int j = 0; j < f.getLocals(); j++) + { + BasicValue local = f.getLocal(j); + if (local == null && locals[j] == null) continue; + if (local != null && local.equals(locals[j])) continue; + + if (label == null) + { + label = new LabelNode(); + labels[i] = label; + } + + if (local == null && locals[j] != null) + { + localVariables.add(localNodes[j]); + localNodes[j].end = label; + localNodes[j] = null; + } + else if (local != null) + { + if (locals[j] != null) + { + localVariables.add(localNodes[j]); + localNodes[j].end = label; + localNodes[j] = null; + } + + String desc = (local.getType() != null) ? local.getType().getDescriptor() : null; + localNodes[j] = new LocalVariableNode("var" + j, desc, null, label, null, j); + } + + locals[j] = local; + } + } + + // Reached the end of the method so flush all current locals and mark the end + LabelNode label = null; + for (int k = 0; k < localNodes.length; k++) + { + if (localNodes[k] != null) + { + if (label == null) + { + label = new LabelNode(); + method.instructions.add(label); + } + + localNodes[k].end = label; + localVariables.add(localNodes[k]); + } + } + + // Insert generated labels into the method body + for (int n = methodSize - 1; n >= 0; n--) + { + if (labels[n] != null) + { + method.instructions.insert(method.instructions.get(n), labels[n]); + } + } + + return localVariables; + } + + /** + * Get the source code name for the specified type. + * + * @param type Type to generate a friendly name for + * @return String representation of the specified type, eg "int" for an + * integer primitive or "String" for java.lang.String + */ + public static String getTypeName(Type type) + { + switch (type.getSort()) + { + case Type.BOOLEAN: return "boolean"; + case Type.CHAR: return "char"; + case Type.BYTE: return "byte"; + case Type.SHORT: return "short"; + case Type.INT: return "int"; + case Type.FLOAT: return "float"; + case Type.LONG: return "long"; + case Type.DOUBLE: return "double"; + case Type.ARRAY: return ByteCodeUtilities.getTypeName(type.getElementType()) + "[]"; + case Type.OBJECT: + String typeName = type.getClassName(); + typeName = typeName.substring(typeName.lastIndexOf('.') + 1); + return typeName; + default: + return "Object"; + } + + } + + /** + * Finds a method in the target class, uses names specified in the + * {@link Obfuscated} annotation if present. + * + * @param targetClass Class to search in + * @param searchFor Method to search for + */ + public static MethodNode findTargetMethod(ClassNode targetClass, MethodNode searchFor) + { + for (MethodNode target : targetClass.methods) + { + if (target.name.equals(searchFor.name) && target.desc.equals(searchFor.desc)) + { + return target; + } + } + + AnnotationNode obfuscatedAnnotation = ByteCodeUtilities.getVisibleAnnotation(searchFor, Obfuscated.class); + if (obfuscatedAnnotation != null) + { + for (String obfuscatedName : ByteCodeUtilities.>getAnnotationValue(obfuscatedAnnotation)) + { + for (MethodNode target : targetClass.methods) + { + if (target.name.equals(obfuscatedName) && target.desc.equals(searchFor.desc)) + { + return target; + } + } + } + } + + return null; + } + + /** + * Finds a field in the target class, uses names specified in the + * {@link Obfuscated} annotation if present + * + * @param targetClass Class to search in + * @param searchFor Field to search for + */ + public static FieldNode findTargetField(ClassNode targetClass, FieldNode searchFor) + { + for (FieldNode target : targetClass.fields) + { + if (target.name.equals(searchFor.name)) + { + return target; + } + } + + AnnotationNode obfuscatedAnnotation = ByteCodeUtilities.getVisibleAnnotation(searchFor, Obfuscated.class); + if (obfuscatedAnnotation != null) + { + for (String obfuscatedName : ByteCodeUtilities.>getAnnotationValue(obfuscatedAnnotation)) + { + for (FieldNode target : targetClass.fields) + { + if (target.name.equals(obfuscatedName)) + { + return target; + } + } + } + } + + return null; + } + + /** + * Find a method in the target class which matches the specified method name + * and descriptor + * + * @param classNode + * @param searchFor + * @param desc + */ + public static MethodNode findMethod(ClassNode classNode, Obf searchFor, String desc) + { + int ordinal = 0; + + for (MethodNode method : classNode.methods) + { + if (searchFor.matches(method.name, ordinal++) && method.desc.equals(desc)) + { + return method; + } + } + + return null; + } + + /** + * Find a field in the target class which matches the specified field name + * + * @param classNode + * @param searchFor + */ + public static FieldNode findField(ClassNode classNode, Obf searchFor) + { + int ordinal = 0; + + for (FieldNode field : classNode.fields) + { + if (searchFor.matches(field.name, ordinal++)) + { + return field; + } + } + + return null; + } + + public static ClassNode loadClass(String className) throws IOException + { + return ByteCodeUtilities.loadClass(className, true, null); + } + + public static ClassNode loadClass(String className, boolean runTransformers) throws IOException + { + return ByteCodeUtilities.loadClass(className, runTransformers, null); + } + + public static ClassNode loadClass(String className, IClassTransformer source) throws IOException + { + return ByteCodeUtilities.loadClass(className, source != null, source); + } + + public static ClassNode loadClass(String className, boolean runTransformers, IClassTransformer source) throws IOException + { + byte[] bytes = Launch.classLoader.getClassBytes(className); + + if (runTransformers) + { + bytes = ByteCodeUtilities.applyTransformers(className, bytes, source); + } + + return ByteCodeUtilities.readClass(bytes); + } + + public static ClassNode readClass(byte[] basicClass) + { + ClassReader classReader = new ClassReader(basicClass); + ClassNode classNode = new ClassNode(); + classReader.accept(classNode, ClassReader.EXPAND_FRAMES); + return classNode; + } + + public static byte[] applyTransformers(String className, byte[] basicClass) + { + return ByteCodeUtilities.applyTransformers(className, basicClass, null); + } + + public static byte[] applyTransformers(String className, byte[] basicClass, IClassTransformer source) + { + final List transformers = Launch.classLoader.getTransformers(); + + for (final IClassTransformer transformer : transformers) + { + if (transformer != source) + { + basicClass = transformer.transform(className, className, basicClass); + } + } + + return basicClass; + } + + /** + * Get an annotation of the specified class from the supplied field node + */ + public static AnnotationNode getVisibleAnnotation(FieldNode field, Class annotationClass) + { + return ByteCodeUtilities.getAnnotation(field.visibleAnnotations, Type.getDescriptor(annotationClass)); + } + + /** + * Get an annotation of the specified class from the supplied field node + */ + public static AnnotationNode getInvisibleAnnotation(FieldNode field, Class annotationClass) + { + return ByteCodeUtilities.getAnnotation(field.invisibleAnnotations, Type.getDescriptor(annotationClass)); + } + + /** + * Get an annotation of the specified class from the supplied method node + */ + public static AnnotationNode getVisibleAnnotation(MethodNode method, Class annotationClass) + { + return ByteCodeUtilities.getAnnotation(method.visibleAnnotations, Type.getDescriptor(annotationClass)); + } + + /** + * Get an annotation of the specified class from the supplied method node + */ + public static AnnotationNode getInvisibleAnnotation(MethodNode method, Class annotationClass) + { + return ByteCodeUtilities.getAnnotation(method.invisibleAnnotations, Type.getDescriptor(annotationClass)); + } + + /** + * Get an annotation of the specified class from the supplied class node + */ + public static AnnotationNode getVisibleAnnotation(ClassNode classNode, Class annotationClass) + { + return ByteCodeUtilities.getAnnotation(classNode.visibleAnnotations, Type.getDescriptor(annotationClass)); + } + + /** + * Get an annotation of the specified class from the supplied class node + */ + public static AnnotationNode getInvisibleAnnotation(ClassNode classNode, Class annotationClass) + { + return ByteCodeUtilities.getAnnotation(classNode.invisibleAnnotations, Type.getDescriptor(annotationClass)); + } + + /** + * Get an annotation of the specified class from the supplied list of + * annotations, returns null if no matching annotation was found + */ + public static AnnotationNode getAnnotation(List annotations, String annotationType) + { + if (annotations != null) + { + for (AnnotationNode annotation : annotations) + { + if (annotationType.equals(annotation.desc)) + { + return annotation; + } + } + } + + return null; + } + + /** + * Get the value of an annotation node (the value at key "value") + * + * @param annotation Annotation node to inspect + */ + public static T getAnnotationValue(AnnotationNode annotation) + { + return ByteCodeUtilities.getAnnotationValue(annotation, "value"); + } + + /** + * Get the value of an annotation node + * + * @param annotation + * @param key + */ + @SuppressWarnings("unchecked") + public static T getAnnotationValue(AnnotationNode annotation, String key) + { + if (annotation == null || annotation.values == null) + { + return null; + } + + boolean getNextValue = false; + for (Object value : annotation.values) + { + if (getNextValue) return (T)value; + if (value.equals(key)) getNextValue = true; + } + return null; + } + + /** + * @param returnType + * @param args + */ + public static String generateDescriptor(Type returnType, Object... args) + { + return ByteCodeUtilities.generateDescriptor(Obf.MCP, returnType, args); + } + + /** + * @param returnType + * @param args + */ + public static String generateDescriptor(Obf returnType, Object... args) + { + return ByteCodeUtilities.generateDescriptor(Obf.MCP, returnType, args); + } + + /** + * @param returnType + * @param args + */ + public static String generateDescriptor(String returnType, Object... args) + { + return ByteCodeUtilities.generateDescriptor(Obf.MCP, returnType, args); + } + + /** + * @param obfType + * @param returnType + * @param args + */ + public static String generateDescriptor(int obfType, Object returnType, Object... args) + { + StringBuilder sb = new StringBuilder().append('('); + + for (Object arg : args) + { + sb.append(ByteCodeUtilities.toDescriptor(obfType, arg)); + } + + return sb.append(')').append(returnType != null ? ByteCodeUtilities.toDescriptor(obfType, returnType) : "V").toString(); + } + + /** + * @param obfType + * @param arg + */ + private static String toDescriptor(int obfType, Object arg) + { + if (arg instanceof Obf) + { + return ((Obf)arg).getDescriptor(obfType); + } + else if (arg instanceof String) + { + return (String)arg; + } + else if (arg instanceof Type) + { + return arg.toString(); + } + else if (arg instanceof Class) + { + return Type.getDescriptor((Class)arg).toString(); + } + + return arg == null ? "" : arg.toString(); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/Callback.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/Callback.java new file mode 100644 index 00000000..51b6d2dc --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/Callback.java @@ -0,0 +1,281 @@ +package com.mumfrey.liteloader.transformers; + +import java.util.ArrayList; +import java.util.List; + +import com.mumfrey.liteloader.core.runtime.Obf; + +/** + * Target information for injected callback methods + * + * @author Adam Mummery-Smith + */ +public class Callback +{ + /** + * Type of callback to inject + */ + public enum CallbackType + { + /** + * Redirect callbacks are injected at the start of a method and + * immediately return, thus short-circuiting the method + */ + REDIRECT(true), + + /** + * Event callbacks are injected at the start of a method but do not + * alter the normal method behaviour. + */ + EVENT(false), + + /** + * Return callbacks are injected immediately prior to every RETURN + * opcode in a particular method. Callback handlers must have the SAME + * return type as the method containing the injected callback + */ + RETURN(false), + + /** + * A profiler callback injected at a profiler "startSection" invocation + */ + PROFILER_STARTSECTION(Obf.startSection, true), + + /** + * A profiler callback injected at a profiler "endSection" invocation + */ + PROFILER_ENDSECTION(Obf.endSection, false), + + /** + * A profiler callback injected at a profiler "endStartSection" + * invocation + */ + PROFILER_ENDSTARTSECTION(Obf.endStartSection, true); + + /** + * + */ + private final boolean injectReturn; + + /** + * + */ + private final boolean isProfilerCallback; + + /** + * + */ + private final boolean sectionRequired; + + /** + * + */ + private final Obf profilerMethod; + + private CallbackType(boolean returnFrom) + { + this.injectReturn = returnFrom; + this.isProfilerCallback = false; + this.profilerMethod = null; + this.sectionRequired = false; + } + + private CallbackType(Obf profilerMethod, boolean sectionRequired) + { + this.injectReturn = false; + this.isProfilerCallback = true; + this.profilerMethod = profilerMethod; + this.sectionRequired = sectionRequired; + } + + boolean injectReturn() + { + return this.injectReturn; + } + + boolean isProfilerCallback() + { + return this.isProfilerCallback; + } + + String getProfilerMethod(int obfType) + { + return this.profilerMethod != null ? this.profilerMethod.names[obfType] : ""; + } + + String getProfilerMethodSignature() + { + return this.sectionRequired ? "(Ljava/lang/String;)V" : "()V"; + } + + boolean isSectionRequired() + { + return this.sectionRequired; + } + + public String getSignature() + { + if (this == CallbackType.EVENT || this == CallbackType.REDIRECT) + { + return "head"; + } + + return this.name().toString().toLowerCase(); + } + } + + /** + * + */ + private final CallbackType callbackType; + + /** + * + */ + private final String sectionName; + + /** + * + */ + private final String profilerMethod; + + /** + * Callback class reference + */ + private final String callbackClass; + + /** + * Callback method name + */ + private final String callbackMethod; + + /** + * Return callbacks are injected before every RETURN opcode in the method, + * each RETURN is thus allocated a sequential refNumber which is passed to + * the callback method so that the callback handler can choose which RETURN + * it wishes to handle. + */ + int refNumber; + + /** + * + */ + private final List chainedCallbacks; + + public Callback(CallbackType callbackType, String callbackMethod, String callbackClass) + { + this(callbackType, callbackMethod, callbackClass, null, 0); + } + + /** + * A new callback method in the specified class + * + * @param callbackMethod Method to call, must be public, static and have the + * appropriate signature for the type of injected callback + * @param callbackClass Fully qualified name of the class containing the + * callback method, must also be public or visible from the calling + * package + */ + public Callback(CallbackType callbackType, String callbackMethod, String callbackClass, String section, int obfType) + { + if (section == null && callbackType.isSectionRequired()) + { + throw new RuntimeException(String.format("Callback of type %s requires a section name but no section name was provided", + callbackType.name())); + } + + this.callbackType = callbackType; + this.callbackClass = callbackClass.replace('.', '/'); + this.callbackMethod = callbackMethod; + this.sectionName = section; + this.chainedCallbacks = new ArrayList(); + this.profilerMethod = callbackType.getProfilerMethod(obfType); + } + + private Callback(Callback other, int refNumber) + { + this.callbackType = other.callbackType; + this.callbackClass = other.callbackClass; + this.callbackMethod = other.callbackMethod; + this.sectionName = other.sectionName; + this.chainedCallbacks = other.chainedCallbacks; + this.profilerMethod = other.profilerMethod; + this.refNumber = refNumber; + } + + public CallbackType getType() + { + return this.callbackType; + } + + public String getCallbackClass() + { + return this.callbackClass; + } + + public String getCallbackMethod() + { + return this.callbackMethod; + } + + public boolean injectReturn() + { + return this.callbackType.injectReturn(); + } + + public boolean isProfilerCallback() + { + return this.callbackType.isProfilerCallback(); + } + + public String getSectionName() + { + return this.sectionName; + } + + public String getProfilerMethod() + { + return this.profilerMethod; + } + + public String getProfilerMethodSignature() + { + return this.callbackType.getProfilerMethodSignature(); + } + + public Callback getNextCallback() + { + return new Callback(this, this.refNumber++); + } + + void addChainedCallback(Callback chained) + { + this.chainedCallbacks.add(chained); + } + + public List getChainedCallbacks() + { + return this.chainedCallbacks; + } + + @Override + public String toString() + { + return this.callbackMethod; + } + + @Override + public boolean equals(Object other) + { + if (other == null || !(other instanceof Callback)) return false; + Callback callback = (Callback)other; + return callback.callbackClass.equals(this.callbackClass) && callback.callbackMethod.equals(this.callbackMethod) + && callback.callbackType == this.callbackType; + } + + @Override + public int hashCode() + { + return super.hashCode(); + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/CallbackInjectionTransformer.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/CallbackInjectionTransformer.java new file mode 100644 index 00000000..f20684c3 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/CallbackInjectionTransformer.java @@ -0,0 +1,361 @@ +package com.mumfrey.liteloader.transformers; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.IntInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.Callback.CallbackType; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Transformer which injects callbacks by searching for profiler invocations + * and RETURN opcodes. + * + * @author Adam Mummery-Smith + * @deprecated Use Event Injection instead + */ +@Deprecated +public abstract class CallbackInjectionTransformer extends ClassTransformer +{ + /** + * Mappings for profiler method invocations + */ + private Map> profilerCallbackMappings = new HashMap>(); + + /** + * Mappings for pre-return and method start callbacks + */ + private Map> callbackMappings = new HashMap>(); + + public CallbackInjectionTransformer() + { + this.addCallbacks(); + } + + /** + * Subclasses must override this method and add their mappings + */ + protected abstract void addCallbacks(); + + /** + * @param className + * @param methodName + * @param methodSignature + * @param callback + */ + protected final void addCallback(String className, String methodName, String methodSignature, Callback callback) + { + if (callback.isProfilerCallback()) + { + if (!this.profilerCallbackMappings.containsKey(className)) + { + this.profilerCallbackMappings.put(className, new HashMap()); + } + + String signature = CallbackInjectionTransformer.generateSignature(className, methodName, methodSignature, callback.getProfilerMethod(), + callback.getProfilerMethodSignature(), callback.getSectionName()); + this.addCallbackMapping(this.profilerCallbackMappings.get(className), signature, callback); + } + else + { + if (!this.callbackMappings.containsKey(className)) + { + this.callbackMappings.put(className, new HashMap()); + } + + String signature = CallbackInjectionTransformer.generateSignature(className, methodName, methodSignature, callback.getType()); + this.addCallbackMapping(this.callbackMappings.get(className), signature, callback); + } + } + + /** + * @param callbacks + * @param signature + * @param callback + */ + private void addCallbackMapping(Map callbacks, String signature, Callback callback) + { + if (callbacks.containsKey(signature)) + { + Callback existingCallback = callbacks.get(signature); + if (existingCallback.equals(callback)) return; + + if (callback.injectReturn() || existingCallback.injectReturn()) + { + String errorMessage = String.format("Callback for %s is already defined for %s, cannot add %s", + signature, existingCallback, callback); + LiteLoaderLogger.severe(errorMessage); + throw new InjectedCallbackCollisionError(errorMessage); + } + + existingCallback.addChainedCallback(callback); + } + else + { + callbacks.put(signature, callback); + } + } + + /* (non-Javadoc) + * @see net.minecraft.launchwrapper.IClassTransformer + * #transform(java.lang.String, java.lang.String, byte[]) + */ + @Override + public final byte[] transform(String name, String transformedName, byte[] basicClass) + { + if (basicClass != null && this.profilerCallbackMappings.containsKey(transformedName) || this.callbackMappings.containsKey(transformedName)) + { + return this.injectCallbacks(basicClass, this.profilerCallbackMappings.get(transformedName), this.callbackMappings.get(transformedName)); + } + + return basicClass; + } + + /** + * @param basicClass + * @param profilerMappings + */ + private byte[] injectCallbacks(byte[] basicClass, Map profilerMappings, Map mappings) + { + ClassNode classNode = this.readClass(basicClass, true); + String className = classNode.name.replace('/', '.'); + String classType = Type.getObjectType(classNode.name).toString(); + + for (MethodNode method : classNode.methods) + { + int returnNumber = 0; + String section = null; + int methodReturnOpcode = Type.getReturnType(method.desc).getOpcode(Opcodes.IRETURN); + + if (mappings != null) + { + String headSignature = CallbackInjectionTransformer.generateSignature(classNode.name, method.name, method.desc, + CallbackType.REDIRECT); + if (mappings.containsKey(headSignature)) + { + Callback callback = mappings.get(headSignature); + InsnList callbackInsns = this.genCallbackInsns(classType, method, callback); + if (callbackInsns != null) + { + LiteLoaderLogger.info("Injecting %s callback for %s in class %s", callback.getType().name().toLowerCase(), + callback, className); + method.instructions.insert(callbackInsns); + if (callback.injectReturn()) continue; + } + } + } + + Map profilerCallbackInjectionNodes = new HashMap(); + + Iterator iter = method.instructions.iterator(); + AbstractInsnNode lastInsn = null; + while (iter.hasNext()) + { + AbstractInsnNode insn = iter.next(); + if (profilerMappings != null && insn.getOpcode() == Opcodes.INVOKEVIRTUAL) + { + MethodInsnNode invokeNode = (MethodInsnNode)insn; + if (Obf.Profiler.ref.equals(invokeNode.owner) || Obf.Profiler.obf.equals(invokeNode.owner)) + { + section = ""; + if (lastInsn instanceof LdcInsnNode) + { + section = ((LdcInsnNode)lastInsn).cst.toString(); + } + + String signature = CallbackInjectionTransformer.generateSignature(classNode.name, method.name, method.desc, invokeNode.name, + invokeNode.desc, section); + + if (profilerMappings.containsKey(signature)) + { + profilerCallbackInjectionNodes.put(invokeNode, profilerMappings.get(signature).getNextCallback()); + } + } + } + else if (mappings != null && insn.getOpcode() == methodReturnOpcode) + { + String returnSignature = CallbackInjectionTransformer.generateSignature(classNode.name, method.name, method.desc, + CallbackType.RETURN); + if (mappings.containsKey(returnSignature)) + { + Callback callback = mappings.get(returnSignature); + InsnList callbackInsns = this.genCallbackInsns(classType, method, callback, returnNumber++); + if (callbackInsns != null) + { + LiteLoaderLogger.info("Injecting method return callback for %s in class %s", callback, className); + method.instructions.insertBefore(insn, callbackInsns); + } + else + { + LiteLoaderLogger.severe("Skipping callback mapping %s because the return behaviour does not match the method signature", + returnSignature); + } + } + } + + lastInsn = insn; + } + + for (Entry profilerCallbackNode : profilerCallbackInjectionNodes.entrySet()) + { + Callback callback = profilerCallbackNode.getValue(); + + LiteLoaderLogger.info("Injecting profiler invocation callback for %s in class %s", callback, className); + InsnList injected = this.genProfilerCallbackInsns(new InsnList(), callback, callback.refNumber++); + method.instructions.insert(profilerCallbackNode.getKey(), injected); + } + } + + return this.writeClass(classNode); + } + + /** + * @param injected + * @param callback + * @param refNumber + */ + private InsnList genProfilerCallbackInsns(InsnList injected, Callback callback, int refNumber) + { + injected.add(new LdcInsnNode(refNumber)); + injected.add(new MethodInsnNode(Opcodes.INVOKESTATIC, callback.getCallbackClass(), callback.getCallbackMethod(), "(I)V", false)); + + if (callback.getChainedCallbacks().size() > 0) + { + for (Callback chainedCallback : callback.getChainedCallbacks()) + this.genProfilerCallbackInsns(injected, chainedCallback, refNumber); + } + + return injected; + } + + /** + * Generate bytecode for injecting the specified callback into the specified + * methodNode. + * + * @param classType + * @param methodNode + * @param callback + */ + private InsnList genCallbackInsns(String classType, MethodNode methodNode, Callback callback) + { + return this.genCallbackInsns(classType, methodNode, callback, -1); + } + + /** + * Generate bytecode for injecting the specified callback into the specified + * methodNode. + * + * @param classType + * @param methodNode + * @param callback + * @param returnNumber + */ + private InsnList genCallbackInsns(String classType, MethodNode methodNode, Callback callback, int returnNumber) + { + return this.genCallbackInsns(new InsnList(), classType, methodNode, callback, returnNumber); + } + + /** + * @param injected + * @param classType + * @param methodNode + * @param callback + * @param returnNumber + */ + private InsnList genCallbackInsns(InsnList injected, String classType, MethodNode methodNode, Callback callback, int returnNumber) + { + // First work out some flags which alter the behaviour of this injection + boolean methodReturnsVoid = Type.getReturnType(methodNode.desc).equals(Type.VOID_TYPE); + boolean methodIsStatic = (methodNode.access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC; + boolean hasReturnRef = returnNumber > -1; + + // Generate the parts of the callback signature that we need + Type callbackReturnType = Type.getReturnType(methodNode.desc); + String callbackReturnValueArg = methodReturnsVoid ? "" : callbackReturnType.toString(); + String classInstanceArg = methodIsStatic ? "" : classType; + + // If this is a pre-return injection, push the invocation reference onto the call stack + if (hasReturnRef) injected.insert(new IntInsnNode(Opcodes.BIPUSH, returnNumber)); + + // If the method is non-static, then we pass in the class instance as an argument + if (!methodIsStatic) injected.add(new VarInsnNode(Opcodes.ALOAD, 0)); + + // Push the method arguments onto the stack + int argNumber = methodIsStatic ? 0 : 1; + for (Type type : Type.getArgumentTypes(methodNode.desc)) + { + injected.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), argNumber)); + argNumber += type.getSize(); + } + + // Generate the callback method descriptor + String callbackMethodDesc = String.format("(%s%s%s%s)%s", hasReturnRef ? callbackReturnValueArg : "", hasReturnRef ? "I" : "", + classInstanceArg, CallbackInjectionTransformer.getMethodArgs(methodNode), callbackReturnType); + + // Add the callback method insn to the injected instructions list + injected.add(new MethodInsnNode(Opcodes.INVOKESTATIC, callback.getCallbackClass(), callback.getCallbackMethod(), callbackMethodDesc, false)); + + // If the callback RETURNs a value then push the appropriate RETURN opcode into the insns list + if (callback.injectReturn()) + { + injected.add(new InsnNode(callbackReturnType.getOpcode(Opcodes.IRETURN))); + } + else if (callback.getChainedCallbacks().size() > 0) + { + for (Callback chainedCallback : callback.getChainedCallbacks()) + { + this.genCallbackInsns(injected, classType, methodNode, chainedCallback, returnNumber); + } + } + + // return the generated code + return injected; + } + + /** + * @param method + */ + private static String getMethodArgs(MethodNode method) + { + return method.desc.substring(1, method.desc.lastIndexOf(')')); + } + + /** + * @param className + * @param methodName + * @param methodSignature + * @param invokeName + * @param invokeSig + * @param section + */ + private static String generateSignature(String className, String methodName, String methodSignature, + String invokeName, String invokeSig, String section) + { + return String.format("%s::%s%s@%s%s/%s", className.replace('.', '/'), methodName, methodSignature, invokeName, invokeSig, section); + } + + /** + * @param className + * @param methodName + * @param methodSignature + * @param callbackType + */ + private static String generateSignature(String className, String methodName, String methodSignature, Callback.CallbackType callbackType) + { + return String.format("%s::%s%s@%s", className.replace('.', '/'), methodName, methodSignature, callbackType.getSignature()); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ClassOverlayTransformer.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ClassOverlayTransformer.java new file mode 100644 index 00000000..5589965e --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ClassOverlayTransformer.java @@ -0,0 +1,537 @@ +package com.mumfrey.liteloader.transformers; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.minecraft.launchwrapper.Launch; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.RemappingClassAdapter; +import org.objectweb.asm.commons.SimpleRemapper; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.LineNumberNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * This transformer applies one class to another as an "overlay". This works by + * merging down and replacing all methods and fields from the "overlay" class + * into the "target" class being transformed. Fields and methods marked with the + * {@link Obfuscated} annotation will search through the list of provided names + * to find a matching member in the target class, this allows methods and fields + * in the target class to be referenced even if they have different names after + * obfuscation. + * + *

      The "target" class is identified by a special field which must be named + * __TARGET in the overlay class which must be a private static field + * of the appropriate target type.

      + * + *

      Notes:

      + * + *
        + *
      • Constructors WILL NOT BE overlaid, see below for instruction merging. + * Constructors in the overlay class should throw an InstantiationError. + *
      • + * + *
      • Static method invocations will not be processed by "transformMethod", + * this means that any static methods invoked must be accessible from the + * context of the transformed class (eg. public or package-private in the + * same package).
      • + * + *
      • The overlay class MUST be a sibling of the target class to ensure + * that calls to super.xxx are properly transformed. In other words the + * overlay and the transformed class should have the same parent class + * although they need not be in the same package unless any package-private + * members are accessed.
      • + * + *
      • It is also possible to merge instructions from a "source" method into + * a specific method in the transformed class by annotating the method with + * a {@link AppendInsns} annotation, specifying the name of the target + * method as the annotation value. The target method signature must match + * the source method's signature and both methods must return VOID. The + * instructions from the source method will be inserted immediately before + * the RETURN opcode in the target method.
      • + * + *
      • To create a method stub for private methods you wish to invoke in the + * target class, decorate the stub method with an {@link Stub} annotation, + * this will cause the overlay transformer to NOT merge the method into the + * target, but merely verify that it exists in the target class.
      • + * + *
      • Merge instructions into the constructor by specifying "" as the + * target method name.
      • + *
      + * + * @author Adam Mummery-Smith + * @deprecated Use mixins instead! + */ +@Deprecated +public abstract class ClassOverlayTransformer extends ClassTransformer +{ + /** + * Global list of overlaid classes, used to transform references in other + * classes. + */ + private static final Map overlayMap = new HashMap(); + + /** + * Remapper for dynamically renaming references to overlays in other classes + */ + private static SimpleRemapper referenceRemapper; + + /** + * The first ClassOverlayTransformer to be instantiated accepts + * responsibility for performing remapping operations and becomes the + * "remapping agent" transformer. This flag is set to true to indicate that + * this instance is the remapping agent. + */ + private boolean remappingAgent = false; + + /** + * Name of the overlay class + */ + private final String overlayClassName, overlayClassRef; + + /** + * Target class to be transformed + */ + private final String targetClassName; + + /** + * Fields which get a different name from an {@link Obfuscated} annotation + */ + private final Map renamedFields = new HashMap(); + + /** + * Methods which get a different name from an {@link Obfuscated} annotation + */ + private final Map renamedMethods = new HashMap(); + + /** + * True to set the sourceFile property when applying the overlay + */ + protected boolean setSourceFile = true; + + /** + * @param overlayClassName + */ + protected ClassOverlayTransformer(String overlayClassName) + { + this.overlayClassName = overlayClassName; + this.overlayClassRef = overlayClassName.replace('.', '/'); + + String targetClassName = null; + ClassNode overlayClass = this.loadOverlayClass("", true); + for (FieldNode field : overlayClass.fields) + { + if ("__TARGET".equals(field.name) && ((field.access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC)) + { + targetClassName = Type.getType(field.desc).getClassName(); + } + } + + if (targetClassName == null) + { + throw new RuntimeException(String.format("Overlay class %s is missing a __TARGET field, unable to identify target class", + this.overlayClassName)); + } + + this.targetClassName = targetClassName; + ClassOverlayTransformer.overlayMap.put(this.overlayClassRef, this.targetClassName.replace('.', '/')); + + // If this is the first ClassOverlayTransformer, the referenceMapper will be null + if (ClassOverlayTransformer.referenceRemapper == null) + { + // Therefore create the referenceMapper and accept responsibility for class remapping + ClassOverlayTransformer.referenceRemapper = new SimpleRemapper(ClassOverlayTransformer.overlayMap); + this.remappingAgent = true; + } + } + + /* (non-Javadoc) + * @see net.minecraft.launchwrapper.IClassTransformer + * #transform(java.lang.String, java.lang.String, byte[]) + */ + @Override + public byte[] transform(String name, String transformedName, byte[] basicClass) + { + if (this.targetClassName != null && this.targetClassName.equals(transformedName)) + { + try + { + return this.applyOverlay(transformedName, basicClass); + } + catch (InvalidOverlayException th) + { + LiteLoaderLogger.severe(th, "Class overlay failed: %s %s", th.getClass().getName(), th.getMessage()); + th.printStackTrace(); + } + } + else if (this.overlayClassName.equals(transformedName)) + { + throw new RuntimeException(String.format("%s is an overlay class and cannot be referenced directly", this.overlayClassName)); + } + else if (this.remappingAgent && basicClass != null) + { + return this.remapClass(transformedName, basicClass); + } + + return basicClass; + } + + /** + * Remap references to overlay classes in other classes to the overlay class + * + * @param transformedName + * @param basicClass + */ + private byte[] remapClass(String transformedName, byte[] basicClass) + { + ClassReader classReader = new ClassReader(basicClass); + ClassWriter classWriter = new ClassWriter(classReader, 0); + + RemappingClassAdapter remappingAdapter = new RemappingClassAdapter(classWriter, ClassOverlayTransformer.referenceRemapper); + classReader.accept(remappingAdapter, ClassReader.EXPAND_FRAMES); + + return classWriter.toByteArray(); + } + + /** + * Apply the overlay to the class described by basicClass + * + * @param transformedName + * @param classBytes + */ + protected byte[] applyOverlay(String transformedName, byte[] classBytes) + { + ClassNode overlayClass = this.loadOverlayClass(transformedName, true); + ClassNode targetClass = this.readClass(classBytes, true); + + LiteLoaderLogger.info("Applying overlay %s to %s", this.overlayClassName, transformedName); + + try + { + this.verifyClasses(targetClass, overlayClass); + this.overlayInterfaces(targetClass, overlayClass); + this.overlayAttributes(targetClass, overlayClass); + this.overlayFields(targetClass, overlayClass); + this.findRenamedMethods(targetClass, overlayClass); + this.overlayMethods(targetClass, overlayClass); + } + catch (Exception ex) + { + throw new InvalidOverlayException("Unexpecteded error whilst applying the overlay class", ex); + } + + this.postOverlayTransform(transformedName, targetClass, overlayClass); + + return this.writeClass(targetClass); + } + + protected void postOverlayTransform(String transformedName, ClassNode targetClass, ClassNode overlayClass) + { + // Stub + } + + /** + * Perform pre-flight checks on the overlay and target classes + * + * @param targetClass + * @param overlayClass + */ + protected void verifyClasses(ClassNode targetClass, ClassNode overlayClass) + { + if (targetClass.superName == null || overlayClass.superName == null || !targetClass.superName.equals(overlayClass.superName)) + { + throw new InvalidOverlayException("Overlay classes must have the same superclass as their target class"); + } + } + + /** + * Overlay interfaces implemented by the overlay class onto the target class + * + * @param targetClass + * @param overlayClass + */ + private void overlayInterfaces(ClassNode targetClass, ClassNode overlayClass) + { + for (String interfaceName : overlayClass.interfaces) + { + if (!targetClass.interfaces.contains(interfaceName)) + { + targetClass.interfaces.add(interfaceName); + } + } + } + + /** + * Overlay misc attributes from overlay class onto the target class + * + * @param targetClass + * @param overlayClass + */ + private void overlayAttributes(ClassNode targetClass, ClassNode overlayClass) + { + if (this.setSourceFile ) targetClass.sourceFile = overlayClass.sourceFile; + } + + /** + * Overlay fields from overlay class into the target class. It is vital that + * this is done before overlayMethods because we need to compute renamed + * fields so that transformMethod can rename field references in the + * method body. + * + * @param targetClass + * @param overlayClass + */ + private void overlayFields(ClassNode targetClass, ClassNode overlayClass) + { + for (FieldNode field : overlayClass.fields) + { + if ((field.access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC && (field.access & Opcodes.ACC_PRIVATE) != Opcodes.ACC_PRIVATE) + { + throw new InvalidOverlayException(String.format("Overlay classes cannot contain non-private static methods or fields, found %s", + field.name)); + } + + FieldNode target = ByteCodeUtilities.findTargetField(targetClass, field); + if (target == null) + { + targetClass.fields.add(field); + } + else + { + if (!target.desc.equals(field.desc)) + { + throw new InvalidOverlayException(String.format("The field %s in the target class has a conflicting signature", field.name)); + } + + if (!target.name.equals(field.name)) + { + this.renamedFields.put(field.name, target.name); + } + } + } + } + + /** + * Called before merging methods to build the map of original method names + * -> new method names, this is then used by transformMethod to remap. + * + * @param targetClass + * @param overlayClass + */ + private void findRenamedMethods(ClassNode targetClass, ClassNode overlayClass) + { + for (MethodNode overlayMethod : overlayClass.methods) + { + if (ByteCodeUtilities.getVisibleAnnotation(overlayMethod, Stub.class) != null + || (ByteCodeUtilities.getVisibleAnnotation(overlayMethod, AppendInsns.class) == null && !overlayMethod.name.startsWith("<"))) + { + this.checkRenameMethod(targetClass, overlayMethod); + } + } + } + + /** + * Overlay methods from the overlay class into the target class + * + * @param targetClass + * @param overlayClass + */ + private void overlayMethods(ClassNode targetClass, ClassNode overlayClass) + { + for (MethodNode overlayMethod : overlayClass.methods) + { + this.transformMethod(overlayMethod, overlayClass.name, targetClass.name); + + AnnotationNode appendAnnotation = ByteCodeUtilities.getVisibleAnnotation(overlayMethod, AppendInsns.class); + AnnotationNode stubAnnotation = ByteCodeUtilities.getVisibleAnnotation(overlayMethod, Stub.class); + + if (stubAnnotation != null) + { + MethodNode target = ByteCodeUtilities.findTargetMethod(targetClass, overlayMethod); + if (target == null) + { + throw new InvalidOverlayException(String.format("Stub method %s was not located in the target class", overlayMethod.name)); + } + } + else if (appendAnnotation != null) + { + String targetMethodName = ByteCodeUtilities.getAnnotationValue(appendAnnotation); + this.appendInsns(targetClass, targetMethodName, overlayMethod); + } + else if (!overlayMethod.name.startsWith("<")) + { + if ((overlayMethod.access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC + && (overlayMethod.access & Opcodes.ACC_PRIVATE) != Opcodes.ACC_PRIVATE) + { + continue; + } + + MethodNode target = ByteCodeUtilities.findTargetMethod(targetClass, overlayMethod); + if (target != null) targetClass.methods.remove(target); + targetClass.methods.add(overlayMethod); + } + else if ("".equals(overlayMethod.name)) + { + this.appendInsns(targetClass, overlayMethod.name, overlayMethod); + } + } + } + + /** + * Handles "re-parenting" the method supplied, changes all references to the + * overlay class to refer to the target class (for field accesses and method + * invocations) and also renames fields accesses to their obfuscated + * versions. + * + * @param method + * @param fromClass + * @param toClass + */ + private void transformMethod(MethodNode method, String fromClass, String toClass) + { + Iterator iter = method.instructions.iterator(); + while (iter.hasNext()) + { + AbstractInsnNode insn = iter.next(); + + if (insn instanceof MethodInsnNode) + { + MethodInsnNode methodInsn = (MethodInsnNode)insn; + if (methodInsn.owner.equals(fromClass)) + { + methodInsn.owner = toClass; + + String methodDescriptor = methodInsn.name + methodInsn.desc; + if (this.renamedMethods.containsKey(methodDescriptor)) + { + methodInsn.name = this.renamedMethods.get(methodDescriptor); + } + } + } + if (insn instanceof FieldInsnNode) + { + FieldInsnNode fieldInsn = (FieldInsnNode)insn; + if (fieldInsn.owner.equals(fromClass)) fieldInsn.owner = toClass; + + if (this.renamedFields.containsKey(fieldInsn.name)) + { + String newName = this.renamedFields.get(fieldInsn.name); + fieldInsn.name = newName; + } + } + } + } + + /** + * Handles appending instructions from the source method to the target + * method. + * + * @param targetClass + * @param targetMethodName + * @param sourceMethod + */ + private void appendInsns(ClassNode targetClass, String targetMethodName, MethodNode sourceMethod) + { + if (Type.getReturnType(sourceMethod.desc) != Type.VOID_TYPE) + { + throw new IllegalArgumentException("Attempted to merge insns into a method which does not return void"); + } + + if (targetMethodName == null || targetMethodName.length() == 0) targetMethodName = sourceMethod.name; + + Set obfuscatedNames = new HashSet(); + AnnotationNode obfuscatedAnnotation = ByteCodeUtilities.getVisibleAnnotation(sourceMethod, Obfuscated.class); + if (obfuscatedAnnotation != null) + { + obfuscatedNames.addAll(ByteCodeUtilities.>getAnnotationValue(obfuscatedAnnotation)); + } + + for (MethodNode method : targetClass.methods) + { + if ((targetMethodName.equals(method.name) || obfuscatedNames.contains(method.name)) && sourceMethod.desc.equals(method.desc)) + { + AbstractInsnNode returnNode = null; + Iterator findReturnIter = method.instructions.iterator(); + while (findReturnIter.hasNext()) + { + AbstractInsnNode insn = findReturnIter.next(); + if (insn.getOpcode() == Opcodes.RETURN) + { + returnNode = insn; + break; + } + } + + Iterator injectIter = sourceMethod.instructions.iterator(); + while (injectIter.hasNext()) + { + AbstractInsnNode insn = injectIter.next(); + if (!(insn instanceof LineNumberNode) && insn.getOpcode() != Opcodes.RETURN) + { + method.instructions.insertBefore(returnNode, insn); + } + } + } + } + } + + /** + * @param targetClass + * @param searchFor + */ + private void checkRenameMethod(ClassNode targetClass, MethodNode searchFor) + { + MethodNode target = ByteCodeUtilities.findTargetMethod(targetClass, searchFor); + if (target != null && !target.name.equals(searchFor.name)) + { + String methodDescriptor = searchFor.name + searchFor.desc; + this.renamedMethods.put(methodDescriptor, target.name); + searchFor.name = target.name; + } + } + + /** + * @param transformedName + * @throws InvalidOverlayException + */ + private ClassNode loadOverlayClass(String transformedName, boolean runTransformers) + { + byte[] overlayBytes = null; + + try + { + if ((overlayBytes = Launch.classLoader.getClassBytes(this.overlayClassName)) == null) + { + throw new InvalidOverlayException(String.format("The specified overlay '%s' was not found", this.overlayClassName)); + } + + if (runTransformers) + { + overlayBytes = ByteCodeUtilities.applyTransformers(this.overlayClassName, overlayBytes, this); + } + } + catch (IOException ex) + { + LiteLoaderLogger.severe("Failed to load overlay %s for %s, no overlay was applied", this.overlayClassName, transformedName); + throw new InvalidOverlayException("An error was encountered whilst loading the overlay class", ex); + } + + return this.readClass(overlayBytes, false); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ClassTransformer.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ClassTransformer.java new file mode 100644 index 00000000..a79a454c --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ClassTransformer.java @@ -0,0 +1,63 @@ +package com.mumfrey.liteloader.transformers; + +import net.minecraft.launchwrapper.IClassTransformer; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; + +/** + * Base class for transformers which work via ClassNode + * + * @author Adam Mummery-Smith + */ +public abstract class ClassTransformer implements IClassTransformer +{ + public static final String HORIZONTAL_RULE = + "----------------------------------------------------------------------------------------------------"; + + private ClassReader classReader; + private ClassNode classNode; + + /** + * @param basicClass + */ + protected final ClassNode readClass(byte[] basicClass, boolean cacheReader) + { + ClassReader classReader = new ClassReader(basicClass); + if (cacheReader) this.classReader = classReader; + + ClassNode classNode = new ClassNode(); + classReader.accept(classNode, ClassReader.EXPAND_FRAMES); + return classNode; + } + + /** + * @param classNode + */ + protected final byte[] writeClass(ClassNode classNode) + { + // Use optimised writer for speed + if (this.classReader != null && this.classNode == classNode) + { + this.classNode = null; + IsolatedClassWriter writer = new IsolatedClassWriter(this.classReader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + this.classReader = null; + classNode.accept(writer); + return writer.toByteArray(); + } + + this.classNode = null; + + IsolatedClassWriter writer = new IsolatedClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + classNode.accept(writer); + return writer.toByteArray(); + } + + protected static String getSimpleClassName(ClassNode classNode) + { + String className = classNode.name.replace('/', '.'); + int dotPos = className.lastIndexOf('.'); + return dotPos == -1 ? className : className.substring(dotPos + 1); + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/InjectedCallbackCollisionError.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/InjectedCallbackCollisionError.java new file mode 100644 index 00000000..0fc658b4 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/InjectedCallbackCollisionError.java @@ -0,0 +1,26 @@ +package com.mumfrey.liteloader.transformers; + +public class InjectedCallbackCollisionError extends Error +{ + private static final long serialVersionUID = 1L; + + public InjectedCallbackCollisionError() + { + } + + public InjectedCallbackCollisionError(String message) + { + super(message); + } + + public InjectedCallbackCollisionError(Throwable cause) + { + super(cause); + } + + public InjectedCallbackCollisionError(String message, Throwable cause) + { + super(message, cause); + } + +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/InvalidOverlayException.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/InvalidOverlayException.java new file mode 100644 index 00000000..0cda72c3 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/InvalidOverlayException.java @@ -0,0 +1,25 @@ +package com.mumfrey.liteloader.transformers; + +/** + * + * @author Adam Mummery-Smith + */ +public class InvalidOverlayException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public InvalidOverlayException(String message) + { + super(message); + } + + public InvalidOverlayException(Throwable cause) + { + super(cause); + } + + public InvalidOverlayException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/IsolatedClassWriter.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/IsolatedClassWriter.java new file mode 100644 index 00000000..69e30406 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/IsolatedClassWriter.java @@ -0,0 +1,22 @@ +package com.mumfrey.liteloader.transformers; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +/** + * ClassWriter isolated from ASM so that it exists in the LaunchClassLoader + * + * @author Adam Mummery-Smith + */ +public class IsolatedClassWriter extends ClassWriter +{ + public IsolatedClassWriter(int flags) + { + super(flags); + } + + public IsolatedClassWriter(ClassReader classReader, int flags) + { + super(classReader, flags); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ObfProvider.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ObfProvider.java new file mode 100644 index 00000000..448c7258 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/ObfProvider.java @@ -0,0 +1,21 @@ +package com.mumfrey.liteloader.transformers; + +import com.mumfrey.liteloader.core.runtime.Obf; + +/** + * Interface for dynamic (context-specific) obfuscation provider, used + * internally by ModEventInjectionTransformer to provide obf entries for the + * AccessorTransformer from JSON + * + * @author Adam Mummery-Smith + */ +public interface ObfProvider +{ + /** + * Try to locate an obfuscation table entry by name (id), returns null if no + * entry was found + * + * @param name + */ + public abstract Obf getByName(String name); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/Obfuscated.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/Obfuscated.java new file mode 100644 index 00000000..05f4f83c --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/Obfuscated.java @@ -0,0 +1,19 @@ +package com.mumfrey.liteloader.transformers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation which provides the obfuscated names for a method or field to the + * ClassOverlayTransformer. + * + * @author Adam Mummery-Smith + */ +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Obfuscated +{ + public String[] value(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/PacketHandlerException.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/PacketHandlerException.java new file mode 100644 index 00000000..5170f8a6 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/PacketHandlerException.java @@ -0,0 +1,30 @@ +package com.mumfrey.liteloader.transformers; + +import net.minecraft.network.Packet; + +/** + * Exception which a packet handler can throw in order to cancel further + * handling of the event. + * + * @author Adam Mummery-Smith + */ +public class PacketHandlerException extends RuntimeException +{ + private static final long serialVersionUID = -330946238844640302L; + + private Packet packet; + + public PacketHandlerException(Packet packet) + { + } + + public PacketHandlerException(Packet packet, String message) + { + super(message); + } + + public Packet getPacket() + { + return this.packet; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/Stub.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/Stub.java new file mode 100644 index 00000000..0cf5ab75 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/Stub.java @@ -0,0 +1,18 @@ +package com.mumfrey.liteloader.transformers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Interface which instructs the ClassOverlayTransformer to NOT merge the + * annotated method. + * + * @author Adam Mummery-Smith + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Stub +{ +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/Accessor.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/Accessor.java new file mode 100644 index 00000000..ceca9223 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/Accessor.java @@ -0,0 +1,19 @@ +package com.mumfrey.liteloader.transformers.access; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines an accessor method within an accessor injection interface, or an + * accessor interface itself. + * + * @author Adam Mummery-Smith + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface Accessor +{ + public String[] value(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/AccessorTransformer.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/AccessorTransformer.java new file mode 100644 index 00000000..a5fa5cd2 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/AccessorTransformer.java @@ -0,0 +1,584 @@ +package com.mumfrey.liteloader.transformers.access; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.minecraft.launchwrapper.Launch; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.ByteCodeUtilities; +import com.mumfrey.liteloader.transformers.ClassTransformer; +import com.mumfrey.liteloader.transformers.ObfProvider; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Transformer which can inject accessor methods defined by an annotated + * interface into a target class. + * + * @author Adam Mummery-Smith + */ +public abstract class AccessorTransformer extends ClassTransformer +{ + static final Pattern ordinalRefPattern = Pattern.compile("^#([0-9]{1,5})$"); + + /** + * An injection record + * + * @author Adam Mummery-Smith + */ + class AccessorInjection + { + /** + * Full name of the interface to inject + */ + private final String iface; + + /** + * Obfuscation table class specified by the interface + */ + private final Class table; + + /** + * Obfuscation provider for this context + */ + private final ObfProvider obfProvider; + + /** + * Target class to inject into + */ + private final Obf target; + + /** + * Create a new new accessor using the specified template interface + * + * @param iface Template interface + * @throws IOException Thrown if an problem occurs when loading the + * interface bytecode + */ + protected AccessorInjection(String iface) throws IOException + { + this(iface, null); + } + + /** + * Create a new new accessor using the specified template interface + * + * @param iface Template interface + * @param obfProvider Obfuscation provider for this context + * @throws IOException Thrown if an problem occurs when loading the + * interface bytecode + */ + protected AccessorInjection(String iface, ObfProvider obfProvider) throws IOException + { + ClassNode ifaceNode = ByteCodeUtilities.loadClass(iface, false); + + if (ifaceNode.interfaces.size() > 0) + { + String interfaceList = ifaceNode.interfaces.toString().replace('/', '.'); + throw new RuntimeException("Accessor interface must not extend other interfaces. Found " + interfaceList + " in " + iface); + } + + this.iface = iface; + this.obfProvider = obfProvider; + this.table = this.setupTable(ifaceNode); + this.target = this.setupTarget(ifaceNode); + } + + /** + * Get an obfuscation table mapping by name, first uses any supplied + * context provider, then any obfuscation table class specified by an + * {@link ObfTableClass} annotation on the interface itself, and fails + * over onto the LiteLoader obfuscation table. If the entry is not + * matched in any of the above locations then an exception is thrown. + * + * @param name Obfuscation table entry to fetch + */ + private Obf getObf(List names) + { + String name = names.get(0); + + Matcher ordinalPattern = AccessorTransformer.ordinalRefPattern.matcher(name); + if (ordinalPattern.matches()) + { + int ordinal = Integer.parseInt(ordinalPattern.group(1)); + return new Obf.Ord(ordinal); + } + + if (this.obfProvider != null) + { + Obf obf = this.obfProvider.getByName(name); + if (obf != null) + { + return obf; + } + } + + Obf obf = Obf.getByName(this.table, name); + if (obf != null) + { + return obf; + } + + if (names.size() > 0 && names.size() < 4) + { + String name2 = names.size() > 1 ? names.get(1) : name; + String name3 = names.size() > 2 ? names.get(2) : name; + return new AccessorTransformer.Mapping(name, name2, name3); + } + + throw new RuntimeException("Invalid obfuscation table entry specified: '" + names + "'"); + } + + /** + * Get the target class of this injection + */ + protected Obf getTarget() + { + return this.target; + } + + /** + * Inspects the target class for an {@link ObfTableClass} annotation and + * attempts to get a handle for the class specified. On failure, the + * LiteLoader {@link Obf} is returned. + */ + @SuppressWarnings("unchecked") + private Class setupTable(ClassNode ifaceNode) + { + AnnotationNode annotation = ByteCodeUtilities.getInvisibleAnnotation(ifaceNode, ObfTableClass.class); + if (annotation != null) + { + try + { + Type obfTableType = ByteCodeUtilities.getAnnotationValue(annotation); + return (Class)Class.forName(obfTableType.getClassName(), true, Launch.classLoader); + } + catch (ClassNotFoundException ex) + { + ex.printStackTrace(); + } + } + + return Obf.class; + } + + /** + * Locates the {@link Accessor} annotation on the interface in order to + * determine the target class. + */ + private Obf setupTarget(ClassNode ifaceNode) + { + AnnotationNode annotation = ByteCodeUtilities.getInvisibleAnnotation(ifaceNode, Accessor.class); + if (annotation == null) + { + throw new RuntimeException("Accessor interfaces must be annotated with an @Accessor annotation specifying the target class"); + } + + List targetClass = ByteCodeUtilities.>getAnnotationValue(annotation); + if (targetClass == null || targetClass.isEmpty()) + { + throw new RuntimeException("Invalid @Accessor annotation, the annotation must specify a target class"); + } + + return this.getObf(targetClass); + } + + /** + * Apply this injection to the specified target ClassNode + * + * @param classNode Class tree to apply to + */ + protected void apply(ClassNode classNode) + { + String ifaceRef = this.iface.replace('.', '/'); + + if (classNode.interfaces.contains(ifaceRef)) + { + LiteLoaderLogger.debug("[AccessorTransformer] Skipping %s because %s was already applied", classNode.name, this.iface); + return; + } + + classNode.interfaces.add(ifaceRef); + + try + { + LiteLoaderLogger.debug("[AccessorTransformer] Loading %s", this.iface); + ClassNode ifaceNode = ByteCodeUtilities.loadClass(this.iface, AccessorTransformer.this); + + for (MethodNode method : ifaceNode.methods) + { + this.addMethod(classNode, method); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + /** + * Add a method from the interface to the target class + * + * @param classNode Target class + * @param method Method to add + */ + private void addMethod(ClassNode classNode, MethodNode method) + { + if (!this.addMethodToClass(classNode, method)) + { + LiteLoaderLogger.debug("[AccessorTransformer] Method %s already exists in %s", method.name, classNode.name); + return; + } + + LiteLoaderLogger.debug("[AccessorTransformer] Attempting to add %s to %s", method.name, classNode.name); + + List targetId = null; + AnnotationNode accessor = ByteCodeUtilities.getInvisibleAnnotation(method, Accessor.class); + AnnotationNode invoker = ByteCodeUtilities.getInvisibleAnnotation(method, Invoker.class); + if (accessor != null) + { + targetId = ByteCodeUtilities.>getAnnotationValue(accessor); + Obf target = this.getObf(targetId); + if (this.injectAccessor(classNode, method, target)) return; + } + else if (invoker != null) + { + targetId = ByteCodeUtilities.>getAnnotationValue(invoker); + Obf target = this.getObf(targetId); + if (this.injectInvoker(classNode, method, target)) return; + } + else + { + LiteLoaderLogger.severe("[AccessorTransformer] Method %s for %s has no @Accessor or @Invoker annotation, the method will " + + "be ABSTRACT!", method.name, this.iface); + this.injectException(classNode, method, "No @Accessor or @Invoker annotation on method"); + return; + } + + LiteLoaderLogger.severe("[AccessorTransformer] Method %s for %s could not locate target member, the method will be ABSTRACT!", + method.name, this.iface); + this.injectException(classNode, method, "Could not locate target class member '" + targetId + "'"); + } + + /** + * Inject an accessor method into the target class + * + * @param classNode + * @param method + * @param targetName + */ + private boolean injectAccessor(ClassNode classNode, MethodNode method, Obf target) + { + FieldNode targetField = ByteCodeUtilities.findField(classNode, target); + if (targetField != null) + { + LiteLoaderLogger.debug("[AccessorTransformer] Found field %s for %s", targetField.name, method.name); + if (Type.getReturnType(method.desc) != Type.VOID_TYPE) + { + this.populateGetter(classNode, method, targetField); + } + else + { + this.populateSetter(classNode, method, targetField); + } + + return true; + } + + return false; + } + + /** + * Inject an invoke (proxy) method into the target class + * + * @param classNode + * @param method + * @param targetName + */ + private boolean injectInvoker(ClassNode classNode, MethodNode method, Obf target) + { + MethodNode targetMethod = ByteCodeUtilities.findMethod(classNode, target, method.desc); + if (targetMethod != null) + { + LiteLoaderLogger.debug("[AccessorTransformer] Found method %s for %s", targetMethod.name, method.name); + this.populateInvoker(classNode, method, targetMethod); + return true; + } + + return false; + } + + /** + * Populate the bytecode instructions for a getter accessor + * + * @param classNode + * @param method + * @param field + */ + private void populateGetter(ClassNode classNode, MethodNode method, FieldNode field) + { + Type returnType = Type.getReturnType(method.desc); + Type fieldType = Type.getType(field.desc); + if (!returnType.equals(fieldType)) + { + throw new RuntimeException("Incompatible types! Field type: " + fieldType + " Method type: " + returnType); + } + boolean isStatic = (field.access & Opcodes.ACC_STATIC) != 0; + + method.instructions.clear(); + method.maxLocals = ByteCodeUtilities.getFirstNonArgLocalIndex(method); + method.maxStack = fieldType.getSize(); + + if (isStatic) + { + method.instructions.add(new FieldInsnNode(Opcodes.GETSTATIC, classNode.name, field.name, field.desc)); + } + else + { + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + method.instructions.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, field.name, field.desc)); + } + + method.instructions.add(new InsnNode(returnType.getOpcode(Opcodes.IRETURN))); + } + + /** + * Populate the bytecode instructions for a setter + * + * @param classNode + * @param method + * @param field + */ + private void populateSetter(ClassNode classNode, MethodNode method, FieldNode field) + { + Type[] argTypes = Type.getArgumentTypes(method.desc); + if (argTypes.length != 1) + { + throw new RuntimeException("Invalid setter! " + method.name + " must take exactly one argument"); + } + Type argType = argTypes[0]; + Type fieldType = Type.getType(field.desc); + if (!argType.equals(fieldType)) + { + throw new RuntimeException("Incompatible types! Field type: " + fieldType + " Method type: " + argType); + } + boolean isStatic = (field.access & Opcodes.ACC_STATIC) != 0; + + method.instructions.clear(); + method.maxLocals = ByteCodeUtilities.getFirstNonArgLocalIndex(method); + method.maxStack = fieldType.getSize(); + + if (isStatic) + { + method.instructions.add(new VarInsnNode(argType.getOpcode(Opcodes.ILOAD), 0)); + method.instructions.add(new FieldInsnNode(Opcodes.PUTSTATIC, classNode.name, field.name, field.desc)); + } + else + { + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + method.instructions.add(new VarInsnNode(argType.getOpcode(Opcodes.ILOAD), 1)); + method.instructions.add(new FieldInsnNode(Opcodes.PUTFIELD, classNode.name, field.name, field.desc)); + } + + method.instructions.add(new InsnNode(Opcodes.RETURN)); + } + + /** + * Populate the bytecode instructions for an invoker (proxy) method + * + * @param classNode + * @param method + * @param targetMethod + */ + private void populateInvoker(ClassNode classNode, MethodNode method, MethodNode targetMethod) + { + Type[] args = Type.getArgumentTypes(targetMethod.desc); + Type returnType = Type.getReturnType(targetMethod.desc); + boolean isStatic = (targetMethod.access & Opcodes.ACC_STATIC) != 0; + + method.instructions.clear(); + method.maxStack = (method.maxLocals = ByteCodeUtilities.getFirstNonArgLocalIndex(method)) + 1; + + if (isStatic) + { + ByteCodeUtilities.loadArgs(args, method.instructions, 0); + method.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, classNode.name, targetMethod.name, targetMethod.desc, false)); + } + else + { + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + ByteCodeUtilities.loadArgs(args, method.instructions, 1); + method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, classNode.name, targetMethod.name, targetMethod.desc, false)); + } + + method.instructions.add(new InsnNode(returnType.getOpcode(Opcodes.IRETURN))); + } + + /** + * Populate bytecode instructions for a method which throws an exception + * + * @param classNode + * @param method + * @param message + */ + private void injectException(ClassNode classNode, MethodNode method, String message) + { + InsnList insns = method.instructions; + method.maxStack = 2; + + insns.clear(); + insns.add(new TypeInsnNode(Opcodes.NEW, "java/lang/RuntimeException")); + insns.add(new InsnNode(Opcodes.DUP)); + insns.add(new LdcInsnNode(message)); + insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/RuntimeException", "", "(Ljava/lang/String;)V", false)); + insns.add(new InsnNode(Opcodes.ATHROW)); + } + + /** + * Add a method from the template interface to the target class + * + * @param classNode + * @param method + */ + private boolean addMethodToClass(ClassNode classNode, MethodNode method) + { + MethodNode existingMethod = ByteCodeUtilities.findTargetMethod(classNode, method); + if (existingMethod != null) return false; + classNode.methods.add(method); + method.access = method.access & ~Opcodes.ACC_ABSTRACT; + return true; + } + } + + protected static class Mapping extends Obf + { + protected Mapping(String seargeName, String obfName, String mcpName) + { + super(seargeName, obfName, mcpName); + } + } + + /** + * List of accessors to inject + */ + private final List accessors = new ArrayList(); + + /** + * ctor + */ + public AccessorTransformer() + { + this.addAccessors(); + } + + /** + * @param interfaceName + */ + public void addAccessor(String interfaceName) + { + this.addAccessor(interfaceName, null); + } + + /** + * Add an accessor to the accessors list + * + * @param interfaceName + * @param obfProvider + */ + public void addAccessor(String interfaceName, ObfProvider obfProvider) + { + try + { + this.accessors.add(new AccessorInjection(interfaceName, obfProvider)); + } + catch (Exception ex) + { + LiteLoaderLogger.debug(ex); + } + } + + /* (non-Javadoc) + * @see net.minecraft.launchwrapper.IClassTransformer + * #transform(java.lang.String, java.lang.String, byte[]) + */ + @Override + public byte[] transform(String name, String transformedName, byte[] basicClass) + { + ClassNode classNode = null; + + classNode = this.apply(name, transformedName, basicClass, classNode); + + if (classNode != null) + { + this.postTransform(name, transformedName, classNode); + return this.writeClass(classNode); + } + + return basicClass; + } + + /** + * Apply this transformer, used when this transformer is acting as a + * delegate via another transformer (eg. an EventTransformer) and the parent + * transformer already has a ClassNode for the target class. + * + * @param name + * @param transformedName + * @param basicClass + * @param classNode + */ + public ClassNode apply(String name, String transformedName, byte[] basicClass, ClassNode classNode) + { + for (Iterator iter = this.accessors.iterator(); iter.hasNext(); ) + { + AccessorInjection accessor = iter.next(); + Obf target = accessor.getTarget(); + if (target.obf.equals(transformedName) || target.name.equals(transformedName)) + { + LiteLoaderLogger.debug("[AccessorTransformer] Processing access injections in %s", transformedName); + if (classNode == null) classNode = this.readClass(basicClass, true); + accessor.apply(classNode); + iter.remove(); + } + } + + return classNode; + } + + /** + * Subclasses should add their accessors here + */ + protected void addAccessors() + { + } + + /** + * Called after transformation is applied, allows custom transforms to be + * performed by subclasses. + * + * @param name + * @param transformedName + * @param classNode + */ + protected void postTransform(String name, String transformedName, ClassNode classNode) + { + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/Invoker.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/Invoker.java new file mode 100644 index 00000000..7ee807ff --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/Invoker.java @@ -0,0 +1,18 @@ +package com.mumfrey.liteloader.transformers.access; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines an invoker method within an accessor injection interface + * + * @author Adam Mummery-Smith + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface Invoker +{ + public String[] value(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/ObfTableClass.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/ObfTableClass.java new file mode 100644 index 00000000..95a8d5ff --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/access/ObfTableClass.java @@ -0,0 +1,21 @@ +package com.mumfrey.liteloader.transformers.access; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.mumfrey.liteloader.core.runtime.Obf; + +/** + * Defines the obfuscation table class to use for an accessor injection + * interface. + * + * @author Adam Mummery-Smith + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.CLASS) +public @interface ObfTableClass +{ + public Class value(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/Event.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/Event.java new file mode 100644 index 00000000..3eaaf97e --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/Event.java @@ -0,0 +1,728 @@ +package com.mumfrey.liteloader.transformers.event; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.ByteCodeUtilities; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * An injectable "event". An event is like a regular callback except that it is + * more intelligent about where it can be injected in the bytecode and also + * supports conditional "cancellation", which is the ability to conditionally + * return from the containing method with a custom return value. + * + * @author Adam Mummery-Smith + */ +public class Event implements Comparable +{ + /** + * Natural ordering of events, for use with sorting events which have the + * same priority. + */ + private static int eventOrder = 0; + + /** + * All the events which exist and their registered listeners + */ + private static final Set events = new HashSet(); + + private static final List>> proxyHandlerMethods = new ArrayList>>(); + + private static int proxyInnerClassIndex = 1; + + static + { + Event.resizeProxyList(); + } + + /** + * The name of this event + */ + protected final String name; + + /** + * Whether this event is cancellable - if it is cancellable then the + * isCancelled() -> RETURN code will be injected. + */ + protected final boolean cancellable; + + /** + * Natural order of this event, for sorting + */ + private final int order; + + /** + * Priority of this event, for sorting + */ + private final int priority; + + private Set listeners = new HashSet(); + + /** + * Method this event is currently "attached" to, we "attach" at the + * beginning of a method injection in order to save recalculating things + * like the return type and descriptor for each invocation, this means we + * need to calculate these things at most once for each method this event is + * injecting into. + */ + protected MethodNode method; + + /** + * Descriptor for this event in the context of the attached method + */ + protected String eventDescriptor; + + /** + * Method's original MAXS, used as a base to work out whether we need to + * increase the MAXS value. + */ + protected int methodMAXS = 0; + + /** + * True if the attached method is static, used so that we know whether to + * push "this" onto the stack when constructing the EventInfo, or "null" + */ + protected boolean methodIsStatic; + + /** + * Return type for the attached method, used to determine which EventInfo + * class to use and which method to invoke. + */ + protected Type methodReturnType; + + protected String eventInfoClass; + + protected Set pendingInjections; + + private int injectionCount = 0; + + protected boolean verbose; + + protected Event(String name, boolean cancellable, int priority) + { + this.name = name.toLowerCase(); + this.priority = priority; + this.order = Event.eventOrder++; + this.cancellable = cancellable; + this.verbose = true; + + if (Event.events.contains(this)) + { + throw new IllegalArgumentException("Event " + name + " is already defined"); + } + + Event.events.add(this); + } + + /** + * Creates a new event with the specified name, if an event with the + * specified name already exists then the existing event is returned + * instead. + * + * @param name Event name (case insensitive) + * @return new Event instance or existing Event instance + */ + public static Event getOrCreate(String name) + { + return Event.getOrCreate(name, false, 1000, false); + } + + /** + * Creates a new event with the specified name, if an event with the + * specified name already exists then the existing event is returned + * instead. + * + * @param name Event name (case insensitive) + * @param cancellable True if the event should be created as cancellable + * @return new Event instance or existing Event instance + */ + public static Event getOrCreate(String name, boolean cancellable) + { + return Event.getOrCreate(name, cancellable, 1000, true); + } + + /** + * Creates a new event with the specified name, if an event with the + * specified name already exists then the existing event is returned + * instead. + * + * @param name Event name (case insensitive) + * @param cancellable True if the event should be created as cancellable + * @param priority Priority for the event, only used when multiple events + * are being injected at the same instruction + * @return new Event instance or existing Event instance + */ + public static Event getOrCreate(String name, boolean cancellable, int priority) + { + return getOrCreate(name, cancellable, priority, true); + } + + protected static Event getOrCreate(String name, boolean cancellable, int priority, boolean defining) + { + Event event = Event.getEvent(name); + if (event != null) + { + if (!event.cancellable && cancellable && defining) + { + throw new IllegalArgumentException("Attempted to define the event " + event.name + " with cancellable '" + + cancellable + "' but the event is already defined with cancellable is '" + event.cancellable + "'"); + } + + return event; + } + + return new Event(name, cancellable, priority); + } + + /** + * Get the name of the event (all lowercase) + */ + public String getName() + { + return this.name; + } + + /** + * Get whether this event is cancellable or not + */ + public boolean isCancellable() + { + return this.cancellable; + } + + /** + * Get the event priority + */ + public int getPriority() + { + return this.priority; + } + + /** + * Set whether to log at INFO or DEBUG + */ + public Event setVerbose(boolean verbose) + { + this.verbose = verbose; + return this; + } + + /** + * Get whether to log at INFO or DEBUG + */ + public boolean isVerbose() + { + return this.verbose; + } + + /** + * Get whether this event is currently attached to a method + */ + public boolean isAttached() + { + return this.method != null; + } + + /** + * Attaches this event to a particular method, this occurs before injection + * in order to allow the event to configure its internal state appropriately + * for the method's signature. Since a single event may be injected into + * multiple target methods, and may also be injected at multiple points in + * the same method, this saves us recalculating this information for every + * injection, and instead just calculate once per method. + * + * @param method Method to attach to + */ + void attach(final MethodNode method) + { + if (this.method != null) + { + throw new IllegalStateException("Attempted to attach the event " + this.name + " to " + method.name + + " but the event was already attached to " + this.method.name + "!"); + } + + this.method = method; + this.methodReturnType = Type.getReturnType(method.desc); + this.methodMAXS = method.maxStack; + this.methodIsStatic = (method.access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC; + this.eventInfoClass = this.getEventInfoClassName(); + this.eventDescriptor = String.format("(L%s;%s)V", this.eventInfoClass, method.desc.substring(1, method.desc.indexOf(')'))); + } + + /** + * Detach from the attached method, called once injection is completed for a + * particular method. + */ + void detach() + { + this.method = null; + } + + void addPendingInjection(MethodInfo targetMethod) + { + if (this.pendingInjections == null) + { + this.pendingInjections = new HashSet(); + } + + this.pendingInjections.add(targetMethod); + } + + void notifyInjected(String method, String desc, String className) + { + MethodInfo thisInjection = null; + + if (this.pendingInjections != null) + { + for (MethodInfo pendingInjection : this.pendingInjections) + { + if (pendingInjection.matches(method, desc, className)) + { + thisInjection = pendingInjection; + break; + } + } + } + + if (thisInjection != null) + { + this.pendingInjections.remove(thisInjection); + } + } + + int dumpInjectionState() + { + int uninjectedCount = 0; + int pendingInjectionCount = this.pendingInjections != null ? this.pendingInjections.size() : 0; + + LiteLoaderLogger.debug(" Event: %-40s Injected: %d Pending: %d %s", this.name, this.injectionCount, pendingInjectionCount, + this.injectionCount == 0 ? " <<< NOT INJECTED >>>" : ""); + if (pendingInjectionCount > 0) + { + for (MethodInfo pending : this.pendingInjections) + { + LiteLoaderLogger.debug(" Pending: %s.%s", pending.getOwners(), pending.toString()); + uninjectedCount++; + } + } + + return uninjectedCount; + } + + /** + * Pre-flight check + * + * @param injectionPoint + * @param cancellable + * @param globalEventID + */ + protected void validate(final AbstractInsnNode injectionPoint, boolean cancellable, final int globalEventID) + { + if (this.method == null) + { + throw new IllegalStateException("Attempted to inject the event " + this.name + " but the event is not attached!"); + } + } + + /** + * Inject bytecode for this event into the currently attached method. When + * multiple events want to be injected into the same method at the same + * point only the first event is injected, subsequent events are simply + * added to the same handler delegate in the EventProxy class. + * + * @param injectionPoint Point to inject code, new instructions will be + * injected directly ahead of the specifed insn + * @param cancellable Cancellable flag, if true then the cancellation code + * (conditional return) will be injected as well + * @param globalEventID Global event ID, used to map a callback to the + * relevant event handler delegate method in EventProxy + * + * @return MethodNode for the event handler delegate + */ + final MethodNode inject(final AbstractInsnNode injectionPoint, boolean cancellable, final int globalEventID, final boolean captureLocals, + final Type[] locals) + { + // Pre-flight checks + this.validate(injectionPoint, cancellable, globalEventID); + + Type[] arguments = Type.getArgumentTypes(this.method.desc); + int initialFrameSize = ByteCodeUtilities.getFirstNonArgLocalIndex(arguments, !this.methodIsStatic); + + boolean doCaptureLocals = captureLocals && locals != null && locals.length > initialFrameSize; + String eventDescriptor = this.generateEventDescriptor(doCaptureLocals, locals, arguments, initialFrameSize); + + // Create the handler delegate method + MethodNode handler = new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC, Event.getHandlerName(globalEventID), + eventDescriptor, null, null); + Event.addMethodToActiveProxy(handler); + + LiteLoaderLogger.debug("Event %s is spawning handler %s in class %s", this.name, handler.name, Event.getActiveProxyRef()); + + int ctorMAXS = 0, invokeMAXS = arguments.length + (doCaptureLocals ? locals.length - initialFrameSize : 0); + int marshallVar = this.method.maxLocals++; + + InsnList insns = new InsnList(); + + boolean pushReturnValue = false; + + // If this is a ReturnEventInfo AND we are right before a RETURN opcode (so we can expect the *original* return + // value to be on the stack, then we dup the return value into a local var so we can push it later when we invoke + // the ReturnEventInfo ctor + if (injectionPoint instanceof InsnNode && injectionPoint.getOpcode() >= Opcodes.IRETURN && injectionPoint.getOpcode() < Opcodes.RETURN) + { + pushReturnValue = true; + insns.add(new InsnNode(Opcodes.DUP)); + insns.add(new VarInsnNode(this.methodReturnType.getOpcode(Opcodes.ISTORE), marshallVar)); + } + + // Instance the EventInfo for this event + insns.add(new TypeInsnNode(Opcodes.NEW, this.eventInfoClass)); ctorMAXS++; + insns.add(new InsnNode(Opcodes.DUP)); ctorMAXS++; invokeMAXS++; + ctorMAXS += this.invokeEventInfoConstructor(insns, cancellable, pushReturnValue, marshallVar); + insns.add(new VarInsnNode(Opcodes.ASTORE, marshallVar)); + + // Call the event handler method in the proxy + insns.add(new VarInsnNode(Opcodes.ALOAD, marshallVar)); + ByteCodeUtilities.loadArgs(arguments, insns, this.methodIsStatic ? 0 : 1); + if (doCaptureLocals) + { + ByteCodeUtilities.loadLocals(locals, insns, initialFrameSize); + } + insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Event.getActiveProxyRef(), handler.name, handler.desc, false)); + + if (cancellable) + { + // Inject the if (e.isCancelled()) return e.getReturnValue(); + this.injectCancellationCode(insns, injectionPoint, marshallVar); + } + + // Inject our generated code into the method + this.method.instructions.insertBefore(injectionPoint, insns); + this.method.maxStack = Math.max(this.method.maxStack, Math.max(this.methodMAXS + ctorMAXS, this.methodMAXS + invokeMAXS)); + + return handler; + } + + private String generateEventDescriptor(final boolean captureLocals, final Type[] locals, Type[] argumentTypes, int startIndex) + { + if (!captureLocals) return this.eventDescriptor; + + String eventDescriptor = this.eventDescriptor.substring(0, this.eventDescriptor.indexOf(')')); + for (int l = startIndex; l < locals.length; l++) + { + if (locals[l] != null) eventDescriptor += locals[l].getDescriptor(); + } + + return eventDescriptor + ")V"; + } + + protected int invokeEventInfoConstructor(InsnList insns, boolean cancellable, boolean pushReturnValue, int marshallVar) + { + int ctorMAXS = 0; + + insns.add(new LdcInsnNode(this.name)); ctorMAXS++; + insns.add(this.methodIsStatic ? new InsnNode(Opcodes.ACONST_NULL) : new VarInsnNode(Opcodes.ALOAD, 0)); ctorMAXS++; + insns.add(new InsnNode(cancellable ? Opcodes.ICONST_1 : Opcodes.ICONST_0)); ctorMAXS++; + + if (pushReturnValue) + { + insns.add(new VarInsnNode(this.methodReturnType.getOpcode(Opcodes.ILOAD), marshallVar)); + insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, this.eventInfoClass, Obf.constructor.name, + EventInfo.getConstructorDescriptor(this.methodReturnType), false)); + } + else + { + insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, this.eventInfoClass, Obf.constructor.name, + EventInfo.getConstructorDescriptor(), false)); + } + + return ctorMAXS; + } + + protected String getEventInfoClassName() + { + return EventInfo.getEventInfoClassName(this.methodReturnType).replace('.', '/'); + } + + /** + * if (e.isCancelled()) return e.getReturnValue(); + * + * @param insns + * @param injectionPoint + * @param marshallVar + */ + protected void injectCancellationCode(final InsnList insns, final AbstractInsnNode injectionPoint, int marshallVar) + { + insns.add(new VarInsnNode(Opcodes.ALOAD, marshallVar)); + insns.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, this.eventInfoClass, EventInfo.getIsCancelledMethodName(), + EventInfo.getIsCancelledMethodSig(), false)); + + LabelNode notCancelled = new LabelNode(); + insns.add(new JumpInsnNode(Opcodes.IFEQ, notCancelled)); + + // If this is a void method, just injects a RETURN opcode, otherwise we need to get the return value from the EventInfo + this.injectReturnCode(insns, injectionPoint, marshallVar); + + insns.add(notCancelled); + } + + /** + * Inject the appropriate return code for the method type + * + * @param insns + * @param injectionPoint + * @param eventInfoVar + */ + protected void injectReturnCode(final InsnList insns, final AbstractInsnNode injectionPoint, int eventInfoVar) + { + if (this.methodReturnType.equals(Type.VOID_TYPE)) + { + // Void method, so just return void + insns.add(new InsnNode(Opcodes.RETURN)); + } + else + { + // Non-void method, so work out which accessor to call to get the return value, and return it + insns.add(new VarInsnNode(Opcodes.ALOAD, eventInfoVar)); + String accessor = ReturnEventInfo.getReturnAccessor(this.methodReturnType); + String descriptor = ReturnEventInfo.getReturnDescriptor(this.methodReturnType); + insns.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, this.eventInfoClass, accessor, descriptor, false)); + if (this.methodReturnType.getSort() == Type.OBJECT) + { + insns.add(new TypeInsnNode(Opcodes.CHECKCAST, this.methodReturnType.getInternalName())); + } + insns.add(new InsnNode(this.methodReturnType.getOpcode(Opcodes.IRETURN))); + } + } + + /** + * Add this event to the specified handler + * + * @param handler + */ + void addToHandler(MethodNode handler) + { + LiteLoaderLogger.debug("Adding event %s to handler %s", this.name, handler.name); + + Event.getEventsForHandlerMethod(handler).add(this); + this.injectionCount++; + } + + /** + * Add a listener for this event, the listener + * + * @param listener + * @return fluent interface + */ + public Event addListener(MethodInfo listener) + { + if (listener.hasDesc()) + { + throw new IllegalArgumentException("Descriptor is not allowed for listener methods"); + } + + if (this.pendingInjections != null && this.pendingInjections.size() == 0) + { + throw new EventAlreadyInjectedException("The event " + this.name + + " was already injected and has 0 pending injections, addListener() is not allowed at this point"); + } + + this.listeners.add(listener); + + return this; + } + + /** + * Get currently registered listeners for this event + */ + public Set getListeners() + { + return Collections.unmodifiableSet(this.listeners); + } + + /** + * Get an event by name (case insensitive) + * + * @param eventName + */ + static Event getEvent(String eventName) + { + for (Event event : Event.events) + if (event.name.equalsIgnoreCase(eventName)) + { + return event; + } + + return null; + } + + /** + * Populates the event proxy class with delegating methods for all injected + * events. + * + * @param classNode + * @param proxyIndex + */ + static ClassNode populateProxy(final ClassNode classNode, int proxyIndex) + { + int handlerCount = 0; + int invokeCount = 0; + int lineNumber = proxyIndex < 2 ? 210 : 10; // From EventProxy.java, this really is only to try and make stack traces a bit easier to read + + LiteLoaderLogger.info("Generating new Event Handler Proxy Class %s", classNode.name.replace('/', '.')); + + Map> handlerMethods = Event.proxyHandlerMethods.get(Event.proxyInnerClassIndex); + Event.proxyInnerClassIndex++; + + // Loop through all handlers and inject a method for each one + for (Entry> handler : handlerMethods.entrySet()) + { + MethodNode handlerMethod = handler.getKey(); + List handlerEvents = handler.getValue(); + + // Args is used to inject appropriate LOAD opcodes to put the method arguments on the stack for each handler invocation + Type[] args = Type.getArgumentTypes(handlerMethod.desc); + + // Add our generated method to the the class + classNode.methods.add(handlerMethod); + handlerCount++; + + InsnList insns = handlerMethod.instructions; + for (Event event : handlerEvents) + { + Set listeners = event.listeners; + if (listeners.size() > 0) + { + LabelNode tryCatchStart = new LabelNode(); + LabelNode tryCatchEnd = new LabelNode(); + LabelNode tryCatchHandler1 = new LabelNode(); + LabelNode tryCatchHandler2 = new LabelNode(); + LabelNode tryCatchExit = new LabelNode(); + + handlerMethod.tryCatchBlocks.add(new TryCatchBlockNode(tryCatchStart, tryCatchEnd, + tryCatchHandler1, "java/lang/NoSuchMethodError")); + handlerMethod.tryCatchBlocks.add(new TryCatchBlockNode(tryCatchStart, tryCatchEnd, + tryCatchHandler2, "java/lang/NoClassDefFoundError")); + + insns.add(tryCatchStart); // try { + + for (MethodInfo listener : listeners) + { + invokeCount++; + + LabelNode lineNumberLabel = new LabelNode(new Label()); + insns.add(lineNumberLabel); + insns.add(new LineNumberNode(++lineNumber, lineNumberLabel)); + + ByteCodeUtilities.loadArgs(args, insns, 0); + insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, listener.ownerRef, listener.getOrInflectName(event.name), + handlerMethod.desc, false)); + } + + insns.add(tryCatchEnd); // } + insns.add(new JumpInsnNode(Opcodes.GOTO, tryCatchExit)); + + insns.add(tryCatchHandler1); // catch (NoSuchMethodError err) { + insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); + insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Obf.EventProxy.ref, "onMissingHandler", + "(Ljava/lang/Error;Lcom/mumfrey/liteloader/transformers/event/EventInfo;)V", false)); + insns.add(new JumpInsnNode(Opcodes.GOTO, tryCatchExit)); + + insns.add(tryCatchHandler2); // } catch (NoClassDefFoundError err) { + insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); + insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Obf.EventProxy.ref, "onMissingClass", + "(Ljava/lang/Error;Lcom/mumfrey/liteloader/transformers/event/EventInfo;)V", false)); + insns.add(new JumpInsnNode(Opcodes.GOTO, tryCatchExit)); + + insns.add(tryCatchExit); // } + } + } + + insns.add(new InsnNode(Opcodes.RETURN)); + } + + LiteLoaderLogger.info("Successfully generated event handler proxy class with %d handlers(s) and %d total invocations", + handlerCount, invokeCount); + + return classNode; + } + + private static List addMethodToActiveProxy(MethodNode handlerMethod) + { + Event.resizeProxyList(); + + ArrayList events = new ArrayList(); + Event.proxyHandlerMethods.get(Event.proxyInnerClassIndex).put(handlerMethod, events); + return events; + } + + private static void resizeProxyList() + { + while (Event.proxyHandlerMethods.size() < Event.proxyInnerClassIndex + 1) + { + Event.proxyHandlerMethods.add(new LinkedHashMap>()); + } + } + + private static List getEventsForHandlerMethod(MethodNode handlerMethod) + { + for (Map> handlers : Event.proxyHandlerMethods) + { + List events = handlers.get(handlerMethod); + if (events != null) return events; + } + + return Event.addMethodToActiveProxy(handlerMethod); + } + + private static String getHandlerName(int globalEventID) + { + return String.format("$event%05x", globalEventID); + } + + private static String getActiveProxyRef() + { + return Obf.EventProxy.ref + (Event.proxyInnerClassIndex > 1 ? "$" + Event.proxyInnerClassIndex : ""); + } + + @Override + public int compareTo(Event other) + { + if (other == null) return 0; + if (other.priority == this.priority) return this.order - other.order; + return (this.priority - other.priority); + } + + @Override + public int hashCode() + { + return this.name.hashCode(); + } + + @Override + public boolean equals(Object other) + { + if (other == this) return true; + if (other instanceof Event) return ((Event)other).name.equals(this.name); + return false; + } + + @Override + public String toString() + { + return this.name; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventAlreadyInjectedException.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventAlreadyInjectedException.java new file mode 100644 index 00000000..2d171e51 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventAlreadyInjectedException.java @@ -0,0 +1,21 @@ +package com.mumfrey.liteloader.transformers.event; + +public class EventAlreadyInjectedException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public EventAlreadyInjectedException(String message) + { + super(message); + } + + public EventAlreadyInjectedException(Throwable cause) + { + super(cause); + } + + public EventAlreadyInjectedException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventInfo.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventInfo.java new file mode 100644 index 00000000..ab2f1596 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventInfo.java @@ -0,0 +1,133 @@ +package com.mumfrey.liteloader.transformers.event; + +import org.objectweb.asm.Type; + +import com.mumfrey.liteloader.core.event.Cancellable; +import com.mumfrey.liteloader.core.event.EventCancellationException; + +/** + * Contains information about an injected event, including the source object and + * whether the event is cancellable and/or cancelled. + * + * @author Adam Mummery-Smith + * + * @param Source object type. For non-static methods this will be the + * containing object instance. + */ +public class EventInfo implements Cancellable +{ + protected static final String STRING = "Ljava/lang/String;"; + protected static final String OBJECT = "Ljava/lang/Object;"; + + private final String name; + + private final S source; + + private final boolean cancellable; + + private boolean cancelled; + + public EventInfo(String name, S source, boolean cancellable) + { + this.name = name; + this.source = source; + this.cancellable = cancellable; + } + + public S getSource() + { + return this.source; + } + + public String getName() + { + return this.name; + } + + protected String getSourceClass() + { + return this.source != null ? this.source.getClass().getSimpleName() : null; + } + + @Override + public String toString() + { + return String.format("EventInfo(TYPE=%s,NAME=%s,SOURCE=%s,CANCELLABLE=%s)", this.getClass().getSimpleName(), + this.name, this.getSourceClass(), this.cancellable); + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.core.event.Cancellable#isCancellable() + */ + @Override + public final boolean isCancellable() + { + return this.cancellable; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.transformers.event.Cancellable#isCancelled() + */ + @Override + public final boolean isCancelled() + { + return this.cancelled; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.transformers.event.Cancellable#cancel() + */ + @Override + public void cancel() throws EventCancellationException + { + if (!this.cancellable) + { + throw new EventCancellationException(String.format("The event %s is not cancellable.", this.name)); + } + + this.cancelled = true; + } + + protected static String getEventInfoClassName() + { + return EventInfo.class.getName(); + } + + /** + * @param returnType + */ + protected static String getEventInfoClassName(Type returnType) + { + return returnType.equals(Type.VOID_TYPE) ? EventInfo.class.getName() : ReturnEventInfo.class.getName(); + } + + public static String getConstructorDescriptor(Type returnType) + { + if (returnType.equals(Type.VOID_TYPE)) + { + return EventInfo.getConstructorDescriptor(); + } + + if (returnType.getSort() == Type.OBJECT) + { + return String.format("(%s%sZ%s)V", EventInfo.STRING, EventInfo.OBJECT, EventInfo.OBJECT); + } + + return String.format("(%s%sZ%s)V", EventInfo.STRING, EventInfo.OBJECT, returnType.getDescriptor()); + } + + public static String getConstructorDescriptor() + { + return String.format("(%s%sZ)V", EventInfo.STRING, EventInfo.OBJECT); + } + + public static String getIsCancelledMethodName() + { + return "isCancelled"; + } + + public static String getIsCancelledMethodSig() + { + return "()Z"; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventInjectionTransformer.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventInjectionTransformer.java new file mode 100644 index 00000000..a8d51b07 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventInjectionTransformer.java @@ -0,0 +1,114 @@ +package com.mumfrey.liteloader.transformers.event; + +import com.mumfrey.liteloader.transformers.ObfProvider; + +import net.minecraft.launchwrapper.IClassTransformer; + +public abstract class EventInjectionTransformer implements IClassTransformer +{ + public EventInjectionTransformer() + { + try + { + this.addEvents(); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + /* (non-Javadoc) + * @see net.minecraft.launchwrapper.IClassTransformer + * #transform(java.lang.String, java.lang.String, byte[]) + */ + @Override + public byte[] transform(String name, String transformedName, byte[] basicClass) + { + return basicClass; + } + + /** + * Subclasses should register events here + */ + protected abstract void addEvents(); + + /** + * Register a new event to be injected, the event instance will be created + * if it does not already exist. + * + * @param eventName Name of the event to use/create. Beware that + * IllegalArgumentException if the event was already defined with + * incompatible parameters + * @param targetMethod Method descriptor to identify the method to inject + * into + * @param injectionPoint Delegate which finds the location(s) in the target + * method to inject into + * + * @return the event - for fluent interface + */ + protected final Event addEvent(String eventName, MethodInfo targetMethod, InjectionPoint injectionPoint) + { + return this.addEvent(Event.getOrCreate(eventName), targetMethod, injectionPoint); + } + + /** + * Register an event to be injected + * + * @param event Event to inject + * @param targetMethod Method descriptor to identify the method to inject + * into + * @param injectionPoint Delegate which finds the location(s) in the target + * method to inject into + * + * @return the event - for fluent interface + */ + protected final Event addEvent(Event event, MethodInfo targetMethod, InjectionPoint injectionPoint) + { + if (event == null) + { + throw new IllegalArgumentException("Event cannot be null!"); + } + + if (injectionPoint == null) + { + throw new IllegalArgumentException("Injection point cannot be null for event " + event.getName()); + } + + if ("true".equals(System.getProperty("mcpenv"))) + { + EventTransformer.addEvent(event, targetMethod.owner, targetMethod.sig, injectionPoint); + } + else + { + EventTransformer.addEvent(event, targetMethod.owner, targetMethod.sigSrg, injectionPoint); + EventTransformer.addEvent(event, targetMethod.ownerObf, targetMethod.sigObf, injectionPoint); + } + + event.addPendingInjection(targetMethod); + + return event; + } + + /** + * Register an access injection interface + * + * @param interfaceName + */ + protected final void addAccessor(String interfaceName) + { + EventTransformer.addAccessor(interfaceName); + } + + /** + * Register an access injection interface and provide a contextual + * obfuscation provider. + * + * @param interfaceName + * @param obfProvider + */ + protected final void addAccessor(String interfaceName, ObfProvider obfProvider) + { + EventTransformer.addAccessor(interfaceName, obfProvider); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventProxyTransformer.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventProxyTransformer.java new file mode 100644 index 00000000..cc513f37 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventProxyTransformer.java @@ -0,0 +1,77 @@ +package com.mumfrey.liteloader.transformers.event; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.MethodNode; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.ClassTransformer; + +/** + * Transformer responsible for transforming/generating the EventProxy inner + * classes, separated from the Event Transformer itself so that we can place it + * higher up the tranformer chain to avoid broken mod transformers screwing + * things up. + * + * @author Adam Mummery-Smith + */ +public class EventProxyTransformer extends ClassTransformer +{ + public EventProxyTransformer() + { + } + + @Override + public byte[] transform(String name, String transformedName, byte[] basicClass) + { + if (transformedName != null && transformedName.startsWith(Obf.EventProxy.name)) + { + int dollarPos = transformedName.indexOf('$'); + int proxyIndex = (dollarPos > -1) ? Integer.parseInt(transformedName.substring(dollarPos + 1)) : 0; + if (proxyIndex != 1) + { + try + { + return this.transformEventProxy(transformedName, basicClass, proxyIndex); + } + catch (Throwable th) + { + th.printStackTrace(); + } + } + } + + return basicClass; + } + + private byte[] transformEventProxy(String transformedName, byte[] basicClass, int proxyIndex) + { + ClassNode classNode = this.getProxyByteCode(transformedName, basicClass, proxyIndex); + return this.writeClass(Event.populateProxy(classNode, proxyIndex == 0 ? 1 : proxyIndex)); + } + + private ClassNode getProxyByteCode(String transformedName, byte[] basicClass, int proxyIndex) + { + if (proxyIndex == 0 || basicClass != null) + { + ClassNode classNode = this.readClass(basicClass, true); + + for (MethodNode method : classNode.methods) + { + // Strip the sanity code out of the EventProxy class initialiser + if ("".equals(method.name)) + { + method.instructions.clear(); + method.instructions.add(new InsnNode(Opcodes.RETURN)); + } + } + + return classNode; + } + + ClassNode classNode = new ClassNode(); + classNode.visit(50, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, transformedName.replace('.', '/'), null, "java/lang/Object", null); + return classNode; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventTransformer.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventTransformer.java new file mode 100644 index 00000000..ba701766 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/EventTransformer.java @@ -0,0 +1,417 @@ +package com.mumfrey.liteloader.transformers.event; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.core.helpers.Booleans; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.util.CheckClassAdapter; +import com.google.common.collect.Maps; +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.ByteCodeUtilities; +import com.mumfrey.liteloader.transformers.ClassTransformer; +import com.mumfrey.liteloader.transformers.ObfProvider; +import com.mumfrey.liteloader.transformers.access.AccessorTransformer; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +/** + * EventTransformer is the spiritual successor to the + * CallbackInjectionTransformer and is a more advanced and flexible + * version of the same premise. Like the CallbackInjectionTransformer, it can be + * used to inject callbacks intelligently into a target method, however it has + * the following additional capabilities which make it more flexible and + * scalable: + * + *
        + *
      • Injections are not restricted to RETURN opcodes or profiler + * invocations, each injection is determined by supplying an InjectionPoint + * instance to the {@code addEvent} method which is used to find the + * injection point(s) in the method.
      • + * + *
      • Injected events can optionally be specified as *cancellable* which + * allows method execution to be pre-emptively halted based on the + * cancellation status of the event. For methods with a return value, the + * return value may be specified by the event handler.
      • + * + *
      • Injected events call back against a dynamically-generated proxy + * class, this means that it is no longer necessary to provide your own + * implementation of a static callback proxy, events can call back directly + * against handler methods in your own codebase.
      • + * + *
      • Event injections are more intelligent about injecting at arbitrary + * points in the bytecode without corrupting the local stack, and increase + * MAXS as required.
      • + * + *
      • Event injections do not "collide" like callback injections do - this + * means that if multiple events are injected by multiple sources at the + * same point in the bytecode, then all event handlers will receive and + * handle the event in one go. To provide for this, each event handler is + * defined with an intrinsic "priority" which determines its call order when + * this situation occurs
      • + *
      + * + * @author Adam Mummery-Smith + */ +public final class EventTransformer extends ClassTransformer +{ + public static final boolean DUMP = Booleans.parseBoolean(System.getProperty("liteloader.debug.dump"), false); + + public static final boolean VALIDATE = Booleans.parseBoolean(System.getProperty("liteloader.debug.validate"), false); + + /** + * Multidimensional map of class names -> target method signatures -> events + * to inject. + */ + private static Map>> eventMappings = Maps.newHashMap(); + + private static AccessorTransformer accessorTransformer; + + private int globalEventID = 0; + + static class Injection + { + private final AbstractInsnNode node; + + private final boolean captureLocals; + + private final Set events = new TreeSet(); + + private boolean hasLocals = false; + + private LocalVariableNode[] locals; + + public Injection(AbstractInsnNode node, boolean captureLocals) + { + this.node = node; + this.captureLocals = captureLocals; + } + + public AbstractInsnNode getNode() + { + return this.node; + } + + public Set getEvents() + { + return this.events; + } + + public LocalVariableNode[] getLocals() + { + return this.locals; + } + + public Type[] getLocalTypes() + { + if (this.locals == null) return null; + + Type[] localTypes = new Type[this.locals.length]; + for (int l = 0; l < this.locals.length; l++) + { + if (this.locals[l] != null) + { + localTypes[l] = Type.getType(this.locals[l].desc); + } + } + return localTypes; + } + + public boolean hasLocals() + { + return this.hasLocals; + } + + public void setLocals(LocalVariableNode[] locals) + { + this.hasLocals = true; + if (locals == null) return; + this.locals = locals; + } + + public boolean captureLocals() + { + return this.captureLocals; + } + + public void checkCaptureLocals(InjectionPoint injectionPoint) + { + if (injectionPoint.captureLocals != this.captureLocals) + { + throw new RuntimeException("Overlapping injection points defined with incompatible settings. Attempting to handle " + + injectionPoint + " with capture locals [" + injectionPoint.captureLocals + "] but already defined injection point with [" + + this.captureLocals + "]"); + } + } + + public void add(Event event) + { + this.events.add(event); + } + + public int size() + { + return this.events.size(); + } + + public Event getHead() + { + return this.events.iterator().next(); + } + + public void addEventsToHandler(MethodNode handler) + { + for (Event event : this.events) + { + event.addToHandler(handler); + } + } + + public boolean isCancellable() + { + boolean cancellable = false; + for (Event event : this.events) + cancellable |= event.isCancellable(); + return cancellable; + } + } + + static void addEvent(Event event, String className, String signature, InjectionPoint injectionPoint) + { + Map> mappings = EventTransformer.eventMappings.get(className); + if (mappings == null) + { + mappings = new HashMap>(); + EventTransformer.eventMappings.put(className, mappings); + } + + Map events = mappings.get(signature); + if (events == null) + { + events = new LinkedHashMap(); + mappings.put(signature, events); + } + + events.put(event, injectionPoint); + } + + static void addAccessor(String interfaceName) + { + EventTransformer.addAccessor(interfaceName, null); + } + + static void addAccessor(String interfaceName, ObfProvider obfProvider) + { + if (EventTransformer.accessorTransformer == null) + { + EventTransformer.accessorTransformer = new AccessorTransformer() + { + @Override + protected void addAccessors() {} + }; + } + + EventTransformer.accessorTransformer.addAccessor(interfaceName, obfProvider); + } + + @Override + public final byte[] transform(String name, String transformedName, byte[] basicClass) + { + if (basicClass != null && EventTransformer.eventMappings.containsKey(transformedName)) + { + return this.injectEvents(name, transformedName, basicClass, EventTransformer.eventMappings.get(transformedName)); + } + + if (EventTransformer.accessorTransformer != null) + { + return EventTransformer.accessorTransformer.transform(name, transformedName, basicClass); + } + + return basicClass; + } + + private byte[] injectEvents(String name, String transformedName, byte[] basicClass, Map> mappings) + { + if (mappings == null) return basicClass; + + ClassNode classNode = this.readClass(basicClass, true); + + for (MethodNode method : classNode.methods) + { + String signature = MethodInfo.generateSignature(method.name, method.desc); + Map methodInjections = mappings.get(signature); + if (methodInjections != null) + { + this.injectIntoMethod(classNode, signature, method, methodInjections); + } + } + + if (EventTransformer.accessorTransformer != null) + { + EventTransformer.accessorTransformer.apply(name, transformedName, basicClass, classNode); + } + + if (EventTransformer.VALIDATE) + { + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + classNode.accept(new CheckClassAdapter(writer)); + } + + byte[] bytes = this.writeClass(classNode); + + if (EventTransformer.DUMP) + { + try + { + FileUtils.writeByteArrayToFile(new File(".classes/" + Obf.lookupMCPName(transformedName).replace('.', '/') + ".class"), bytes); + } + catch (IOException ex) {} + } + + return bytes; + } + + /** + * @param classNode + * @param signature + * @param method + * @param methodInjections + */ + void injectIntoMethod(ClassNode classNode, String signature, MethodNode method, Map methodInjections) + { + Map injectionPoints = this.findInjectionPoints(classNode, method, methodInjections); + + for (Entry injectionPoint : injectionPoints.entrySet()) + { + this.injectEventsAt(classNode, method, injectionPoint.getKey(), injectionPoint.getValue()); + } + + for (Event event : methodInjections.keySet()) + { + event.notifyInjected(method.name, method.desc, classNode.name); + event.detach(); + } + } + + /** + * @param classNode + * @param method + * @param methodInjections + */ + private Map findInjectionPoints(ClassNode classNode, MethodNode method, Map methodInjections) + { + ReadOnlyInsnList insns = new ReadOnlyInsnList(method.instructions); + Collection nodes = new ArrayList(32); + Map injectionPoints = new LinkedHashMap(); + for (Entry eventEntry : methodInjections.entrySet()) + { + Event event = eventEntry.getKey(); + event.attach(method); + InjectionPoint injectionPoint = eventEntry.getValue(); + nodes.clear(); + if (injectionPoint.find(method.desc, insns, nodes, event)) + { + for (AbstractInsnNode node : nodes) + { + Injection injection = injectionPoints.get(node); + if (injection == null) + { + injection = new Injection(node, injectionPoint.captureLocals()); + injectionPoints.put(node, injection); + } + else + { + injection.checkCaptureLocals(injectionPoint); + } + + if (injectionPoint.captureLocals() && !injection.hasLocals()) + { + LocalVariableNode[] locals = ByteCodeUtilities.getLocalsAt(classNode, method, node); + injection.setLocals(locals); + if (injectionPoint.logLocals()) + { + int startPos = ByteCodeUtilities.getFirstNonArgLocalIndex(method); + + LiteLoaderLogger.debug(ClassTransformer.HORIZONTAL_RULE); + LiteLoaderLogger.debug("Logging local variables for " + injectionPoint); + for (int i = startPos; i < locals.length; i++) + { + LocalVariableNode local = locals[i]; + if (local != null) + { + LiteLoaderLogger.debug(" Local[%d] %s %s", i, ByteCodeUtilities.getTypeName(Type.getType(local.desc)), + local.name); + } + } + LiteLoaderLogger.debug(ClassTransformer.HORIZONTAL_RULE); + } + } + + injection.add(event); + } + } + } + + return injectionPoints; + } + + /** + * @param classNode + * @param method + * @param injectionPoint + * @param injection + */ + private void injectEventsAt(ClassNode classNode, MethodNode method, AbstractInsnNode injectionPoint, Injection injection) + { + Event head = injection.getHead(); + + Verbosity verbosity = head.isVerbose() ? Verbosity.NORMAL : Verbosity.VERBOSE; + LiteLoaderLogger.info(verbosity, "Injecting %s[x%d] in %s in %s", head.getName(), injection.size(), method.name, + ClassTransformer.getSimpleClassName(classNode)); + + MethodNode handler = head.inject(injectionPoint, injection.isCancellable(), this.globalEventID, injection.captureLocals(), + injection.getLocalTypes()); + injection.addEventsToHandler(handler); + + this.globalEventID++; + } + + public static void dumpInjectionState() + { + int uninjectedCount = 0; + int eventCount = 0; + + LiteLoaderLogger.debug("EventInjectionTransformer: Injection State"); + LiteLoaderLogger.debug(ClassTransformer.HORIZONTAL_RULE); + for (Entry>> mapping : EventTransformer.eventMappings.entrySet()) + { + LiteLoaderLogger.debug("Class: %s", mapping.getKey()); + for (Entry> classMapping : mapping.getValue().entrySet()) + { + LiteLoaderLogger.debug(" Method: %s", classMapping.getKey()); + for (Event event : classMapping.getValue().keySet()) + { + uninjectedCount += event.dumpInjectionState(); + eventCount++; + } + } + } + LiteLoaderLogger.debug(ClassTransformer.HORIZONTAL_RULE); + LiteLoaderLogger.debug("Listed %d injection candidates with %d uninjected", eventCount, uninjectedCount); + LiteLoaderLogger.debug(ClassTransformer.HORIZONTAL_RULE); + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/InjectionPoint.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/InjectionPoint.java new file mode 100644 index 00000000..63f148de --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/InjectionPoint.java @@ -0,0 +1,331 @@ +package com.mumfrey.liteloader.transformers.event; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; + +import com.google.common.base.Joiner; + +/** + * Base class for injection point discovery classes. Each subclass describes a + * strategy for locating code injection points within a method, with the + * {@link #find} method populating a collection with insn nodes from the method + * which satisfy its strategy. + * + *

      This base class also contains composite strategy factory methods such as + * {@code and} and {@code or} which allow strategies to be combined using + * intersection (and) or union (or) relationships to allow multiple strategies + * to be easily combined.

      + * + *

      You are free to create your own injection point subclasses, but take note + * that it is allowed for a single InjectionPoint instance to be used for + * multiple injections and thus implementing classes MUST NOT cache the insn + * list, event, or nodes instance passed to the {@code find} method, as each + * call to {@code find} must be considered a separate functional contract and + * the InjectionPoint's lifespan is not linked to the discovery lifespan, + * therefore it is important that the InjectionPoint implementation is fully + * STATELESS.

      + * + * @author Adam Mummery-Smith + */ +public abstract class InjectionPoint +{ + /** + * Capture locals as well as args + */ + protected boolean captureLocals; + + protected boolean logLocals; + + /** + * Find injection points in the supplied insn list + * + * @param desc Method descriptor, supplied to allow return types and + * arguments etc. to be determined + * @param insns Insn list to search in, the strategy MUST ONLY add nodes + * from this list to the {@code nodes} collection + * @param nodes Collection of nodes to populate. Injectors should NOT make + * any assumptions about the state of this collection and should only + * call add() + * @param event Event being injected here, supplied to allow alteration of + * behaviour for specific event configurations (eg. cancellable) + * @return true if one or more injection points were found + */ + public abstract boolean find(String desc, InsnList insns, Collection nodes, Event event); + + /** + * Set whether this injection point should capture local variables as well + * as method arguments. + * + * @param captureLocals + * @return this, for fluent interface + */ + public InjectionPoint setCaptureLocals(boolean captureLocals) + { + this.captureLocals = captureLocals; + return this; + } + + /** + * Get whether capture locals is enabled + */ + public boolean captureLocals() + { + return this.captureLocals; + } + + /** + * Since it's virtually impossible to know what locals are available at a + * given injection point by reading the source, this method causes the + * injection point to dump the locals to the debug log at injection time. + * + * @param logLocals + * @return this, for fluent interface + */ + public InjectionPoint setLogLocals(boolean logLocals) + { + this.logLocals = logLocals; + return this; + } + + /** + * Get whether log locals is enabled + */ + public boolean logLocals() + { + return this.logLocals; + } + + @Override + public String toString() + { + return "InjectionPoint(" + this.getClass().getSimpleName() + ")"; + } + + /** + * Composite injection point + * + * @author Adam Mummery-Smith + */ + abstract static class CompositeInjectionPoint extends InjectionPoint + { + protected final InjectionPoint[] components; + + protected CompositeInjectionPoint(InjectionPoint... components) + { + if (components == null || components.length < 2) + { + throw new IllegalArgumentException("Must supply two or more component injection points for composite point!"); + } + + this.components = components; + + for (InjectionPoint component : this.components) + { + this.captureLocals |= component.captureLocals; + this.logLocals |= component.logLocals; + } + } + + @Override + public String toString() + { + return "CompositeInjectionPoint(" + this.getClass().getSimpleName() + ")[" + Joiner.on(',').join(this.components) + "]"; + } + } + + static final class Intersection extends InjectionPoint.CompositeInjectionPoint + { + public Intersection(InjectionPoint... points) + { + super(points); + } + + @Override + public boolean find(String desc, InsnList insns, Collection nodes, Event event) + { + boolean found = false; + + @SuppressWarnings("unchecked") + ArrayList[] allNodes = new ArrayList[this.components.length]; + + for (int i = 0; i < this.components.length; i++) + { + allNodes[i] = new ArrayList(); + this.components[i].find(desc, insns, allNodes[i], event); + } + + ArrayList alpha = allNodes[0]; + for (int nodeIndex = 0; nodeIndex < alpha.size(); nodeIndex++) + { + AbstractInsnNode node = alpha.get(nodeIndex); + boolean in = true; + + for (int b = 1; b < allNodes.length; b++) + { + if (!allNodes[b].contains(node)) + { + break; + } + } + + if (!in) continue; + + nodes.add(node); + found = true; + } + + return found; + } + } + + static final class Union extends InjectionPoint.CompositeInjectionPoint + { + public Union(InjectionPoint... points) + { + super(points); + } + + @Override + public boolean find(String desc, InsnList insns, Collection nodes, Event event) + { + LinkedHashSet allNodes = new LinkedHashSet(); + + for (int i = 0; i < this.components.length; i++) + { + this.components[i].find(desc, insns, allNodes, event); + } + + nodes.addAll(allNodes); + + return allNodes.size() > 0; + } + } + + static final class Shift extends InjectionPoint + { + private final InjectionPoint input; + private final int shift; + + public Shift(InjectionPoint input, int shift) + { + if (input == null) + { + throw new IllegalArgumentException("Must supply an input injection point for SHIFT"); + } + + this.input = input; + this.shift = shift; + } + + @Override + public InjectionPoint setCaptureLocals(boolean captureLocals) + { + return this.input.setCaptureLocals(captureLocals); + } + + @Override + public boolean captureLocals() + { + return this.input.captureLocals(); + } + + @Override + public InjectionPoint setLogLocals(boolean logLocals) + { + return this.input.setLogLocals(logLocals); + } + + @Override + public boolean logLocals() + { + return this.input.logLocals(); + } + + @Override + public String toString() + { + return "InjectionPoint(" + this.getClass().getSimpleName() + ")[" + this.input + "]"; + } + + @Override + public boolean find(String desc, InsnList insns, Collection nodes, Event event) + { + List list = (nodes instanceof List) ? (List)nodes : new ArrayList(nodes); + + this.input.find(desc, insns, nodes, event); + + for (int i = 0; i < list.size(); i++) + { + list.set(i, insns.get(insns.indexOf(list.get(i)) + this.shift)); + } + + if (nodes != list) + { + nodes.clear(); + nodes.addAll(list); + } + + return nodes.size() > 0; + } + } + + /** + * Returns a composite injection point which returns the intersection of + * nodes from all component injection points + * + * @param operands + */ + public static InjectionPoint and(InjectionPoint... operands) + { + return new InjectionPoint.Intersection(operands); + } + + /** + * Returns a composite injection point which returns the union of nodes from + * all component injection points. + * + * @param operands + */ + public static InjectionPoint or(InjectionPoint... operands) + { + return new InjectionPoint.Union(operands); + } + + /** + * Returns an injection point which returns all insns immediately following + * insns from the supplied injection point. + * + * @param point + */ + public static InjectionPoint after(InjectionPoint point) + { + return new InjectionPoint.Shift(point, 1); + } + + /** + * Returns an injection point which returns all insns immediately prior to + * insns from the supplied injection point. + * + * @param point + */ + public static InjectionPoint before(InjectionPoint point) + { + return new InjectionPoint.Shift(point, -1); + } + + /** + * Returns an injection point which returns all insns offset by the + * specified "count" from insns from the supplied injection point. + * + * @param point + */ + public static InjectionPoint shift(InjectionPoint point, int count) + { + return new InjectionPoint.Shift(point, count); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/Jump.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/Jump.java new file mode 100644 index 00000000..723c2415 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/Jump.java @@ -0,0 +1,54 @@ +//package com.mumfrey.liteloader.transformers.event; +// +//import org.objectweb.asm.Opcodes; +//import org.objectweb.asm.tree.AbstractInsnNode; +//import org.objectweb.asm.tree.InsnList; +//import org.objectweb.asm.tree.InsnNode; +//import org.objectweb.asm.tree.JumpInsnNode; +//import org.objectweb.asm.tree.MethodInsnNode; +//import org.objectweb.asm.tree.VarInsnNode; +// +//public class Jump extends Event +//{ +// Jump(String name, boolean cancellable, int priority) +// { +// super(name, cancellable, priority); +// } +// +// @Override +// protected void validate(AbstractInsnNode injectionPoint, boolean cancellable, int globalEventID) +// { +// if (!(injectionPoint instanceof JumpInsnNode)) +// { +// throw new IllegalArgumentException("Attempted to inject a JUMP event where no JUMP is present"); +// } +// +// super.validate(injectionPoint, cancellable, globalEventID); +// } +// +// @Override +// protected void injectCancellationCode(InsnList insns, AbstractInsnNode injectionPoint, int eventInfoVar) throws IllegalArgumentException +// { +// int opcode = injectionPoint.getOpcode(); +// +// if (opcode == Opcodes.JSR) throw new IllegalArgumentException("Can't jump on finally clause"); +// +// if (opcode == Opcodes.IFEQ || opcode == Opcodes.IFNE || opcode == Opcodes.IFLT || opcode == Opcodes.IFGE +// || opcode == Opcodes.IFGT || opcode == Opcodes.IFLE || opcode == Opcodes.IFNULL || opcode == Opcodes.IFNONNULL) +// { +// insns.add(new InsnNode(Opcodes.POP)); +// } +// +// if (opcode == Opcodes.IF_ICMPEQ || opcode == Opcodes.IF_ICMPNE || opcode == Opcodes.IF_ICMPLT || opcode == Opcodes.IF_ICMPGE +// || opcode == Opcodes.IF_ICMPGT || opcode == Opcodes.IF_ICMPLE || opcode == Opcodes.IF_ACMPEQ || opcode == Opcodes.IF_ACMPNE) +// { +// insns.add(new InsnNode(Opcodes.POP)); +// insns.add(new InsnNode(Opcodes.POP)); +// } +// +// insns.add(new VarInsnNode(Opcodes.ALOAD, eventInfoVar)); +// insns.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, this.eventInfoClass, "isCancelled", "()Z")); +// +// ((JumpInsnNode)injectionPoint).setOpcode(Opcodes.IFEQ); +// } +//} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/MethodInfo.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/MethodInfo.java new file mode 100644 index 00000000..364b58dc --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/MethodInfo.java @@ -0,0 +1,375 @@ +package com.mumfrey.liteloader.transformers.event; + +import joptsimple.internal.Strings; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.ByteCodeUtilities; + +/** + * Encapsulates a method descriptor with varying degrees of accuracy from a + * simpler owner/method mapping up to and including a multi-faceted + * notch/srg/mcp method descriptor which works in all obfuscation environments. + * + * @author Adam Mummery-Smith + */ +public class MethodInfo +{ + public static final String INFLECT = Strings.EMPTY; + + // Owning class + final String owner; + final String ownerRef; + final String ownerObf; + + // Method name + final String name; + final String nameSrg; + final String nameObf; + + // Descriptor + final String desc; + final String descObf; + + // "Signature" - method name plus descriptor + final String sig; + final String sigSrg; + final String sigObf; + + /** + * Create a MethodInfo for the specified class with a method name inflected + * by context + * + * @param owner Literal owner class name + */ + public MethodInfo(String owner) + { + this(owner, owner, MethodInfo.INFLECT, MethodInfo.INFLECT, MethodInfo.INFLECT, null, null); + } + + /** + * Create a MethodInfo for the specified class with a method name inflected + * by context + * + * @param owner Owner name descriptor + */ + public MethodInfo(Obf owner) + { + this(owner.name, owner.obf, MethodInfo.INFLECT, MethodInfo.INFLECT, MethodInfo.INFLECT, null, null); + } + + /** + * Create a MethodInfo for the specified class and method names (literal) + * + * @param owner Literal owner class name + * @param method Literal method name + */ + public MethodInfo(String owner, String method) + { + this(owner, owner, method, method, method, null, null); + } + + /** + * Create a MethodInfo for the specified class and literal method name + * + * @param owner Owner name descriptor + * @param method Literal method name + */ + public MethodInfo(Obf owner, String method) + { + this(owner.name, owner.obf, method, method, method, null, null); + } + + /** + * Create a MethodInfo for the specified class and method name + * + * @param owner Owner name descriptor + * @param method Literal method name + */ + public MethodInfo(Obf owner, Obf method) + { + this(owner.name, owner.obf, method.name, method.srg, method.obf, null, null); + } + + /** + * Create a MethodInfo for the specified class, literal method name and + * literal descriptor + * + * @param owner Owner name descriptor + * @param method Literal method name + * @param descriptor Literal descriptor (useful for methods which only + * accept primitive types and therefore have a fixed descriptor) + */ + public MethodInfo(Obf owner, String method, String descriptor) + { + this(owner.name, owner.obf, method, method, method, descriptor, descriptor); + } + + /** + * Create a MethodInfo for the specified literal class, literal method and + * literal descriptor + * + * @param owner Literal class name + * @param method Literal method name + * @param descriptor Literal descriptor (useful for methods which only + * accept primitive types and therefore have a fixed descriptor) + */ + public MethodInfo(String owner, String method, String descriptor) + { + this(owner, owner, method, method, method, descriptor, descriptor); + } + + /** + * Create a MethodInfo for the specified class and method, with a literal + * descriptor + * + * @param owner Owner class name descriptor + * @param method Method name descriptor + * @param descriptor Literal descriptor (useful for methods which only + * accept primitive types and therefore have a fixed descriptor) + */ + public MethodInfo(Obf owner, Obf method, String descriptor) + { + this(owner.name, owner.obf, method.name, method.srg, method.obf, descriptor, descriptor); + } + + /** + *

      Create a MethodInfo for the specified class and literal method and + * compute the descriptor using the supplied arguments, both the returnType + * and args values can be one of four types:

      + * + *
        + *
      • Obf instances - are converted to the appropriate class + * name for the obf type internally
      • + *
      • Strings - are added directly to the descriptor
      • + *
      • Type instances - are expanded to their bytecode literal + *
      • + *
      • Class instances - are expanded to their bytecode + * descriptor via Type.getDescriptor
      • + *
      + * + * @param owner Owner name descriptor + * @param method Literal method name + * @param returnType Return type for the method (use Void.TYPE for void + * methods) + * @param args (optional) list of method arguments as Obf/String/Type/Class + * instances + */ + public MethodInfo(Obf owner, String method, Object returnType, Object... args) + { + this(owner.name, owner.obf, method, method, method, + ByteCodeUtilities.generateDescriptor(Obf.MCP, returnType, args), + ByteCodeUtilities.generateDescriptor(Obf.OBF, returnType, args)); + } + + /** + *

      Create a MethodInfo for the specified class and method names and + * compute the descriptor using the supplied arguments, both the returnType + * and args values can be one of four types:

      + * + *
        + *
      • Obf instances - are converted to the appropriate class + * name for the obf type internally
      • + *
      • Strings - are added directly to the descriptor
      • + *
      • Type instances - are expanded to their bytecode literal + *
      • + *
      • Class instances - are expanded to their bytecode + * descriptor via Type.getDescriptor
      • + *
      + * + * @param owner Owner name descriptor + * @param method Method name descriptor + * @param returnType Return type for the method (use Void.TYPE for void + * methods) + * @param args (optional) list of method arguments as Obf/String/Type/Class + * instances + */ + public MethodInfo(Obf owner, Obf method, Object returnType, Object... args) + { + this(owner.name, owner.obf, method.name, method.srg, method.obf, + ByteCodeUtilities.generateDescriptor(Obf.MCP, returnType, args), + ByteCodeUtilities.generateDescriptor(Obf.OBF, returnType, args)); + } + + /** + * @param owner + * @param ownerObf + * @param name + * @param nameSrg + * @param nameObf + * @param desc + * @param descObf + */ + MethodInfo(String owner, String ownerObf, String name, String nameSrg, String nameObf, String desc, String descObf) + { + this.owner = owner.replace('/', '.'); + this.ownerRef = owner.replace('.', '/'); + this.ownerObf = ownerObf; + this.name = name; + this.nameSrg = nameSrg; + this.nameObf = nameObf; + this.desc = desc; + this.descObf = descObf; + this.sig = MethodInfo.generateSignature(this.name, this.desc); + this.sigSrg = MethodInfo.generateSignature(this.nameSrg, this.desc); + this.sigObf = MethodInfo.generateSignature(this.nameObf, this.descObf); + } + + /** + * Get the method's owning class + */ + public String getOwner() + { + return this.owner; + } + + /** + * Get the method's owning class's obfuscated name (if it has one, otherwise + * returns the same as getOwner()) + */ + public String getOwnerObf() + { + return this.ownerObf; + } + + /** + * Get all owner variants in an array + */ + public String[] getOwners() + { + return new String[] { this.ownerObf, this.owner, this.owner }; + } + + /** + * Get the method's name + */ + public String getName() + { + return this.name; + } + + /** + * Get the method name or inflects it using the supplied context if this + * MethodInfo was created with inflection enabled + */ + public String getOrInflectName(String context) + { + return this.name == MethodInfo.INFLECT ? context : this.name; + } + + /** + * Get the Searge name of the method (if it has one, otherwise returns the + * base name) + */ + public String getNameSrg() + { + return this.nameSrg; + } + + /** + * Get the obfuscated name of the method (if it has one, otherwise returns + * the base name) + */ + public String getNameObf() + { + return this.nameObf; + } + + /** + * Get all name variants in an array + */ + public String[] getNames() + { + return new String[] { this.nameObf, this.nameSrg, this.name }; + } + + /** + * Get the method descriptor + */ + public String getDesc() + { + return this.desc; + } + + /** + * Get the method descriptor with obfuscated parameter types (if available, + * otherwise returns the same as getDesc()) + */ + public String getDescObf() + { + return this.descObf; + } + + /** + * Get all descriptors in an array + */ + public String[] getDescriptors() + { + return this.desc == null ? null : new String[] { this.descObf, this.desc, this.desc }; + } + + /** + * Returns true if this MethodInfo has a descriptor + */ + public boolean hasDesc() + { + return this.desc != null; + } + + /** + * Get the signature (combined method name and descriptor) for the method + * represented by this methodInfo + * + * @param type Obfuscation type to use + */ + public String getSignature(int type) + { + if (type == Obf.OBF) return this.sigObf; + if (type == Obf.SRG) return this.sigSrg; + return this.sig; + } + + public boolean matches(String method, String desc) + { + return this.matches(method, desc, null); + } + + public boolean matches(String method, String desc, String className) + { + if ((className == null || this.ownerRef.equals(className)) && (this.name.equals(method) || this.nameSrg.equals(method))) + { + return this.desc == null || this.desc.equals(desc); + } + else if ((className == null || this.ownerObf.equals(className)) && this.nameObf.equals(method)) + { + return this.descObf == null || this.descObf.equals(desc); + } + + return false; + } + + static String generateSignature(String methodName, String methodSignature) + { + return String.format("%s%s", methodName, methodSignature == null ? "" : methodSignature); + } + + @Override + public boolean equals(Object other) + { + if (other == this) return true; + if (other instanceof MethodInfo) return this.sig.equals(((MethodInfo)other).sig); + if (other instanceof String) return this.sig.equals(other); + return false; + } + + @Override + public String toString() + { + return this.sig; + } + + @Override + public int hashCode() + { + return this.sig.hashCode(); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/ReadOnlyInsnList.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/ReadOnlyInsnList.java new file mode 100644 index 00000000..b8f4ec8f --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/ReadOnlyInsnList.java @@ -0,0 +1,147 @@ +package com.mumfrey.liteloader.transformers.event; + +import java.util.ListIterator; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; + +/** + * Read-only wrapper for InsnList + * + * @author Adam Mummery-Smith + */ +public class ReadOnlyInsnList extends InsnList +{ + private InsnList insnList; + + public ReadOnlyInsnList(InsnList insns) + { + this.insnList = insns; + } + + void dispose() + { + this.insnList = null; + } + + @Override + public void set(AbstractInsnNode location, AbstractInsnNode insn) + { + throw new UnsupportedOperationException(); + } + + @Override + public void add(AbstractInsnNode insn) + { + throw new UnsupportedOperationException(); + } + + @Override + public void add(InsnList insns) + { + throw new UnsupportedOperationException(); + } + + @Override + public void insert(AbstractInsnNode insn) + { + throw new UnsupportedOperationException(); + } + + @Override + public void insert(InsnList insns) + { + throw new UnsupportedOperationException(); + } + + @Override + public void insert(AbstractInsnNode location, AbstractInsnNode insn) + { + throw new UnsupportedOperationException(); + } + + @Override + public void insert(AbstractInsnNode location, InsnList insns) + { + throw new UnsupportedOperationException(); + } + + @Override + public void insertBefore(AbstractInsnNode location, AbstractInsnNode insn) + { + throw new UnsupportedOperationException(); + } + + @Override + public void insertBefore(AbstractInsnNode location, InsnList insns) + { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(AbstractInsnNode insn) + { + throw new UnsupportedOperationException(); + } + + @Override + public AbstractInsnNode[] toArray() + { +// throw new UnsupportedOperationException(); + return this.insnList.toArray(); + } + + @Override + public int size() + { + return this.insnList.size(); + } + + @Override + public AbstractInsnNode getFirst() + { + return this.insnList.getFirst(); + } + + @Override + public AbstractInsnNode getLast() + { + return this.insnList.getLast(); + } + + @Override + public AbstractInsnNode get(int index) + { + return this.insnList.get(index); + } + + @Override + public boolean contains(AbstractInsnNode insn) + { + return this.insnList.contains(insn); + } + + @Override + public int indexOf(AbstractInsnNode insn) + { + return this.insnList.indexOf(insn); + } + + @Override + public ListIterator iterator() + { + return this.insnList.iterator(); + } + + @Override + public ListIterator iterator(int index) + { + return this.insnList.iterator(index); + } + + @Override + public void resetLabels() + { + this.insnList.resetLabels(); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/ReturnEventInfo.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/ReturnEventInfo.java new file mode 100644 index 00000000..81d69c92 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/ReturnEventInfo.java @@ -0,0 +1,140 @@ +package com.mumfrey.liteloader.transformers.event; + +import org.objectweb.asm.Type; + +import com.mumfrey.liteloader.core.event.EventCancellationException; + +/** + * EventInfo for events which have a return type + * + * @author Adam Mummery-Smith + * + * @param Source object type. For non-static methods this will be the + * containing object instance. + * @param Return type + */ +public class ReturnEventInfo extends EventInfo +{ + private R returnValue; + + public ReturnEventInfo(String name, S source, boolean cancellable) + { + super(name, source, cancellable); + this.returnValue = null; + } + + public ReturnEventInfo(String name, S source, boolean cancellable, R returnValue) + { + super(name, source, cancellable); + this.returnValue = returnValue; + } + + @SuppressWarnings("unchecked") + public ReturnEventInfo(String name, S source, boolean cancellable, byte returnValue) + { + super(name, source, cancellable); + this.returnValue = (R)Byte.valueOf(returnValue); + } + + @SuppressWarnings("unchecked") + public ReturnEventInfo(String name, S source, boolean cancellable, char returnValue) + { + super(name, source, cancellable); + this.returnValue = (R)Character.valueOf(returnValue); + } + + @SuppressWarnings("unchecked") + public ReturnEventInfo(String name, S source, boolean cancellable, double returnValue) + { + super(name, source, cancellable); + this.returnValue = (R)Double.valueOf(returnValue); + } + + @SuppressWarnings("unchecked") + public ReturnEventInfo(String name, S source, boolean cancellable, float returnValue) + { + super(name, source, cancellable); + this.returnValue = (R)Float.valueOf(returnValue); + } + + @SuppressWarnings("unchecked") + public ReturnEventInfo(String name, S source, boolean cancellable, int returnValue) + { + super(name, source, cancellable); + this.returnValue = (R)Integer.valueOf(returnValue); + } + + @SuppressWarnings("unchecked") + public ReturnEventInfo(String name, S source, boolean cancellable, long returnValue) + { + super(name, source, cancellable); + this.returnValue = (R)Long.valueOf(returnValue); + } + + @SuppressWarnings("unchecked") + public ReturnEventInfo(String name, S source, boolean cancellable, short returnValue) + { + super(name, source, cancellable); + this.returnValue = (R)Short.valueOf(returnValue); + } + + @SuppressWarnings("unchecked") + public ReturnEventInfo(String name, S source, boolean cancellable, boolean returnValue) + { + super(name, source, cancellable); + this.returnValue = (R)Boolean.valueOf(returnValue); + } + + /** + * Sets a return value for this event and cancels the event (required in + * order to return the new value). + * + * @param returnValue + */ + public void setReturnValue(R returnValue) throws EventCancellationException + { + super.cancel(); + + this.returnValue = returnValue; + } + + public R getReturnValue() + { + return this.returnValue; + } + + // CHECKSTYLE:OFF + + // All of the accessors below are to avoid having to generate unboxing conversions in bytecode + public byte getReturnValueB() { if (this.returnValue == null) return 0; return (Byte) this.returnValue; } + public char getReturnValueC() { if (this.returnValue == null) return 0; return (Character)this.returnValue; } + public double getReturnValueD() { if (this.returnValue == null) return 0.0; return (Double) this.returnValue; } + public float getReturnValueF() { if (this.returnValue == null) return 0.0F; return (Float) this.returnValue; } + public int getReturnValueI() { if (this.returnValue == null) return 0; return (Integer) this.returnValue; } + public long getReturnValueJ() { if (this.returnValue == null) return 0; return (Long) this.returnValue; } + public short getReturnValueS() { if (this.returnValue == null) return 0; return (Short) this.returnValue; } + public boolean getReturnValueZ() { if (this.returnValue == null) return false; return (Boolean) this.returnValue; } + + // CHECKSTYLE:ON + + + public static String getReturnAccessor(Type returnType) + { + if (returnType.getSort() == Type.OBJECT) + { + return "getReturnValue"; + } + + return String.format("getReturnValue%s", returnType.getDescriptor()); + } + + public static String getReturnDescriptor(Type returnType) + { + if (returnType.getSort() == Type.OBJECT) + { + return String.format("()%s", EventInfo.OBJECT); + } + + return String.format("()%s", returnType.getDescriptor()); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeFieldAccess.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeFieldAccess.java new file mode 100644 index 00000000..6afe5fb4 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeFieldAccess.java @@ -0,0 +1,99 @@ +package com.mumfrey.liteloader.transformers.event.inject; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.event.MethodInfo; + +/** + * An injection point which searches for GETFIELD and SETFIELD opcodes matching + * its arguments and returns a list of insns immediately prior to matching + * instructions. Only the field name is required, owners and signatures are + * optional and can be used to disambiguate between fields of the same name but + * with different types, or belonging to different classes. + * + * @author Adam Mummery-Smith + */ +public class BeforeFieldAccess extends BeforeInvoke +{ + private final int opcode; + + public BeforeFieldAccess(int opcode, String... fieldNames) + { + super(fieldNames); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, String fieldName, int ordinal) + { + super(fieldName, ordinal); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, String[] fieldNames, int ordinal) + { + super(fieldNames, ordinal); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, String[] fieldNames, String[] fieldOwners) + { + super(fieldNames, fieldOwners); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, String[] fieldNames, String[] fieldOwners, int ordinal) + { + super(fieldNames, fieldOwners, ordinal); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, String[] fieldNames, String[] fieldOwners, String[] fieldSignatures) + { + super(fieldNames, fieldOwners, fieldSignatures); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, String[] fieldNames, String[] fieldOwners, String[] fieldSignatures, int ordinal) + { + super(fieldNames, fieldOwners, fieldSignatures, ordinal); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, Obf fieldNames, int ordinal) + { + super(fieldNames.names, ordinal); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, Obf fieldNames, Obf fieldOwners) + { + super(fieldNames.names, fieldOwners.names); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, Obf fieldNames, Obf fieldOwners, int ordinal) + { + super(fieldNames.names, fieldOwners.names, ordinal); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, MethodInfo fieldInfo) + { + super(fieldInfo); + this.opcode = opcode; + } + + public BeforeFieldAccess(int opcode, MethodInfo fieldInfo, int ordinal) + { + super(fieldInfo, ordinal); + this.opcode = opcode; + } + + @Override + protected boolean matchesInsn(AbstractInsnNode insn) + { + return insn instanceof FieldInsnNode && ((FieldInsnNode)insn).getOpcode() == this.opcode; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeInvoke.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeInvoke.java new file mode 100644 index 00000000..e37002f1 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeInvoke.java @@ -0,0 +1,387 @@ +package com.mumfrey.liteloader.transformers.event.inject; + +import java.util.Collection; +import java.util.ListIterator; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodInsnNode; + +import com.mumfrey.liteloader.transformers.ClassTransformer; +import com.mumfrey.liteloader.transformers.event.Event; +import com.mumfrey.liteloader.transformers.event.InjectionPoint; +import com.mumfrey.liteloader.transformers.event.MethodInfo; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * An injection point which searches for method invocations matching its + * arguments and returns a list of insns immediately prior to matching + * invocations. Only the method name is required, owners and signatures are + * optional and can be used to disambiguate between methods of the same name but + * with different args, or belonging to different classes. + * + * @author Adam Mummery-Smith + */ +public class BeforeInvoke extends InjectionPoint +{ + protected class InsnInfo + { + public final String owner; + public final String name; + public final String desc; + + public InsnInfo(AbstractInsnNode insn) + { + if (insn instanceof MethodInsnNode) + { + MethodInsnNode methodNode = (MethodInsnNode)insn; + this.owner = methodNode.owner; + this.name = methodNode.name; + this.desc = methodNode.desc; + } + else if (insn instanceof FieldInsnNode) + { + FieldInsnNode fieldNode = (FieldInsnNode)insn; + this.owner = fieldNode.owner; + this.name = fieldNode.name; + this.desc = fieldNode.desc; + } + else + { + throw new IllegalArgumentException("insn must be an instance of MethodInsnNode or FieldInsnNode"); + } + } + } + + /** + * Method name(s) to search for, usually this will contain the different + * names of the method for different obfuscations (mcp, srg, notch) + */ + protected final String[] methodNames; + + /** + * Method owner(s) to search for, the values in this array MUST much the + * equivalent indices in methodNames, if the array is NULL then all owners + * are valid. + */ + protected final String[] methodOwners; + + /** + * Method signature(s) to search for, the values in this array MUST much the + * equivalent indices in methodNames, if the array is NULL then all + * signatures are valid. + */ + protected final String[] methodSignatures; + + /** + * This strategy can be used to identify a particular invocation if the same + * method is invoked at multiple points, if this value is -1 then the + * strategy returns ALL invocations of the method. + */ + protected final int ordinal; + + /** + * True to turn on strategy debugging to the console + */ + protected boolean logging = false; + + protected final String className; + + /** + * Match all occurrences of the specified method or methods + * + * @param methodNames Method name(s) to search for + */ + public BeforeInvoke(String... methodNames) + { + this(methodNames, null, -1); + } + + /** + * Match the specified invocation of the specified method + * + * @param methodName Method name to search for + * @param ordinal ID of the invocation to hook, or -1 to hook all + * invocations + */ + public BeforeInvoke(String methodName, int ordinal) + { + this(new String[] { methodName }, null, null, ordinal); + } + + /** + * Match the specified invocation of the specified method(s) + * + * @param methodNames Method names to search for + * @param ordinal ID of the invocation to hook, or -1 to hook all + * invocations + */ + public BeforeInvoke(String[] methodNames, int ordinal) + { + this(methodNames, null, null, ordinal); + } + + /** + * Match all occurrences of the specified method or methods with the + * specified owners. + * + * @param methodNames Method names to search for + * @param methodOwners Owners to search for, indices in this array MUST + * match the indices in methodNames, eg. if methodNames contains + * { "mcpName", "func_12345_a", "a" } then methodOwners should contain + * { "net/minecraft/pkg/ClsName", "net/minecraft/pkg/ClsName", "abc" } + * in order that the appropriate owner name obfuscation matches the + * corresponding index in the methodNames array + */ + public BeforeInvoke(String[] methodNames, String[] methodOwners) + { + this(methodNames, methodOwners, null, -1); + } + + /** + * Match the specified invocation of the specified method or methods with + * the specified owners. + * + * @param methodNames Method names to search for + * @param methodOwners Owners to search for, indices in this array MUST + * match the indices in methodNames, eg. if methodNames contains + * { "mcpName", "func_12345_a", "a" } then methodOwners should contain + * { "net/minecraft/pkg/ClsName", "net/minecraft/pkg/ClsName", "abc" } + * in order that the appropriate owner name obfuscation matches the + * corresponding index in the methodNames array + * @param ordinal ID of the invocation to hook or -1 to hook all invocations + */ + public BeforeInvoke(String[] methodNames, String[] methodOwners, int ordinal) + { + this(methodNames, methodOwners, null, ordinal); + } + + /** + * Match all occurrences of the specified method or methods with the + * specified owners or signatures, pass null to the owners array if you only + * want to match signatures. + * + * @param methodNames Method names to search for + * @param methodOwners Owners to search for, indices in this array MUST + * match the indices in methodNames, eg. if methodNames contains + * { "mcpName", "func_12345_a", "a" } then methodOwners should contain + * { "net/minecraft/pkg/ClsName", "net/minecraft/pkg/ClsName", "abc" } + * in order that the appropriate owner name obfuscation matches the + * corresponding index in the methodNames array + * @param methodSignatures Signatures to search for, indices in this array + * MUST match the indices in methodNames, eg. if methodNames contains + * { "mcpName", "func_12345_a", "a" } then methodSignatures should + * contain + * { "(Lnet/minecraft/pkg/ClsName;)V", + * "(Lnet/minecraft/pkg/ClsName;)V", "(Labc;)V" } + * in order that the appropriate signature obfuscation matches the + * corresponding index in the methodNames array (and ownerNames array + * if present) + */ + public BeforeInvoke(String[] methodNames, String[] methodOwners, String[] methodSignatures) + { + this(methodNames, methodOwners, methodSignatures, -1); + } + + /** + * Match the specified invocation of the specified method or methods with + * the specified owners or signatures, pass null to the owners array if you + * only want to match signatures. + * + * @param methodNames Method names to search for + * @param methodOwners Owners to search for, indices in this array MUST + * match the indices in methodNames, eg. if methodNames contains + * { "mcpName", "func_12345_a", "a" } then methodOwners should contain + * { "net/minecraft/pkg/ClsName", "net/minecraft/pkg/ClsName", "abc" } + * in order that the appropriate owner name obfuscation matches the + * corresponding index in the methodNames array + * @param methodSignatures Signatures to search for, indices in this array + * MUST match the indices in methodNames, eg. if methodNames contains + * { "mcpName", "func_12345_a", "a" } then methodSignatures should + * contain { "(Lnet/minecraft/pkg/ClassName;)V", + * "(Lnet/minecraft/pkg/ClassName;)V", "(Labc;)V" } + * in order that the appropriate signature obfuscation matches the + * corresponding index in the methodNames array (and ownerNames array + * if present) + * @param ordinal ID of the invocation to hook or -1 to hook all invocations + */ + public BeforeInvoke(String[] methodNames, String[] methodOwners, String[] methodSignatures, int ordinal) + { + if (methodNames == null || methodNames.length == 0) + { + throw new IllegalArgumentException("Method name selector must not be null"); + } + + if (methodSignatures != null && methodSignatures.length == 0) methodSignatures = null; + if (methodOwners != null && methodOwners.length == 0) methodOwners = null; + if (ordinal < 0) ordinal = -1; + + this.methodNames = methodNames; + this.methodOwners = methodOwners; + this.methodSignatures = methodSignatures; + this.ordinal = ordinal; + this.className = this.getClass().getSimpleName(); + + this.convertClassRefs(); + } + + /** + * Match the invocation described by the supplied MethodInfo + * + * @param method + */ + public BeforeInvoke(MethodInfo method) + { + this(method, -1); + } + + /** + * Match the invocation described by the supplied MethodInfo at the + * specified ordinal. + * + * @param method + * @param ordinal + */ + public BeforeInvoke(MethodInfo method, int ordinal) + { + this.methodNames = method.getNames(); + this.methodOwners = method.getOwners(); + this.methodSignatures = method.getDescriptors(); + this.ordinal = ordinal; + this.className = this.getClass().getSimpleName(); + + this.convertClassRefs(); + } + + private void convertClassRefs() + { + for (int i = 0; i < this.methodOwners.length; i++) + { + if (this.methodOwners[i] != null) this.methodOwners[i] = this.methodOwners[i].replace('.', '/'); + } + + if (this.methodSignatures != null) + { + for (int i = 0; i < this.methodSignatures.length; i++) + { + if (this.methodSignatures[i] != null) this.methodSignatures[i] = this.methodSignatures[i].replace('.', '/'); + } + } + } + + public BeforeInvoke setLogging(boolean logging) + { + this.logging = logging; + return this; + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.transformers.event.InjectionStrategy + * #findInjectionPoint(java.lang.String, + * org.objectweb.asm.tree.InsnList, + * com.mumfrey.liteloader.transformers.event.Event, + * java.util.Collection) + */ + @Override + public boolean find(String desc, InsnList insns, Collection nodes, Event event) + { + int ordinal = 0; + boolean found = false; + + if (this.logging) + { + LiteLoaderLogger.debug(ClassTransformer.HORIZONTAL_RULE); + LiteLoaderLogger.debug(this.className + " is searching for an injection point in method with descriptor %s", desc); + } + + ListIterator iter = insns.iterator(); + while (iter.hasNext()) + { + AbstractInsnNode insn = iter.next(); + + if (this.matchesInsn(insn)) + { + InsnInfo nodeInfo = new InsnInfo(insn); + + if (this.logging) + { + LiteLoaderLogger.debug(this.className + " is considering insn NAME=%s DESC=%s OWNER=%s", + nodeInfo.name, nodeInfo.desc, nodeInfo.owner); + } + + int index = BeforeInvoke.arrayIndexOf(this.methodNames, nodeInfo.name, -1); + if (index > -1 && this.logging) LiteLoaderLogger.debug(this.className + " found a matching insn, checking owner/signature..."); + + int ownerIndex = BeforeInvoke.arrayIndexOf(this.methodOwners, nodeInfo.owner, index); + int descIndex = BeforeInvoke.arrayIndexOf(this.methodSignatures, nodeInfo.desc, index); + if (index > -1 && ownerIndex == index && descIndex == index) + { + if (this.logging) LiteLoaderLogger.debug(this.className + " found a matching insn, checking preconditions..."); + if (this.matchesInsn(nodeInfo, ordinal)) + { + if (this.logging) LiteLoaderLogger.debug(this.className + " found a matching insn at ordinal %d", ordinal); + nodes.add(insn); + found = true; + + if (this.ordinal == ordinal) + { + break; + } + } + + ordinal++; + } + } + + this.inspectInsn(desc, insns, insn); + } + + if (this.logging) LiteLoaderLogger.debug(ClassTransformer.HORIZONTAL_RULE); + + return found; + } + + protected boolean matchesInsn(AbstractInsnNode insn) + { + return insn instanceof MethodInsnNode; + } + + protected void inspectInsn(String desc, InsnList insns, AbstractInsnNode insn) + { + // stub for subclasses + } + + protected boolean matchesInsn(InsnInfo nodeInfo, int ordinal) + { + if (this.logging) + { + LiteLoaderLogger.debug(this.className + " comparing target ordinal %d with current ordinal %d", this.ordinal, ordinal); + } + return this.ordinal == -1 || this.ordinal == ordinal; + } + + /** + * Special version of contains which returns TRUE if the haystack array is + * null, which is an odd behaviour we actually want here because null + * indicates that the value is not important. + * + * @param haystack + * @param needle + */ + private static int arrayIndexOf(String[] haystack, String needle, int pos) + { + if (haystack == null) return pos; + if (pos > -1 && pos < haystack.length && needle.equals(haystack[pos])) return pos; + + for (int index = 0; index < haystack.length; index++) + { + if (needle.equals(haystack[index])) + { + return index; + } + } + + return -1; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeNew.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeNew.java new file mode 100644 index 00000000..9c218157 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeNew.java @@ -0,0 +1,83 @@ +package com.mumfrey.liteloader.transformers.event.inject; + +import java.util.Collection; +import java.util.ListIterator; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.TypeInsnNode; + +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.event.Event; +import com.mumfrey.liteloader.transformers.event.InjectionPoint; + +public class BeforeNew extends InjectionPoint +{ + private final String[] classNames; + + private final int ordinal; + + public BeforeNew(Obf className) + { + this(-1, className.names); + } + + public BeforeNew(String... classNames) + { + this(-1, classNames); + } + + public BeforeNew(int ordinal, Obf className) + { + this(ordinal, className.names); + } + + public BeforeNew(int ordinal, String... classNames) + { + this.ordinal = Math.max(-1, ordinal); + this.classNames = classNames; + + for (int i = 0; i < this.classNames.length; i++) + { + this.classNames[i] = this.classNames[i].replace('.', '/'); + } + } + + @Override + public boolean find(String desc, InsnList insns, Collection nodes, Event event) + { + boolean found = false; + int ordinal = 0; + + ListIterator iter = insns.iterator(); + while (iter.hasNext()) + { + AbstractInsnNode insn = iter.next(); + + if (insn instanceof TypeInsnNode && insn.getOpcode() == Opcodes.NEW && this.matchesOwner((TypeInsnNode)insn)) + { + if (this.ordinal == -1 || this.ordinal == ordinal) + { + nodes.add(insn); + found = true; + } + + ordinal++; + } + } + + return found; + } + + private boolean matchesOwner(TypeInsnNode insn) + { + for (String className : this.classNames) + { + if (className.equals(insn.desc)) return true; + } + + return false; + } + +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeReturn.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeReturn.java new file mode 100644 index 00000000..d35ac9d5 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeReturn.java @@ -0,0 +1,61 @@ +package com.mumfrey.liteloader.transformers.event.inject; + +import java.util.Collection; +import java.util.ListIterator; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; + +import com.mumfrey.liteloader.transformers.event.Event; +import com.mumfrey.liteloader.transformers.event.InjectionPoint; + +/** + * An injection point which searches for RETURN opcodes in the supplied method + * and either finds all insns or the insn at the specified ordinal. + * + * @author Adam Mummery-Smith + */ +public class BeforeReturn extends InjectionPoint +{ + private final int ordinal; + + public BeforeReturn() + { + this(-1); + } + + public BeforeReturn(int ordinal) + { + this.ordinal = Math.max(-1, ordinal); + } + + @Override + public boolean find(String desc, InsnList insns, Collection nodes, Event event) + { + boolean found = false; + int returnOpcode = Type.getReturnType(desc).getOpcode(Opcodes.IRETURN); + int ordinal = 0; + + ListIterator iter = insns.iterator(); + while (iter.hasNext()) + { + AbstractInsnNode insn = iter.next(); + + if (insn instanceof InsnNode && insn.getOpcode() == returnOpcode) + { + if (this.ordinal == -1 || this.ordinal == ordinal) + { + nodes.add(insn); + found = true; + } + + ordinal++; + } + } + + return found; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeStringInvoke.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeStringInvoke.java new file mode 100644 index 00000000..91e24815 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/BeforeStringInvoke.java @@ -0,0 +1,77 @@ +package com.mumfrey.liteloader.transformers.event.inject; + +import java.util.Collection; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LdcInsnNode; + +import com.mumfrey.liteloader.transformers.event.Event; +import com.mumfrey.liteloader.transformers.event.MethodInfo; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * An injection point which searches for a matching String LDC insn immediately + * prior to a qualifying invoke. + * + * @author Adam Mummery-Smith + */ +public class BeforeStringInvoke extends BeforeInvoke +{ + private static final String STRING_VOID_SIG = "(Ljava/lang/String;)V"; + + private final String ldcValue; + + private boolean foundLdc; + + public BeforeStringInvoke(String ldcValue, MethodInfo method) + { + this(ldcValue, method, -1); + } + + public BeforeStringInvoke(String ldcValue, MethodInfo method, int ordinal) + { + super(method, ordinal); + this.ldcValue = ldcValue; + + for (int i = 0; i < this.methodSignatures.length; i++) + { + if (!STRING_VOID_SIG.equals(this.methodSignatures[i])) + { + throw new IllegalArgumentException("BeforeStringInvoke requires method with with signature " + STRING_VOID_SIG); + } + } + } + + @Override + public boolean find(String desc, InsnList insns, Collection nodes, Event event) + { + this.foundLdc = false; + + return super.find(desc, insns, nodes, event); + } + + @Override + protected void inspectInsn(String desc, InsnList insns, AbstractInsnNode insn) + { + if (insn instanceof LdcInsnNode) + { + LdcInsnNode node = (LdcInsnNode)insn; + if (node.cst instanceof String && this.ldcValue.equals(node.cst)) + { + if (this.logging) LiteLoaderLogger.info("BeforeInvoke found a matching LDC with value %s", node.cst); + this.foundLdc = true; + return; + } + } + + this.foundLdc = false; + } + + @Override + protected boolean matchesInsn(InsnInfo nodeInfo, int ordinal) + { + if (this.logging) LiteLoaderLogger.debug("BeforeInvoke foundLdc \"%s\" = %s", this.ldcValue, this.foundLdc); + return this.foundLdc && super.matchesInsn(nodeInfo, ordinal); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/JumpInsnPoint.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/JumpInsnPoint.java new file mode 100644 index 00000000..9469b423 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/JumpInsnPoint.java @@ -0,0 +1,69 @@ +package com.mumfrey.liteloader.transformers.event.inject; + +import java.util.Collection; +import java.util.ListIterator; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.JumpInsnNode; + +import com.mumfrey.liteloader.transformers.event.Event; +import com.mumfrey.liteloader.transformers.event.InjectionPoint; + +/** + * An injection point which searches for JUMP opcodes (if, try/catch, continue, + * break, conditional assignment, etc.) with either a particular opcode or at a + * particular ordinal in the method body (eg. "the Nth JUMP insn" where N is the + * ordinal of the instruction). By default it returns all JUMP instructions in a + * method body. + * + * @author Adam Mummery-Smith + */ +public class JumpInsnPoint extends InjectionPoint +{ + private final int opCode; + + private final int ordinal; + + public JumpInsnPoint() + { + this(0, -1); + } + + public JumpInsnPoint(int ordinal) + { + this(0, ordinal); + } + + public JumpInsnPoint(int opCode, int ordinal) + { + this.opCode = opCode; + this.ordinal = ordinal; + } + + @Override + public boolean find(String desc, InsnList insns, Collection nodes, Event event) + { + boolean found = false; + int ordinal = 0; + + ListIterator iter = insns.iterator(); + while (iter.hasNext()) + { + AbstractInsnNode insn = iter.next(); + + if (insn instanceof JumpInsnNode && (this.opCode == -1 || insn.getOpcode() == this.opCode)) + { + if (this.ordinal == -1 || this.ordinal == ordinal) + { + nodes.add(insn); + found = true; + } + + ordinal++; + } + } + + return found; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/MethodHead.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/MethodHead.java new file mode 100644 index 00000000..0b62586d --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/inject/MethodHead.java @@ -0,0 +1,28 @@ +package com.mumfrey.liteloader.transformers.event.inject; + +import java.util.Collection; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; + +import com.mumfrey.liteloader.transformers.event.Event; +import com.mumfrey.liteloader.transformers.event.InjectionPoint; + +/** + * An injection point which locates the first instruction in a method body + * + * @author Adam Mummery-Smith + */ +public class MethodHead extends InjectionPoint +{ + public MethodHead() + { + } + + @Override + public boolean find(String desc, InsnList insns, Collection nodes, Event event) + { + nodes.add(insns.getFirst()); + return true; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/InvalidEventJsonException.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/InvalidEventJsonException.java new file mode 100644 index 00000000..320730d7 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/InvalidEventJsonException.java @@ -0,0 +1,25 @@ +package com.mumfrey.liteloader.transformers.event.json; + +public class InvalidEventJsonException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public InvalidEventJsonException() + { + } + + public InvalidEventJsonException(String message) + { + super(message); + } + + public InvalidEventJsonException(Throwable cause) + { + super(cause); + } + + public InvalidEventJsonException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonDescriptor.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonDescriptor.java new file mode 100644 index 00000000..e8c0e265 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonDescriptor.java @@ -0,0 +1,93 @@ +package com.mumfrey.liteloader.transformers.event.json; + +import java.io.Serializable; +import java.util.UUID; + +import com.google.gson.annotations.SerializedName; +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.event.MethodInfo; + +/** + * A JSON method descriptor, + * + * @author Adam Mummery-Smith + */ +public class JsonDescriptor implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** + * Key used to refer to this method descriptor elsewhere + */ + @SerializedName("id") + private String key; + + /** + * Name of the class which owns this method + */ + @SerializedName("owner") + private String owner; + + /** + * Method name + */ + @SerializedName("name") + private String name; + + /** + * Method return type, assumes VOID if none specified + */ + @SerializedName("return") + private String returnType; + + /** + * Argument types for the method + */ + @SerializedName("args") + private String[] argumentTypes; + + /** + * Get the key used to refer to this method descriptor + */ + public String getKey() + { + if (this.key == null) + { + this.key = "UserDescriptor" + UUID.randomUUID().toString(); + } + + return this.key; + } + + /** + * @param obfTable + * @return MethodInfo for this descriptor + */ + public MethodInfo parse(JsonObfuscationTable obfTable) + { + if (this.owner == null || this.name == null) + { + throw new InvalidEventJsonException("Method descriptor was invalid, must specify owner and name!"); + } + + Obf owner = obfTable.parseClass(this.owner); + Obf name = obfTable.parseMethod(this.name); + + if (this.argumentTypes == null && this.returnType == null) + { + return new MethodInfo(owner, name); + } + + Object returnType = obfTable.parseType(this.returnType == null ? "VOID" : this.returnType); + Object[] args = (this.argumentTypes != null ? new Object[this.argumentTypes.length] : new Object[0]); + if (this.argumentTypes != null) + { + for (int arg = 0; arg < this.argumentTypes.length; arg++) + { + args[arg] = obfTable.parseType(this.argumentTypes[arg]); + } + } + + return new MethodInfo(owner, name, returnType, args); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonEvent.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonEvent.java new file mode 100644 index 00000000..4871ae39 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonEvent.java @@ -0,0 +1,159 @@ +package com.mumfrey.liteloader.transformers.event.json; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.annotations.SerializedName; +import com.mumfrey.liteloader.transformers.event.Event; +import com.mumfrey.liteloader.transformers.event.InjectionPoint; +import com.mumfrey.liteloader.transformers.event.MethodInfo; + +/** + * An event definition in JSON, serialisable class read by Gson + * + * @author Adam Mummery-Smith + */ +public class JsonEvent implements Serializable +{ + private static final long serialVersionUID = 1L; + + private static int nextEventID = 0; + + /** + * Event name + */ + @SerializedName("name") + private String name; + + /** + * Whether the event is cancellable + */ + @SerializedName("cancellable") + private boolean cancellable; + + /** + * Event priority (relative to other events at the same injection point) + */ + @SerializedName("priority") + private int priority = 1000; + + /** + * Injection points specified in the JSON file + */ + @SerializedName("injections") + private List jsonInjections; + + /** + * Listeners defined in the JSON file + */ + @SerializedName("listeners") + private List jsonListeners; + + /** + * Listener methods parsed from the JSON + */ + private transient List listeners = new ArrayList(); + + /** + * Get the name of this event + */ + public String getName() + { + if (this.name == null) + { + this.name = "onUserEvent" + (JsonEvent.nextEventID++); + } + + return this.name; + } + + /** + * Get whether this event is cancellable or not + */ + public boolean isCancellable() + { + return this.cancellable; + } + + /** + * Get the event priority + */ + public int getPriority() + { + return this.priority; + } + + /** + * Get the list of listeners parsed from the JSON + */ + public List getListeners() + { + return this.listeners; + } + + /** + * Parse the JSON to initialise this object + */ + public void parse(JsonMethods methods) + { + this.parseInjectionPoints(methods); + this.parseListeners(methods); + } + + /** + * @param methods + */ + private void parseInjectionPoints(JsonMethods methods) + { + if (this.jsonInjections == null || this.jsonInjections.isEmpty()) + { + throw new InvalidEventJsonException("Event " + this.getName() + " does not have any defined injections"); + } + + for (JsonInjection injection : this.jsonInjections) + { + injection.parse(methods); + } + } + + /** + * @param methods + */ + private void parseListeners(JsonMethods methods) + { + if (this.jsonListeners == null || this.jsonListeners.isEmpty()) + { + throw new InvalidEventJsonException("Event " + this.getName() + " does not have any defined listeners"); + } + + for (String listener : this.jsonListeners) + { + this.listeners.add(methods.get(listener)); + } + } + + /** + * @param transformer Transformer to register events with + * @return Event which was registered + */ + public Event register(ModEventInjectionTransformer transformer) + { + Event event = Event.getOrCreate(this.getName(), this.isCancellable(), this.getPriority()); + + for (JsonInjection injection : this.jsonInjections) + { + MethodInfo targetMethod = injection.getMethod(); + InjectionPoint injectionPoint = injection.getInjectionPoint(); + + transformer.registerEvent(event, targetMethod, injectionPoint); + } + + for (MethodInfo listener : this.listeners) + { + event.addListener(listener); + } + + return event; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonEvents.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonEvents.java new file mode 100644 index 00000000..0eb81491 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonEvents.java @@ -0,0 +1,205 @@ +package com.mumfrey.liteloader.transformers.event.json; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.transformers.ObfProvider; + +/** + * Serialisable class which represents a set of event injection definitions. + * Instances of this class are created by deserialising with JSON. The JSON + * string should be passed to the static {@link #parse} method which returns an + * instance of the class. + * + *

      After parsing, the events defined here can be injected into an event + * transformer instance by calling the {@link #register} method.

      + * + * @author Adam Mummery-Smith + */ +public class JsonEvents implements Serializable, ObfProvider +{ + private static final long serialVersionUID = 1L; + + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + /** + * Tokens are an instruction to the parser to look up a value rather than + * using a literal. + */ + private static final Pattern tokenPattern = Pattern.compile("^\\$\\{([a-zA-Z0-9_\\-\\.\\$]+)\\}$"); + + /** + * Serialised obfusctation entries + */ + @SerializedName("obfuscation") + private JsonObfuscationTable obfuscation; + + /** + * Serialised method descriptors + */ + @SerializedName("descriptors") + private List descriptors; + + /** + * Serialised events + */ + @SerializedName("events") + private List events; + + /** + * List of accessor interfaces + */ + @SerializedName("accessors") + private List accessors; + + /** + * Parsed method descriptors + */ + private transient JsonMethods methods; + + /** + * Parsed accessors + */ + private transient List accessorInterfaces = new ArrayList(); + + /** + * Attempts to parse the information in this object + */ + private void parse() + { + if (this.obfuscation == null) + { + this.obfuscation = new JsonObfuscationTable(); + } + + try + { + // Parse the obfuscation table + this.obfuscation.parse(); + + // Parse the descriptor list + this.methods = new JsonMethods(this.obfuscation, this.descriptors); + + if (this.events != null) + { + // Parse the events + for (JsonEvent event : this.events) + { + event.parse(this.methods); + } + } + + if (this.accessors != null) + { + for (String accessor : this.accessors) + { + if (accessor != null) + { + Obf accessorName = this.obfuscation.parseClass(accessor); + this.accessorInterfaces.add(accessorName.name); + } + } + } + } + catch (InvalidEventJsonException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new InvalidEventJsonException("An error occurred whilst parsing the event definition: " + ex.getClass().getSimpleName() + + ": " + ex.getMessage(), ex); + } + } + + public boolean hasAccessors() + { + return this.accessorInterfaces.size() > 0; + } + + /** + * Parse a token name, returns the token name as a string if the token is + * valid, or null if the token is not valid + * + * @param token + */ + static String parseToken(String token) + { + token = token.replace(" ", "").trim(); + + Matcher tokenPatternMatcher = JsonEvents.tokenPattern.matcher(token); + if (tokenPatternMatcher.matches()) + { + return tokenPatternMatcher.group(1); + } + + return null; + } + + /** + * Called to register all events defined in this object into the specified + * transformer. + * + * @param transformer + */ + public void register(ModEventInjectionTransformer transformer) + { + for (JsonEvent event : this.events) + { + event.register(transformer); + } + + for (String interfaceName : this.accessorInterfaces) + { + transformer.registerAccessor(interfaceName, this); + } + } + + /* (non-Javadoc) + * @see com.mumfrey.liteloader.transformers.ObfProvider + * #getByName(java.lang.String) + */ + @Override + public Obf getByName(String name) + { + return this.obfuscation.getByName(name); + } + +// public String toJson() +// { +// return JsonEvents.gson.toJson(this); +// } + + /** + * Parse a new JsonEvents object from the supplied JSON string + * + * @param json + * @return new JsonEvents instance + * @throws InvalidEventJsonException if the JSON ins invalid + */ + public static JsonEvents parse(String json) throws InvalidEventJsonException + { + try + { + JsonEvents newJsonEvents = JsonEvents.gson.fromJson(json, JsonEvents.class); + newJsonEvents.parse(); + return newJsonEvents; + } + catch (InvalidEventJsonException ex) + { + throw ex; + } + catch (Throwable th) + { + throw new InvalidEventJsonException("An error occurred whilst parsing the event definition: " + th.getClass().getSimpleName() + + ": " + th.getMessage(), th); + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjection.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjection.java new file mode 100644 index 00000000..c010329d --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjection.java @@ -0,0 +1,188 @@ +package com.mumfrey.liteloader.transformers.event.json; + +import java.io.Serializable; +import java.lang.reflect.Constructor; + +import com.google.gson.annotations.SerializedName; +import com.mumfrey.liteloader.transformers.event.InjectionPoint; +import com.mumfrey.liteloader.transformers.event.MethodInfo; +import com.mumfrey.liteloader.transformers.event.inject.BeforeInvoke; +import com.mumfrey.liteloader.transformers.event.inject.BeforeReturn; +import com.mumfrey.liteloader.transformers.event.inject.BeforeStringInvoke; +import com.mumfrey.liteloader.transformers.event.inject.MethodHead; + +/** + * A JSON injection point definition + * + * @author Adam Mummery-Smith + */ +public class JsonInjection implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** + * Method to inject into + */ + @SerializedName("method") + private String methodName; + + /** + * Type of injection point + */ + @SerializedName("type") + private JsonInjectionType type; + + /** + * Shift type (optional) + */ + @SerializedName("shift") + private JsonInjectionShiftType shift; + + /** + * Target method to search for when using INVOKE and INVOKESTRING + */ + @SerializedName("target") + private String target; + + /** + * Ordinal to use when using INVOKE and INVOKESTRING + */ + @SerializedName("ordinal") + private int ordinal = -1; + + /** + * InjectionPoint class to use for CUSTOM + */ + @SerializedName("class") + private String className; + + /** + * Constructor arguments to pass wehn using CUSTOM + */ + @SerializedName("args") + private Object[] args; + + private transient MethodInfo method; + + private transient InjectionPoint injectionPoint; + + public MethodInfo getMethod() + { + return this.method; + } + + public InjectionPoint getInjectionPoint() + { + return this.injectionPoint; + } + + public void parse(JsonMethods methods) + { + this.method = this.parseMethod(methods); + this.injectionPoint = this.parseInjectionPoint(methods); + } + + private MethodInfo parseMethod(JsonMethods methods) + { + try + { + return methods.get(this.methodName); + } + catch (NullPointerException ex) + { + throw new InvalidEventJsonException("'method' must not be null for injection"); + } + } + + public InjectionPoint parseInjectionPoint(JsonMethods methods) + { + switch (this.type) + { + case INVOKE: + return this.applyShift(new BeforeInvoke(methods.get(this.getTarget()), this.ordinal)); + + case INVOKESTRING: + return this.applyShift(new BeforeStringInvoke(this.getArg(0).toString(), methods.get(this.getTarget()), this.ordinal)); + + case RETURN: + return this.applyShift(new BeforeReturn(this.ordinal)); + + case HEAD: + return new MethodHead(); + + case CUSTOM: + try + { + @SuppressWarnings("unchecked") + Class injectionPointClass = (Class)Class.forName(this.className); + if (this.args != null) + { + Constructor ctor = injectionPointClass.getDeclaredConstructor(Object[].class); + return ctor.newInstance(this.args); + } + return injectionPointClass.newInstance(); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + + default: + throw new InvalidEventJsonException("Could not parse injection type"); + } + } + + private Object getArg(int arg) + { + if (this.args == null || this.args.length >= this.args.length || arg < 0) + { + return ""; + } + + return this.args[arg]; + } + + private String getTarget() + { + if (this.target != null && this.shift == null) + { + if (this.target.startsWith("before(") && this.target.endsWith(")")) + { + this.target = this.target.substring(7, this.target.length() - 1); + this.shift = JsonInjectionShiftType.BEFORE; + } + else if (this.target.startsWith("after(") && this.target.endsWith(")")) + { + this.target = this.target.substring(6, this.target.length() - 1); + this.shift = JsonInjectionShiftType.AFTER; + } + } + + if (this.target == null) + { + throw new InvalidEventJsonException("'target' is required for injection type " + this.type.name()); + } + + return this.target; + } + + private InjectionPoint applyShift(InjectionPoint injectionPoint) + { + if (this.shift != null) + { + switch (this.shift) + { + case AFTER: + return InjectionPoint.after(injectionPoint); + + case BEFORE: + return InjectionPoint.before(injectionPoint); + + default: + return injectionPoint; + } + } + + return injectionPoint; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjectionShiftType.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjectionShiftType.java new file mode 100644 index 00000000..c10ebbb2 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjectionShiftType.java @@ -0,0 +1,7 @@ +package com.mumfrey.liteloader.transformers.event.json; + +public enum JsonInjectionShiftType +{ + BEFORE, + AFTER +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjectionType.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjectionType.java new file mode 100644 index 00000000..ec50a18b --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonInjectionType.java @@ -0,0 +1,11 @@ +package com.mumfrey.liteloader.transformers.event.json; + +public enum JsonInjectionType +{ + INVOKE, + INVOKESTRING, + FIELD, + RETURN, + HEAD, + CUSTOM; +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonMethods.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonMethods.java new file mode 100644 index 00000000..af0315c4 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonMethods.java @@ -0,0 +1,87 @@ +package com.mumfrey.liteloader.transformers.event.json; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.mumfrey.liteloader.core.runtime.Methods; +import com.mumfrey.liteloader.transformers.event.MethodInfo; + +/** + * A simple registry of MethodInfo objects parsed from the JSON, objects which + * consume the specified MethodInfo objects will be passed an instance of this + * object at parse time. + * + * @author Adam Mummery-Smith + */ +public class JsonMethods +{ + /** + * Serialised obfusctation entries + */ + private final JsonObfuscationTable obfuscation; + + /** + * Method descriptors + */ + private final List descriptors; + + /** + * Method descriptors which have been parsed from the descriptors collection + */ + private Map methods = new HashMap(); + + /** + * @param obfuscation + * @param descriptors + */ + public JsonMethods(JsonObfuscationTable obfuscation, List descriptors) + { + this.obfuscation = obfuscation; + this.descriptors = descriptors; + + this.parse(); + } + + /** + * + */ + private void parse() + { + if (this.descriptors != null) + { + for (JsonDescriptor descriptor : this.descriptors) + { + this.methods.put(descriptor.getKey(), descriptor.parse(this.obfuscation)); + } + } + } + + /** + * Fetches a method descriptor by token + * + * @param token + */ + public MethodInfo get(String token) + { + String key = JsonEvents.parseToken(token); + if (key == null) + { + throw new InvalidEventJsonException("\"" + token + "\" is not a valid token"); + } + + MethodInfo method = this.methods.get(key); + if (method != null) + { + return method; + } + + MethodInfo builtinMethod = Methods.getByName(key); + if (builtinMethod != null) + { + return builtinMethod; + } + + throw new InvalidEventJsonException("Could not locate method descriptor with token " + token); + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonObf.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonObf.java new file mode 100644 index 00000000..54b7c94f --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonObf.java @@ -0,0 +1,61 @@ +package com.mumfrey.liteloader.transformers.event.json; + +import java.io.Serializable; +import java.util.UUID; + +import com.google.gson.annotations.SerializedName; +import com.mumfrey.liteloader.core.runtime.Obf; + +public class JsonObf implements Serializable +{ + private static final long serialVersionUID = 1L; + + @SerializedName("id") + private String key; + + @SerializedName("mcp") + private String mcp; + + @SerializedName("srg") + private String srg; + + @SerializedName("obf") + private String obf; + + public String getKey() + { + if (this.key == null) + { + this.key = "UserObfuscationMapping" + UUID.randomUUID().toString(); + } + + return this.key; + } + + public Obf parse() + { + String seargeName = this.getFirstValidEntry(this.srg, this.mcp, this.obf, this.getKey()); + String obfName = this.getFirstValidEntry(this.obf, this.srg, this.mcp, this.getKey()); + String mcpName = this.getFirstValidEntry(this.mcp, this.srg, this.obf, this.getKey()); + + return new Mapping(seargeName, obfName, mcpName); + } + + private String getFirstValidEntry(String... entries) + { + for (String entry : entries) + { + if (entry != null) return entry; + } + + throw new InvalidEventJsonException("No valid entry found in list!"); + } + + public static class Mapping extends Obf + { + protected Mapping(String seargeName, String obfName, String mcpName) + { + super(seargeName, obfName, mcpName); + } + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonObfuscationTable.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonObfuscationTable.java new file mode 100644 index 00000000..1cd594e2 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/JsonObfuscationTable.java @@ -0,0 +1,182 @@ +package com.mumfrey.liteloader.transformers.event.json; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.annotations.SerializedName; +import com.mumfrey.liteloader.core.runtime.Obf; +import com.mumfrey.liteloader.core.runtime.Packets; + +/** + * JSON-defined obfuscation table entries used like a registry by the other JSON + * components to look up obfuscation mappings for methods and fields. + * + * @author Adam Mummery-Smith + */ +public class JsonObfuscationTable implements Serializable +{ + private static final long serialVersionUID = 1L; + + @SerializedName("classes") + private List jsonClasses; + + @SerializedName("methods") + private List jsonMethods; + + @SerializedName("fields") + private List jsonFields; + + // Parsed values + private transient Map classObfs = new HashMap(); + private transient Map methodObfs = new HashMap(); + private transient Map fieldObfs = new HashMap(); + + /** + * Parse the entries in each collection to actual Obf objects + */ + public void parse() + { + if (this.jsonClasses != null) + { + for (JsonObf jsonClass : this.jsonClasses) + { + this.classObfs.put(jsonClass.getKey(), jsonClass.parse()); + } + } + + if (this.jsonMethods != null) + { + for (JsonObf jsonMethod : this.jsonMethods) + { + this.methodObfs.put(jsonMethod.getKey(), jsonMethod.parse()); + } + } + + if (this.jsonFields != null) + { + for (JsonObf jsonField : this.jsonFields) + { + this.fieldObfs.put(jsonField.getKey(), jsonField.parse()); + } + } + } + + /** + * Look up a type (a class or primitive type) by token + */ + public Object parseType(String token) + { + token = token.replace(" ", "").trim(); + + if ("I".equals(token) || "INT".equals(token)) return Integer.TYPE; + if ("J".equals(token) || "LONG".equals(token)) return Long.TYPE; + if ("V".equals(token) || "VOID".equals(token)) return Void.TYPE; + if ("Z".equals(token) || "BOOLEAN".equals(token) || "BOOL".equals(token)) return Boolean.TYPE; + if ("B".equals(token) || "BYTE".equals(token)) return Byte.TYPE; + if ("C".equals(token) || "CHAR".equals(token)) return Character.TYPE; + if ("S".equals(token) || "SHORT".equals(token)) return Short.TYPE; + if ("D".equals(token) || "DOUBLE".equals(token)) return Double.TYPE; + if ("F".equals(token) || "FLOAT".equals(token)) return Float.TYPE; + if ("STRING".equals(token)) return String.class; + + if (token.startsWith("L") && token.endsWith(";")) + { + token = token.substring(1, token.length() - 1).replace('/', '.'); + } + + return this.parseClass(token); + } + + /** + * Find an obf entry of any type by name + * + * @param name + */ + public Obf getByName(String name) + { + Obf classObf = this.classObfs.get(name); + if (classObf != null) + { + return classObf; + } + + Obf methodObf = this.methodObfs.get(name); + if (methodObf != null) + { + return methodObf; + } + + Obf fieldObf = this.fieldObfs.get(name); + if (fieldObf != null) + { + return fieldObf; + } + + return null; + } + + /** + * @param token + */ + public Obf parseClass(String token) + { + return this.parseObf(token, this.classObfs, false); + } + + /** + * @param token + */ + public Obf parseMethod(String token) + { + return this.parseObf(token, this.methodObfs, false); + } + + /** + * @param token + */ + public Obf parseField(String token) + { + return this.parseObf(token, this.fieldObfs, false); + } + + /** + * @param token + * @param obfs + * @param returnNullOnFailure return null instead of throwing an exception + */ + private Obf parseObf(String token, Map obfs, boolean returnNullOnFailure) + { + String key = JsonEvents.parseToken(token); + + if (key != null) + { + if (obfs.containsKey(key)) + { + return obfs.get(key); + } + + Obf obf = Obf.getByName(key); + if (obf != null) + { + return obf; + } + + Packets packet = Packets.getByName(key); + if (packet != null) + { + return packet; + } + + if (returnNullOnFailure) + { + return null; + } + + throw new InvalidEventJsonException("The token " + token + " could not be resolved to a type"); + } + + return new JsonObf.Mapping(token, token, token); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/ModEventInjectionTransformer.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/ModEventInjectionTransformer.java new file mode 100644 index 00000000..7cd71644 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/ModEventInjectionTransformer.java @@ -0,0 +1,87 @@ +package com.mumfrey.liteloader.transformers.event.json; + +import com.mumfrey.liteloader.transformers.ClassTransformer; +import com.mumfrey.liteloader.transformers.ObfProvider; +import com.mumfrey.liteloader.transformers.event.Event; +import com.mumfrey.liteloader.transformers.event.EventInjectionTransformer; +import com.mumfrey.liteloader.transformers.event.InjectionPoint; +import com.mumfrey.liteloader.transformers.event.MethodInfo; +import com.mumfrey.liteloader.transformers.event.json.ModEvents.ModEventDefinition; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger.Verbosity; + +/** + * Event transformer which manages injections of mod events specified via + * events.json in the mod container. + * + * @author Adam Mummery-Smith + */ +public class ModEventInjectionTransformer extends EventInjectionTransformer +{ + @Override + protected void addEvents() + { + for (ModEventDefinition eventsDefinition : ModEvents.getEvents().values()) + { + this.addEvents(eventsDefinition); + } + } + + /** + * @param identifier + * @param json + */ + private void addEvents(ModEventDefinition def) + { + JsonEvents events = null; + + try + { + LiteLoaderLogger.info("Parsing events for mod with id %s", def.getIdentifier()); + events = JsonEvents.parse(def.getJson()); + } + catch (InvalidEventJsonException ex) + { + LiteLoaderLogger.debug(ClassTransformer.HORIZONTAL_RULE); + LiteLoaderLogger.debug(ex.getMessage()); + LiteLoaderLogger.debug(ClassTransformer.HORIZONTAL_RULE); + LiteLoaderLogger.debug(def.getJson()); + LiteLoaderLogger.debug(ClassTransformer.HORIZONTAL_RULE); + LiteLoaderLogger.severe(ex, "Invalid JSON event declarations for mod with id %s", def.getIdentifier()); + } + catch (Throwable ex) + { + LiteLoaderLogger.severe(ex, "Error whilst parsing event declarations for mod with id %s", def.getIdentifier()); + } + + try + { + if (events != null) + { + if (events.hasAccessors()) + { + LiteLoaderLogger.info("%s contains Accessor definitions, injecting into classpath...", def.getIdentifier()); + def.injectIntoClassPath(); + } + + LiteLoaderLogger.info(Verbosity.REDUCED, "Registering events for mod with id %s", def.getIdentifier()); + events.register(this); + def.onEventsInjected(); + } + } + catch (Throwable ex) + { + LiteLoaderLogger.severe(ex, "Error whilst parsing event declarations for mod with id %s", def.getIdentifier()); + } + } + + protected Event registerEvent(Event event, MethodInfo targetMethod, InjectionPoint injectionPoint) + { + return super.addEvent(event, targetMethod, injectionPoint); + } + + protected void registerAccessor(String interfaceName, ObfProvider obfProvider) + { + super.addAccessor(interfaceName, obfProvider); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/ModEvents.java b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/ModEvents.java new file mode 100644 index 00000000..4e2e876e --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/transformers/event/json/ModEvents.java @@ -0,0 +1,104 @@ +package com.mumfrey.liteloader.transformers.event.json; + +import java.io.File; +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; + +import net.minecraft.launchwrapper.Launch; + +import com.google.common.base.Charsets; +import com.mumfrey.liteloader.api.ContainerRegistry.DisabledReason; +import com.mumfrey.liteloader.api.EnumerationObserver; +import com.mumfrey.liteloader.core.ModInfo; +import com.mumfrey.liteloader.core.api.LoadableModFile; +import com.mumfrey.liteloader.interfaces.LoadableMod; +import com.mumfrey.liteloader.interfaces.LoaderEnumerator; +import com.mumfrey.liteloader.interfaces.TweakContainer; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +public class ModEvents implements EnumerationObserver +{ + public static class ModEventDefinition + { + private final LoadableModFile file; + + private final String identifier; + + private final String json; + + public ModEventDefinition(LoadableModFile file, String json) + { + this.file = file; + this.identifier = file.getIdentifier(); + this.json = json; + } + + public String getIdentifier() + { + return this.identifier; + } + + public String getJson() + { + return this.json; + } + + public void onEventsInjected() + { + this.file.onEventsInjected(); + } + + public void injectIntoClassPath() + { + try + { + this.file.injectIntoClassPath(Launch.classLoader, true); + } + catch (MalformedURLException ex) + { + ex.printStackTrace(); + } + } + } + + private static final String DEFINITION_FILENAME = "events.json"; + + private static Map events = new HashMap(); + + @Override + public void onRegisterEnabledContainer(LoaderEnumerator enumerator, LoadableMod container) + { + if (container instanceof LoadableModFile) + { + LoadableModFile file = (LoadableModFile)container; + if (!file.exists()) return; + + String json = file.getFileContents(ModEvents.DEFINITION_FILENAME, Charsets.UTF_8); + if (json == null) return; + + LiteLoaderLogger.info("Registering %s for mod with id %s", ModEvents.DEFINITION_FILENAME, file.getIdentifier()); + ModEvents.events.put(file.getIdentifier(), new ModEventDefinition(file, json)); + } + } + + @Override + public void onRegisterDisabledContainer(LoaderEnumerator enumerator, LoadableMod container, DisabledReason reason) + { + } + + @Override + public void onRegisterTweakContainer(LoaderEnumerator enumerator, TweakContainer container) + { + } + + @Override + public void onModAdded(LoaderEnumerator enumerator, ModInfo> mod) + { + } + + static Map getEvents() + { + return events; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/update/UpdateSite.java b/liteloader/src/main/java/com/mumfrey/liteloader/update/UpdateSite.java new file mode 100644 index 00000000..7b3f5c0a --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/update/UpdateSite.java @@ -0,0 +1,421 @@ +package com.mumfrey.liteloader.update; + +import java.text.DateFormat; +import java.util.Comparator; +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; +import com.mumfrey.liteloader.util.net.HttpStringRetriever; + +/** + * An update site, used by liteloader to check for new versions but is also + * available to mods who may want to use a similar system. + * + * @author Adam Mummery-Smith + */ +public class UpdateSite implements Comparator +{ + /** + * Gson instance for deserializing remote version data + */ + private static Gson gson = new Gson(); + + /** + * Base URL of the remote update site + */ + private final String updateSiteUrl; + + /** + * Name of the json file containing the remote versions data, eg. + * versions.json + */ + private final String updateSiteJsonFileName; + + /** + * The version of minecraft being targetted + */ + private final String targetVersion; + + /** + * Artefact name in the form "com.somedomain.pkg:artefactname" + */ + private final String artefact; + + /** + * Local artefact timestamp + */ + private final long currentTimeStamp; + + /** + * Comparator for the timestamps + */ + private final Comparator timeStampComparator; + + /** + * Threading lock object + */ + private final Object lock = new Object(); + + /** + * StringRetriever which will fetch the remote json file, null when not + * performing a fetch operation. + */ + private HttpStringRetriever stringRetriever; + + /** + * True if the check is complete (even if it failed) + */ + private volatile boolean checkComplete = false; + + /** + * True if the check succeeded + */ + private volatile boolean checkSuccess = false; + + /** + * True if an updated version is available + */ + private volatile boolean updateAvailable = false; + + /** + * The version which is available + */ + private String availableVersion = null; + + /** + * The version which is available + */ + private String availableVersionDate = null; + + /** + * The URL to the available artefact + */ + private String availableVersionURL = null; + + /** + * Create a new UpdateSite with the specified information + * + * @param updateSiteUrl Base URL of the update site, should include the + * trailing slash + * @param jsonFileName Name of the json file on the remote site containing + * the version data, eg. versions.json + * @param targetVersion The target minecraft version + * @param artefact Artefact name in the form + * "com.somedomain.pkg:artefactname" + * @param currentTimeStamp Timestamp of the current artefact + * @param timeStampComparator Comparator to use for comparing timestamps, if + * null uses built in comparator + */ + public UpdateSite(String updateSiteUrl, String jsonFileName, String targetVersion, String artefact, long currentTimeStamp, + Comparator timeStampComparator) + { + this.updateSiteUrl = updateSiteUrl + (updateSiteUrl.endsWith("/") ? "" : "/"); + this.updateSiteJsonFileName = jsonFileName; + + this.targetVersion = targetVersion; + this.artefact = artefact; + this.currentTimeStamp = currentTimeStamp; + + this.timeStampComparator = timeStampComparator != null ? timeStampComparator : this; + } + + /** + * Create a new UpdateSite with the specified information + * + * @param updateSiteUrl Base URL of the update site, should include the + * trailing slash + * @param jsonFileName Name of the json file on the remote site containing + * the version data, eg. versions.json + * @param targetVersion The target minecraft version + * @param artefact Artefact name in the form + * "com.somedomain.pkg:artefactname" + * @param currentTimeStamp Timestamp of the current artefact + */ + public UpdateSite(String updateSiteUrl, String jsonFileName, String targetVersion, String artefact, long currentTimeStamp) + { + this(updateSiteUrl, jsonFileName, targetVersion, artefact, currentTimeStamp, null); + } + + /** + * If an update check is not already in progress, starts an update check + */ + public void beginUpdateCheck() + { + synchronized (this.lock) + { + if (this.stringRetriever == null) + { + LiteLoaderLogger.debug("Update site for %s is starting the update check", this.artefact); + this.stringRetriever = new HttpStringRetriever(String.format("%s%s", this.updateSiteUrl, this.updateSiteJsonFileName)); + this.stringRetriever.start(); + } + } + } + + /** + * Gets whether a check is in progress + */ + public boolean isCheckInProgress() + { + this.update(); + boolean checkInProgress = false; + + synchronized (this.lock) + { + checkInProgress = this.stringRetriever != null; + } + + return checkInProgress; + } + + /** + * Gets whether a check has been completed + */ + public boolean isCheckComplete() + { + this.update(); + return this.checkComplete; + } + + /** + * Gets whether the last check was a success + */ + public boolean isCheckSucceess() + { + this.update(); + return this.checkComplete && this.checkSuccess; + } + + /** + * Gets whether an update is available at the remote site + */ + public boolean isUpdateAvailable() + { + this.update(); + return this.updateAvailable; + } + + /** + * Gets the latest version available at the remote site + */ + public String getAvailableVersion() + { + this.update(); + return this.availableVersion; + } + + /** + * Gets the latest version available at the remote site + */ + public String getAvailableVersionDate() + { + this.update(); + return this.availableVersionDate; + } + + /** + * Gets the URL to the available version + */ + public String getAvailableVersionURL() + { + this.update(); + return this.availableVersionURL; + } + + /** + * Check whether an in-progress check has completed and if it has parse the + * retrieved data. + */ + private void update() + { + synchronized (this.lock) + { + if (this.stringRetriever != null && this.stringRetriever.isDone()) + { + this.checkComplete = true; + this.checkSuccess = this.stringRetriever.getSuccess(); + + if (this.checkSuccess) + { + try + { + LiteLoaderLogger.debug("Update site for %s is parsing the update response", this.artefact); + this.parseData(this.stringRetriever.getString()); + LiteLoaderLogger.debug("Update site for %s successfully parsed the update response", this.artefact); + } + catch (Exception ex) + { + LiteLoaderLogger.debug("Update site for %s failed parsing the update response: %s:%s", this.artefact, + ex.getClass().getSimpleName(), ex.getMessage()); + this.checkSuccess = false; + ex.printStackTrace(); + } + } + + this.stringRetriever = null; + } + } + } + + /** + * Parse data receieved from a query + * + * @param json + */ + private void parseData(String json) + { + this.updateAvailable = false; + + try + { + Map data = UpdateSite.gson.fromJson(json, Map.class); + + if (data.containsKey("versions")) + { + this.handleVersionsData((Map)data.get("versions")); + } + else + { + LiteLoaderLogger.warning("No key 'versions' in update site JSON"); + } + } + catch (JsonSyntaxException ex) + { + ex.printStackTrace(); + LiteLoaderLogger.warning("Error parsing update site JSON: %s: %s", ex.getClass().getSimpleName(), ex.getMessage()); + } + } + + /** + * @param versions + */ + private void handleVersionsData(Map versions) + { + if (versions.containsKey(this.targetVersion)) + { + for (Entry versionDataEntry : ((Map)versions.get(this.targetVersion)).entrySet()) + { + this.handleVersionData(versionDataEntry.getKey(), (Map)versionDataEntry.getValue()); + } + } + else + { + LiteLoaderLogger.warning("No version entry for current version '%s' in update site JSON", this.targetVersion); + } + } + + /** + * @param key + * @param value + */ + private void handleVersionData(Object key, Map value) + { + if ("artefacts".equals(key)) + { + if (value.containsKey(this.artefact)) + { + this.handleArtefactData((Map)value.get(this.artefact)); + } + else + { + LiteLoaderLogger.warning("No artefacts entry for specified artefact '%s' in update site JSON", this.artefact); + } + } + } + + /** + * @param availableArtefacts + */ + @SuppressWarnings("unchecked") + private void handleArtefactData(Map availableArtefacts) + { + if (availableArtefacts.containsKey("latest")) + { + Map latestArtefact = (Map)availableArtefacts.get("latest"); + this.checkAndUseRemoteArtefact(latestArtefact, this.currentTimeStamp, false); + } + else + { + LiteLoaderLogger.warning("No key 'latest' in update site JSON"); + + long bestTimeStamp = this.currentTimeStamp; + Map bestRemoteArtefact = null; + + for (Map remoteArtefact : ((Map>)availableArtefacts).values()) + { + if (this.checkAndUseRemoteArtefact(remoteArtefact, bestTimeStamp, true)) + { + bestTimeStamp = Long.parseLong(remoteArtefact.get("timestamp").toString()); + bestRemoteArtefact = remoteArtefact; + } + } + + if (bestRemoteArtefact != null) + { + this.availableVersion = bestRemoteArtefact.get("version").toString(); + this.availableVersionURL = this.createArtefactURL(bestRemoteArtefact.get("file").toString()); + this.updateAvailable = this.compareTimeStamps(this.currentTimeStamp, bestTimeStamp); + } + } + } + + /** + * @param artefact + * @param bestTimeStamp + * @param checkOnly + */ + private boolean checkAndUseRemoteArtefact(Map artefact, long bestTimeStamp, boolean checkOnly) + { + if (artefact.containsKey("file") && artefact.containsKey("version") && artefact.containsKey("timestamp")) + { + Long remoteTimeStamp = Long.parseLong(artefact.get("timestamp").toString()); + + if (checkOnly) + { + return this.compareTimeStamps(bestTimeStamp, remoteTimeStamp); + } + + this.availableVersion = artefact.get("version").toString(); + this.availableVersionDate = DateFormat.getDateTimeInstance().format(new Date(remoteTimeStamp * 1000L)); + this.availableVersionURL = this.createArtefactURL(artefact.get("file").toString()); + this.updateAvailable = this.compareTimeStamps(bestTimeStamp, remoteTimeStamp); + + return true; + } + + return false; + } + + /** + * @param bestTimeStamp + * @param remoteTimeStamp + */ + private boolean compareTimeStamps(long bestTimeStamp, Long remoteTimeStamp) + { + return this.timeStampComparator.compare(bestTimeStamp, remoteTimeStamp) < 0; + } + + /** + * @param file + */ + private String createArtefactURL(String file) + { + return String.format("%s%s/%s/%s", this.updateSiteUrl, this.artefact.replace('.', '/').replace(':', '/'), this.targetVersion, file); + } + + /* (non-Javadoc) + * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) + */ + @Override + public int compare(Long localTimestamp, Long remoteTimestamp) + { + if (localTimestamp == null && remoteTimestamp == null) return 0; + if (localTimestamp == null) return -1; + if (remoteTimestamp == null) return 1; + return (int)(localTimestamp.longValue() - remoteTimestamp.longValue()); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/ChatUtilities.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/ChatUtilities.java new file mode 100644 index 00000000..983f8a24 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/ChatUtilities.java @@ -0,0 +1,132 @@ +package com.mumfrey.liteloader.util; + +import java.util.List; + +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.ChatStyle; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.IChatComponent; + +/** + * Utility functions for chat + * + * @author Adam Mummery-Smith + */ +public abstract class ChatUtilities +{ + private static String formattingCodeLookup; + + static + { + StringBuilder formattingCodes = new StringBuilder(); + + for (EnumChatFormatting chatFormat : EnumChatFormatting.values()) + { + formattingCodes.append(chatFormat.toString().charAt(1)); + } + + ChatUtilities.formattingCodeLookup = formattingCodes.toString(); + } + + private ChatUtilities() {} + + /** + * Get a chat style from a legacy formatting code + * + * @param code Code + * @return chat style + */ + public static ChatStyle getChatStyleFromCode(char code) + { + int pos = ChatUtilities.formattingCodeLookup.indexOf(code); + if (pos < 0) return null; + EnumChatFormatting format = EnumChatFormatting.values()[pos]; + + ChatStyle style = new ChatStyle(); + if (format.isColor()) + { + style.setColor(format); + } + else if (format.isFancyStyling()) + { + switch (format) + { + case BOLD: style.setBold(true); break; + case ITALIC: style.setItalic(true); break; + case STRIKETHROUGH: style.setStrikethrough(true); break; + case UNDERLINE: style.setUnderlined(true); break; + case OBFUSCATED: style.setObfuscated(true); break; + default: return style; + } + } + + return style; + } + + /** + * Convert a component containing text formatted with legacy codes to a + * native ChatComponent structure. + */ + public static IChatComponent convertLegacyCodes(IChatComponent chat) + { + return ChatUtilities.covertCodesInPlace(chat); + } + + private static List covertCodesInPlace(List siblings) + { + for (int index = 0; index < siblings.size(); index++) + { + siblings.set(index, ChatUtilities.covertCodesInPlace(siblings.get(index))); + } + + return siblings; + } + + @SuppressWarnings("unchecked") + private static IChatComponent covertCodesInPlace(IChatComponent component) + { + IChatComponent newComponent = null; + if (component instanceof ChatComponentText) + { + ChatComponentText textComponent = (ChatComponentText)component; + ChatStyle style = textComponent.getChatStyle(); + String text = textComponent.getChatComponentText_TextValue(); + + int pos = text.indexOf('\247'); + while (pos > -1 && text != null) + { + if (pos < text.length() - 1) + { + IChatComponent head = new ChatComponentText(pos > 0 ? text.substring(0, pos) : "").setChatStyle(style); + style = ChatUtilities.getChatStyleFromCode(text.charAt(pos + 1)); + text = text.substring(pos + 2); + newComponent = (newComponent == null) ? head : newComponent.appendSibling(head); + pos = text.indexOf('\247'); + } + else + { + text = null; + } + } + + if (text != null) + { + IChatComponent tail = new ChatComponentText(text).setChatStyle(style); + newComponent = (newComponent == null) ? tail : newComponent.appendSibling(tail); + } + } + + if (newComponent == null) + { + ChatUtilities.covertCodesInPlace(component.getSiblings()); + return component; + } + + for (IChatComponent oldSibling : ChatUtilities.covertCodesInPlace((List)component.getSiblings())) + { + newComponent.appendSibling(oldSibling); + } + + return newComponent; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/EntityUtilities.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/EntityUtilities.java new file mode 100644 index 00000000..7feb857f --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/EntityUtilities.java @@ -0,0 +1,29 @@ +package com.mumfrey.liteloader.util; + +import net.minecraft.entity.Entity; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; + +public abstract class EntityUtilities +{ + public static MovingObjectPosition rayTraceFromEntity(Entity entity, double traceDistance, float partialTicks) + { + Vec3 var4 = EntityUtilities.getPositionEyes(entity, partialTicks); + Vec3 var5 = entity.getLook(partialTicks); + Vec3 var6 = var4.addVector(var5.xCoord * traceDistance, var5.yCoord * traceDistance, var5.zCoord * traceDistance); + return entity.worldObj.rayTraceBlocks(var4, var6, false, false, true); + } + + public static Vec3 getPositionEyes(Entity entity, float partialTicks) + { + if (partialTicks == 1.0F) + { + return new Vec3(entity.posX, entity.posY + entity.getEyeHeight(), entity.posZ); + } + + double interpX = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks; + double interpY = entity.prevPosY + (entity.posY - entity.prevPosY) * partialTicks + entity.getEyeHeight(); + double interpZ = entity.prevPosZ + (entity.posZ - entity.prevPosZ) * partialTicks; + return new Vec3(interpX, interpY, interpZ); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/Input.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/Input.java new file mode 100644 index 00000000..f72df8c4 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/Input.java @@ -0,0 +1,93 @@ +package com.mumfrey.liteloader.util; + +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.network.INetHandler; +import net.minecraft.network.play.server.S01PacketJoinGame; +import net.minecraft.world.World; + +import com.mumfrey.liteloader.api.CoreProvider; +import com.mumfrey.liteloader.core.LiteLoaderMods; +import com.mumfrey.liteloader.util.jinput.ComponentRegistry; + +public abstract class Input implements CoreProvider +{ + /** + * Register a key for a mod + * + * @param binding + */ + public abstract void registerKeyBinding(KeyBinding binding); + + /** + * Unregisters a registered keybind with the game settings class, thus + * removing it from the "controls" screen. + * + * @param binding + */ + public abstract void unRegisterKeyBinding(KeyBinding binding); + + /** + * Writes mod bindings to disk + */ + public abstract void storeBindings(); + + /** + * Gets the underlying JInput component registry + */ + public abstract ComponentRegistry getComponentRegistry(); + + /** + * Returns a handle to the event described by descriptor (or null if no + * component is found matching the descriptor. Retrieving an event via this + * method adds the controller (if found) to the polling list and causes it + * to raise events against the specified handler. + * + *

      This method returns an {@link InputEvent} which is passed as an + * argument to the relevant callback on the supplied handler in order to + * identify the event. For example:

      + * + * this.joystickButton = input.getEvent(descriptor, this); + * + *

      then in onAxisEvent

      + * + * if (source == this.joystickButton) // do something with button + * + * + * @param descriptor + * @param handler + */ + public abstract InputEvent getEvent(String descriptor, InputHandler handler); + + /** + * Get events for all components which match the supplied descriptor + * + * @param descriptor + * @param handler + */ + public abstract InputEvent[] getEvents(String descriptor, InputHandler handler); + + @Override + public void onPostInitComplete(LiteLoaderMods mods) + { + } + + @Override + public void onStartupComplete() + { + } + + @Override + public void onJoinGame(INetHandler netHandler, S01PacketJoinGame loginPacket) + { + } + + @Override + public void onWorldChanged(World world) + { + } + + @Override + public void onPostRender(int mouseX, int mouseY, float partialTicks) + { + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/InputEvent.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/InputEvent.java new file mode 100644 index 00000000..6e383789 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/InputEvent.java @@ -0,0 +1,136 @@ +package com.mumfrey.liteloader.util; + +import net.java.games.input.Component; +import net.java.games.input.Controller; +import net.java.games.input.Event; + +/** + * A handle to an input event, this handle will be used to call back the handler + * for the specified component's events. This class represents a singly-linked + * list of delegates with each delegate's next field pointing to the next + * delegate in the chain. + * + * @author Adam Mummery-Smith + */ +public class InputEvent +{ + /** + * Controller this event is delegating events for + */ + private final Controller controller; + + /** + * Component this event is delegating events for + */ + private final Component component; + + /** + * Event handler + */ + private final InputHandler handler; + + /** + * Next event handler in the chain + */ + private InputEvent next; + + /** + * @param controller + * @param component + * @param handler + */ + InputEvent(Controller controller, Component component, InputHandler handler) + { + this.controller = controller; + this.component = component; + this.handler = handler; + } + + /** + * Link this delegate to the specified delegate and return the start of the + * delegate chain. + * + * @param chain delegate to link to (will be null if the chain is empty) + */ + InputEvent link(InputEvent chain) + { + if (chain == null) return this; // Chain is empty, return self + return chain.append(this); // Append self to the start of the chain + } + + /** + * Append specified delegate to the end of the delegate chain + * + * @param delegate + */ + private InputEvent append(InputEvent delegate) + { + InputEvent tail = this; // Start here + + while (tail.next != null) // Find the end of the chain + { + tail = tail.next; + } + + tail.next = delegate; // Append the new delegate + return this; // Return the start of the delegate chain (eg. this node) + } + + /** + * @param event + */ + void onEvent(Event event) + { + if (this.component.isAnalog()) + { + this.onAxisEvent(event.getValue(), event.getNanos()); + } + else if (this.component.getIdentifier() == Component.Identifier.Axis.POV) + { + this.onPovEvent(event.getValue(), event.getNanos()); + } + else + { + this.onButtonEvent(event.getValue() == 1.0F); + } + } + + /** + * @param value + * @param nanos + */ + private void onAxisEvent(float value, long nanos) + { + this.handler.onAxisEvent(this, value, nanos); + if (this.next != null) this.next.onAxisEvent(value, nanos); + } + + /** + * @param value + * @param nanos + */ + private void onPovEvent(float value, long nanos) + { + this.handler.onPovEvent(this, value, nanos); + if (this.next != null) this.next.onPovEvent(value, nanos); + } + + /** + * @param pressed + */ + private void onButtonEvent(boolean pressed) + { + this.handler.onButtonEvent(this, pressed); + if (this.next != null) this.next.onButtonEvent(pressed); + } + + public Controller getController() + { + return this.controller; + } + + public Component getComponent() + { + return this.component; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/InputHandler.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/InputHandler.java new file mode 100644 index 00000000..fb2e0049 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/InputHandler.java @@ -0,0 +1,36 @@ +package com.mumfrey.liteloader.util; + +/** + * Interface for objects which handle JInput events + * + * @author Adam Mummery-Smith + */ +public interface InputHandler +{ + /** + * Called when an analogue (axis) event is raised on the specified component + * + * @param source + * @param nanos + * @param value + */ + void onAxisEvent(InputEvent source, float value, long nanos); + + /** + * Called when a POV (Point-Of-View) event is raised on the specified + * component. + * + * @param source + * @param value + * @param nanos + */ + void onPovEvent(InputEvent source, float value, long nanos); + + /** + * Called when a button event is raised on the specified component + * + * @param source + * @param value + */ + void onButtonEvent(InputEvent source, boolean value); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/ObfuscationUtilities.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/ObfuscationUtilities.java new file mode 100644 index 00000000..db74c709 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/ObfuscationUtilities.java @@ -0,0 +1,87 @@ +package com.mumfrey.liteloader.util; + +import net.minecraft.launchwrapper.IClassTransformer; +import net.minecraft.launchwrapper.Launch; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.BlockPos; + +import com.mumfrey.liteloader.core.runtime.Obf; + +public class ObfuscationUtilities +{ + /** + * True if FML is being used, in which case we use searge names instead of + * raw field/method names. + */ + private static boolean fmlDetected = false; + + private static boolean checkedObfEnv = false; + private static boolean seargeNames = false; + + static + { + // Check for FML + ObfuscationUtilities.fmlDetected = ObfuscationUtilities.fmlIsPresent(); + } + + public static boolean fmlIsPresent() + { + for (IClassTransformer transformer : Launch.classLoader.getTransformers()) + { + if (transformer.getClass().getName().contains("fml")) + { + return true; + } + } + + return false; + } + + public static boolean useSeargeNames() + { + if (!ObfuscationUtilities.checkedObfEnv) + { + ObfuscationUtilities.checkedObfEnv = true; + + try + { + MinecraftServer.class.getDeclaredField("serverRunning"); + } + catch (SecurityException ex) + { + } + catch (NoSuchFieldException ex) + { + ObfuscationUtilities.seargeNames = true; + } + } + + return ObfuscationUtilities.seargeNames; + } + + /** + * Abstraction helper function + * + * @param fieldName Name of field to get, returned unmodified if in debug + * mode + * @return Obfuscated field name if present + */ + public static String getObfuscatedFieldName(String fieldName, String obfuscatedFieldName, String seargeFieldName) + { + boolean deobfuscated = BlockPos.class.getSimpleName().equals("BlockPos"); + return deobfuscated ? (ObfuscationUtilities.useSeargeNames() ? seargeFieldName : fieldName) + : (ObfuscationUtilities.fmlDetected ? seargeFieldName : obfuscatedFieldName); + } + + /** + * Abstraction helper function + * + * @param obf Field to get, returned unmodified if in debug mode + * @return Obfuscated field name if present + */ + public static String getObfuscatedFieldName(Obf obf) + { + boolean deobfuscated = BlockPos.class.getSimpleName().equals("BlockPos"); + return deobfuscated ? (ObfuscationUtilities.useSeargeNames() ? obf.srg : obf.name) : (ObfuscationUtilities.fmlDetected ? obf.srg : obf.obf); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/Position.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/Position.java new file mode 100644 index 00000000..24f4c26c --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/Position.java @@ -0,0 +1,58 @@ +package com.mumfrey.liteloader.util; + +import net.minecraft.entity.Entity; +import net.minecraft.util.Vec3; + +/** + * A 3D vector position with rotation as well + * + * @author Adam Mummery-Smith + */ +public class Position extends Vec3 +{ + public final float yaw; + + public final float pitch; + + public Position(double x, double y, double z) + { + this(x, y, z, 0.0F, 0.0F); + } + + public Position(double x, double y, double z, float yaw, float pitch) + { + super(x, y, z); + + this.yaw = yaw; + this.pitch = pitch; + } + + public Position(Entity entity) + { + this(entity.posX, entity.posY, entity.posZ, entity.rotationYaw, entity.rotationPitch); + } + + public Position(Entity entity, boolean usePrevious) + { + this(usePrevious ? entity.prevPosX : entity.posX, + usePrevious ? entity.prevPosY : entity.posY, + usePrevious ? entity.prevPosZ : entity.posZ, + usePrevious ? entity.prevRotationYaw : entity.rotationYaw, + usePrevious ? entity.prevRotationPitch : entity.rotationPitch); + } + + public void applyTo(Entity entity) + { + entity.posX = this.xCoord; + entity.posY = this.yCoord; + entity.posZ = this.zCoord; + entity.rotationYaw = this.yaw; + entity.rotationPitch = this.pitch; + } + + @Override + public String toString() + { + return "(" + this.xCoord + ", " + this.yCoord + ", " + this.zCoord + ", " + this.yaw + ", " + this.pitch + ")"; + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/PrivateFields.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/PrivateFields.java new file mode 100644 index 00000000..c3108766 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/PrivateFields.java @@ -0,0 +1,125 @@ +package com.mumfrey.liteloader.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +import com.mumfrey.liteloader.core.runtime.Obf; + +/** + * Wrapper for obf/mcp reflection-accessed private fields, mainly added to + * centralise the locations I have to update the obfuscated field names. + * + * @author Adam Mummery-Smith + * + * @param

      Parent class type, the type of the class that owns the field + * @param Field type, the type of the field value + */ +public class PrivateFields +{ + /** + * Class to which this field belongs + */ + public final Class

      parentClass; + + /** + * Name used to access the field, determined at init + */ + private final String fieldName; + + private boolean errorReported = false; + + /** + * Creates a new private field entry + * + * @param obf + */ + protected PrivateFields(Class

      owner, Obf obf) + { + this.parentClass = owner; + this.fieldName = ObfuscationUtilities.getObfuscatedFieldName(obf); + } + + /** + * Get the current value of this field on the instance class supplied + * + * @param instance Class to get the value of + * @return field value or null if errors occur + */ + @SuppressWarnings("unchecked") + public T get(P instance) + { + try + { + Field field = this.parentClass.getDeclaredField(this.fieldName); + field.setAccessible(true); + return (T)field.get(instance); + } + catch (Exception ex) + { + if (!this.errorReported) + { + this.errorReported = true; + ex.printStackTrace(); + } + return null; + } + } + + /** + * Set the value of this field on the instance class supplied + * + * @param instance Object to set the value of the field on + * @param value value to set + * @return value + */ + public T set(P instance, T value) + { + try + { + Field field = this.parentClass.getDeclaredField(this.fieldName); + field.setAccessible(true); + field.set(instance, value); + } + catch (Exception ex) + { + if (!this.errorReported) + { + this.errorReported = true; + ex.printStackTrace(); + } + } + + return value; + } + + /** + * Set the value of this FINAL field on the instance class supplied + * + * @param instance Object to set the value of the field on + * @param value value to set + * @return value + */ + public T setFinal(P instance, T value) + { + try + { + Field modifiers = Field.class.getDeclaredField("modifiers"); + modifiers.setAccessible(true); + + Field field = this.parentClass.getDeclaredField(this.fieldName); + modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.setAccessible(true); + field.set(instance, value); + } + catch (Exception ex) + { + if (!this.errorReported) + { + this.errorReported = true; + ex.printStackTrace(); + } + } + + return value; + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/SortableValue.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/SortableValue.java new file mode 100644 index 00000000..00e2b1a9 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/SortableValue.java @@ -0,0 +1,46 @@ +package com.mumfrey.liteloader.util; + +/** + * Struct which contains a mapping of a priority value to another object, used + * for sorting items using the native functionality in TreeMap and TreeSet. + * + * @author Adam Mummery-Smith + */ +public class SortableValue implements Comparable> +{ + private final int priority; + + private final int order; + + private final T value; + + public SortableValue(int priority, int order, T value) + { + this.priority = priority; + this.order = order; + this.value = value; + } + + public int getPriority() + { + return this.priority; + } + + public int getOrder() + { + return this.order; + } + + public T getValue() + { + return this.value; + } + + @Override + public int compareTo(SortableValue other) + { + if (other == null) return 0; + if (other.priority == this.priority) return this.order - other.order; + return (this.priority - other.priority); + } +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/jinput/ComponentRegistry.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/jinput/ComponentRegistry.java new file mode 100644 index 00000000..bc31bd93 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/jinput/ComponentRegistry.java @@ -0,0 +1,285 @@ +package com.mumfrey.liteloader.util.jinput; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; + +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +import net.java.games.input.Component; +import net.java.games.input.Controller; +import net.java.games.input.ControllerEnvironment; + +/** + * Registry which keeps track of mappings of JInput descriptors to the + * controller and component references. + * + * @author Adam Mummery-Smith + */ +public class ComponentRegistry +{ + /** + * List of components + */ + private static HashMap components = new HashMap(); + + /** + * List of controllers + */ + private static HashMap controllers = new HashMap(); + + /** + * Constructor + */ + public ComponentRegistry() + { + } + + public void enumerate() + { + try + { + LiteLoaderLogger.info("JInput Component Registry is initialising..."); + this.enumerate(ControllerEnvironment.getDefaultEnvironment()); + LiteLoaderLogger.info("JInput Component Registry initialised, found %d controller(s) %d component(s)", + ControllerEnvironment.getDefaultEnvironment().getControllers().length, components.size()); + } + catch (Throwable th) + { + } + } + + public void enumerate(ControllerEnvironment environment) + { + components.clear(); + controllers.clear(); + + for (Controller controller : environment.getControllers()) + { + LiteLoaderLogger.info("Inspecting %s controller %s on %s...", controller.getType(), controller.getName(), controller.getPortType()); + for (Component component : controller.getComponents()) + { + this.addComponent(controller, component); + } + } + } + + private String addComponent(Controller controller, Component component) + { + String descriptor = ComponentRegistry.getDescriptor(controller, component); + components.put(descriptor, component); + controllers.put(descriptor, controller); + return descriptor; + } + + public ArrayList getComponents(String descriptor) + { + ArrayList components = new ArrayList(); + + int offset = 0; + Component component = null; + + do + { + component = this.getComponent(descriptor, offset++); + + if (components.contains(component)) + { + component = null; + } + + if (component != null) + { + components.add(component); + } + + } while (component != null && components.size() < 32); + + return components; + } + + public Component getComponent(String descriptor) + { + return this.getComponent(descriptor, 0); + } + + protected Component getComponent(String descriptor, int offset) + { + if (components.containsKey(descriptor)) + { + return components.get(descriptor); + } + + for (Entry entry : components.entrySet()) + { + if (matches(entry.getKey(), descriptor)) + { + if (--offset < 0) + { + return entry.getValue(); + } + } + } + + return null; + } + + public ArrayList getControllers(String descriptor) + { + ArrayList controllers = new ArrayList(); + + int offset = 0; + Controller controller = null; + + do + { + controller = this.getController(descriptor, offset++); + + if (controllers.contains(controller)) + { + controller = null; + } + + if (controller != null) + { + controllers.add(controller); + } + + } while (controller != null && controllers.size() < 32); + + return controllers; + } + + public Controller getController(String descriptor) + { + return this.getController(descriptor, 0); + } + + protected Controller getController(String descriptor, int offset) + { + if (controllers.containsKey(descriptor)) + { + return controllers.get(descriptor); + } + + for (Entry entry : controllers.entrySet()) + { + if (matches(entry.getKey(), descriptor)) + { + if (--offset < 0) + { + return entry.getValue(); + } + } + } + + return null; + } + + public static String getDescriptor(Controller controller, Component component) + { + int index = 0; + String controllerPath = ComponentRegistry.getControllerPath(controller); + String componentId = component.getIdentifier().getName(); + + String descriptor = ComponentRegistry.getDescriptor(controllerPath, componentId, index); + + while (components.containsKey(descriptor) && components.get(descriptor) != component) + { + descriptor = ComponentRegistry.getDescriptor(controllerPath, componentId, ++index); + } + + return descriptor; + } + + /** + * @param type + * @param name + * @param portType + * @param portNumber + * @param component + * @param index + */ + public static String getDescriptor(String type, String name, String portType, int portNumber, String component, int index) + { + String controllerPath = ComponentRegistry.getControllerPath(type, name, portType, portNumber); + return ComponentRegistry.getDescriptor(controllerPath, component, index); + } + + /** + * @param controller + * @param component + * @param index + */ + private static String getDescriptor(String controller, String component, int index) + { + String descriptor = String.format("jinput:%s/%s/%d", controller, ComponentRegistry.format(component), index); + return descriptor; + } + + /** + * @param controller + */ + private static String getControllerPath(Controller controller) + { + return ComponentRegistry.getControllerPath( + controller.getType().toString().toLowerCase(), + controller.getName(), + controller.getPortType().toString(), + controller.getPortNumber() + ); + } + + /** + * @param type + * @param name + * @param portType + * @param portNumber + */ + public static String getControllerPath(String type, String name, String portType, int portNumber) + { + return String.format("%s/%s/%s/%d", + ComponentRegistry.format(type), + ComponentRegistry.format(name), + ComponentRegistry.format(portType), + portNumber + ); + } + + public static boolean matches(String descriptor, String wildDescriptor) + { + String[] descriptorParts = ComponentRegistry.splitDescriptor(descriptor.trim()); + String[] wildDescriptorParts = ComponentRegistry.splitDescriptor(wildDescriptor.trim()); + + if (descriptorParts.length != wildDescriptorParts.length) return false; + + for (int i = 0; i < descriptorParts.length; i++) + { + if (wildDescriptorParts[i].length() > 0 && descriptorParts[i].length() > 0 + && !wildDescriptorParts[i].equals(descriptorParts[i]) + && !wildDescriptorParts[i].equals("*")) + { + return false; + } + } + + return true; + } + + public static String[] splitDescriptor(String descriptor) + { + if (descriptor.startsWith("jinput:")) + { + String[] path = descriptor.split("(? logTail = new LinkedList(); + + private static long logIndex = 0; + + private static Throwable lastThrowable; + + /** + * Provides some wiggle-room within log4j's Level so we can have different + * levels of logging on the same, um.. Level + */ + public static enum Verbosity + { + VERBOSE(3), + NORMAL(2), + REDUCED(1), + SILENT(0); + + protected final int level; + + private Verbosity(int level) + { + this.level = level; + } + + public int getLevel() + { + return this.level; + } + } + + public static Verbosity verbosity = LiteLoaderLogger.DEBUG ? Verbosity.VERBOSE : Verbosity.NORMAL; + + static + { + LiteLoaderLogger.logger.addAppender(new LiteLoaderLogger()); + } + + protected LiteLoaderLogger() + { + super("Internal Log Appender", null, null); + this.start(); + } + + @Override + public void append(LogEvent event) + { + synchronized (LiteLoaderLogger.logTail) + { + LiteLoaderLogger.logIndex++; + this.append(event.getMillis(), event.getMessage().getFormattedMessage()); + Throwable thrown = event.getThrown(); + if (thrown != null) + { + this.append(event.getMillis(), String.format("\2474%s: \2476%s", thrown.getClass().getSimpleName(), thrown.getMessage())); + } + } + } + + /** + * @param message + */ + private void append(long timestamp, String message) + { + String date = new java.text.SimpleDateFormat("[HH:mm:ss] ").format(new Date(timestamp)); + + while (message.indexOf('\n') > -1) + { + int lineFeedPos = message.indexOf('\n'); + this.appendLine(date + message.substring(0, lineFeedPos)); + message = message.substring(lineFeedPos + 1); + } + + this.appendLine(date + message); + } + + /** + * @param line + */ + private void appendLine(String line) + { + LiteLoaderLogger.logTail.add(line); + + if (LiteLoaderLogger.logTail.size() > LiteLoaderLogger.LOG_TAIL_SIZE) + { + LiteLoaderLogger.logTail.remove(); + } + } + + public static long getLogIndex() + { + return LiteLoaderLogger.logIndex; + } + + public static List getLogTail() + { + List log = new ArrayList(); + + synchronized (LiteLoaderLogger.logTail) + { + log.addAll(LiteLoaderLogger.logTail); + } + + return log; + } + + public static Logger getLogger() + { + return LiteLoaderLogger.logger; + } + + public static void clearLastThrowable() + { + LiteLoaderLogger.lastThrowable = null; + } + + public static Throwable getLastThrowable() + { + Throwable lastThrowableWrapped = null; + + // Wrap the throwable to avoid loader constraint violations during PREINIT and INIT + if (LiteLoaderLogger.lastThrowable != null) + { + StringWriter sw = new StringWriter(); + LiteLoaderLogger.lastThrowable.printStackTrace(new PrintWriter(sw)); + lastThrowableWrapped = new Throwable(sw.toString()); + try + { + sw.close(); + } + catch (IOException ex) + { + // oh well + } + } + + return lastThrowableWrapped; + } + + private static void log(Level level, Verbosity verbosity, String format, Object... data) + { + if (verbosity.level > LiteLoaderLogger.verbosity.level) + { + return; + } + + try + { + LiteLoaderLogger.logger.log(level, String.format(format, data)); + } + catch (MissingFormatArgumentException ex) + { + LiteLoaderLogger.logger.log(level, format.replace('%', '@')); + } + } + + private static void log(Level level, Verbosity verbosity, Throwable th, String format, Object... data) + { + if (verbosity.level > LiteLoaderLogger.verbosity.level) + { + return; + } + + LiteLoaderLogger.lastThrowable = th; + + try + { + LiteLoaderLogger.logger.log(level, String.format(format, data), th); + } + catch (LinkageError ex) // This happens because of ClassLoader scope derpiness during the PREINIT and INIT phases + { + th.printStackTrace(); + } + catch (Throwable th2) + { + th2.initCause(th); + th2.printStackTrace(); + } + } + + public static void severe(String format, Object... data) + { + LiteLoaderLogger.severe(Verbosity.REDUCED, format, data); + } + + public static void severe(Verbosity verbosity, String format, Object... data) + { + LiteLoaderLogger.log(Level.ERROR, verbosity, format, data); + } + + public static void severe(Throwable th, String format, Object... data) + { + LiteLoaderLogger.severe(Verbosity.REDUCED, th, format, data); + } + + public static void severe(Verbosity verbosity, Throwable th, String format, Object... data) + { + LiteLoaderLogger.lastThrowable = th; + + try + { + LiteLoaderLogger.log(Level.ERROR, verbosity, th, format, data); + } + catch (LinkageError ex) // This happens because of ClassLoader scope derpiness during the PREINIT and INIT phases + { + th.printStackTrace(); + } + catch (Throwable th2) + { + th2.initCause(th); + th2.printStackTrace(); + } + } + + public static void warning(String format, Object... data) + { + LiteLoaderLogger.warning(Verbosity.REDUCED, format, data); + } + + public static void warning(Verbosity verbosity, String format, Object... data) + { + LiteLoaderLogger.log(Level.WARN, verbosity, format, data); + } + + public static void warning(Throwable th, String format, Object... data) + { + LiteLoaderLogger.warning(Verbosity.REDUCED, th, format, data); + } + + public static void warning(Verbosity verbosity, Throwable th, String format, Object... data) + { + LiteLoaderLogger.lastThrowable = th; + + try + { + LiteLoaderLogger.log(Level.WARN, verbosity, th, format, data); + } + catch (LinkageError ex) // This happens because of ClassLoader scope derpiness during the PREINIT and INIT phases + { + th.printStackTrace(); + } + catch (Throwable th2) + { + th2.initCause(th); + th2.printStackTrace(); + } + } + + public static void info(String format, Object... data) + { + LiteLoaderLogger.info(Verbosity.NORMAL, format, data); + } + + public static void info(Verbosity verbosity, String format, Object... data) + { + LiteLoaderLogger.log(Level.INFO, verbosity, format, data); + } + + public static void debug(String format, Object... data) + { + if (LiteLoaderLogger.DEBUG) + { + System.err.print("[DEBUG] "); + System.err.println(String.format(format, data)); + } + } + + public static void debug(Throwable th, String format, Object... data) + { + if (LiteLoaderLogger.DEBUG) + { + th.printStackTrace(); + LiteLoaderLogger.debug(format, data); + } + } + + public static void debug(Throwable th) + { + if (LiteLoaderLogger.DEBUG) + { + th.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/net/HttpStringRetriever.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/net/HttpStringRetriever.java new file mode 100644 index 00000000..beeffb5a --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/net/HttpStringRetriever.java @@ -0,0 +1,232 @@ +package com.mumfrey.liteloader.util.net; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.io.IOUtils; + +/** + * Utility class to fetch the contents of a URL into a string + * + * @author Adam Mummery-Smith + */ +public class HttpStringRetriever extends Thread +{ + public static final String LINE_ENDING_LF = "\n"; + public static final String LINE_ENDING_CR = "\r"; + public static final String LINE_ENDING_CRLF = "\r\n"; + + /** + * URL to connect to + */ + private final String url; + + /** + * Additional headers to pass, if required + */ + private final Map headers; + + private final String lineEnding; + + /** + * Response code returned from the remote server (if any) + */ + private int httpResponseCode = 0; + + private final Object resultLock = new Object(); + + /** + * String retrieved from the remote server + */ + private String string; + + /** + * True when this retriever is complete, even if retrieval failed + */ + private volatile boolean done = false; + + /** + * True if the fetch operation was a success + */ + private volatile boolean success = false; + + /** + * Create a new string retriever for the specified URL, with the supplied + * headers and line end characters. + * + * @param url URL to download from + * @param headers Additional headers to add to the request + * @param lineEnding Line ending to use + */ + public HttpStringRetriever(String url, Map headers, String lineEnding) + { + this.url = url; + this.headers = headers; + this.lineEnding = lineEnding; + } + + /** + * Create a new string retriever for the specified URL, with the supplied + * headers. + * + * @param url URL to download from + * @param headers Additional headers to add to the request + */ + public HttpStringRetriever(String url, Map headers) + { + this(url, headers, HttpStringRetriever.LINE_ENDING_LF); + } + + /** + * Create a new string retriever for the specified URL + * + * @param url URL to download from + */ + public HttpStringRetriever(String url) + { + this(url, null); + } + + /** + * Create a new string retriever to be used synchronously + */ + public HttpStringRetriever() + { + this(null, null); + } + + /** + * Get the string which was retrieved + */ + public String getString() + { + synchronized (this.resultLock) + { + return this.string; + } + } + + /** + * True if the download is complete, even if it failed + */ + public boolean isDone() + { + return this.done; + } + + /** + * True if the download completed successfully + */ + public boolean getSuccess() + { + return this.success; + } + + /** + * Get the response code from the HTTP request, -1 if a connection error + * occurred. + */ + public int getHttpResponseCode() + { + return this.httpResponseCode; + } + + /* (non-Javadoc) + * @see java.lang.Thread#run() + */ + @Override + public void run() + { + try + { + String result = this.fetch(new URL(this.url)); + + synchronized (this.resultLock) + { + this.string = result; + } + } + catch (MalformedURLException ex) + { + ex.printStackTrace(); + } + + this.done = true; + } + + /** + * Fetch a String in the current thread, normally this method is called by + * the run() method to fetch the resource in a new thread but can be called + * directly to fetch the result in the current thread. + * + * @param url URL to fetch + * @return retrieved string or empty string on failure + */ + public String fetch(URL url) + { + StringBuilder readString = new StringBuilder(); + HttpURLConnection httpClient = null; + + try + { + // Open a HTTP connection to the URL + httpClient = (HttpURLConnection)url.openConnection(); + httpClient.setDoInput(true); + httpClient.setUseCaches(false); + + httpClient.setRequestMethod("GET"); + httpClient.setRequestProperty("Connection", "Close"); + httpClient.addRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:19.0) Gecko/20100101 Firefox/21.0"); // For CloudFlare + + if (this.headers != null) + { + for (Entry header : this.headers.entrySet()) + httpClient.addRequestProperty(header.getKey(), header.getValue()); + } + + this.httpResponseCode = httpClient.getResponseCode(); + if (this.httpResponseCode >= 200 && this.httpResponseCode < 300) + { + InputStream httpStream = httpClient.getInputStream(); + BufferedReader reader = null; + try + { + reader = new BufferedReader(new InputStreamReader(httpStream)); + + String readLine; + while ((readLine = reader.readLine()) != null) + { + readString.append(readLine).append(this.lineEnding); + } + + this.success = true; + } + catch (IOException ex) + { + } + finally + { + IOUtils.closeQuietly(reader); + IOUtils.closeQuietly(httpStream); + } + } + } + catch (IOException ex) + { + this.httpResponseCode = -1; + } + finally + { + if (httpClient != null) httpClient.disconnect(); + } + + return readString.toString(); + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/net/LiteLoaderLogUpload.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/net/LiteLoaderLogUpload.java new file mode 100644 index 00000000..187eea3e --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/net/LiteLoaderLogUpload.java @@ -0,0 +1,126 @@ +package com.mumfrey.liteloader.util.net; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import com.mumfrey.liteloader.core.LiteLoader; +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Upload manager for posting logs to liteloader.com + * + * @author Adam Mummery-Smith + */ +public class LiteLoaderLogUpload extends Thread +{ + private static final String POST_URL = "http://logs.liteloader.com/post"; + + private static final String LITELOADER_KEY = "liteloader0cea4593b6a51e7c"; + + private final String encodedData; + + private volatile boolean completed; + + private String response = "Unknown Error"; + + public LiteLoaderLogUpload(String nick, String uuid, String content) + { + Map data = new HashMap(); + + data.put("nick", nick); + data.put("uuid", uuid); + data.put("token", LITELOADER_KEY); + data.put("version", LiteLoader.getVersion()); + data.put("brand", "" + LiteLoader.getBranding()); + data.put("log", content); + + StringBuilder sb = new StringBuilder(); + + try + { + String separator = ""; + for (Entry postValue : data.entrySet()) + { + sb.append(separator).append(postValue.getKey()).append("=").append(URLEncoder.encode(postValue.getValue(), "UTF-8")); + separator = "&"; + } + } + catch (UnsupportedEncodingException ex) + { + ex.printStackTrace(); + } + + this.encodedData = sb.toString(); + } + + public boolean isCompleted() + { + return this.completed; + } + + public String getLogUrl() + { + return this.response; + } + + @Override + public void run() + { + try + { + URL url = new URL(POST_URL); + HttpURLConnection httpClient = (HttpURLConnection)url.openConnection(); + httpClient.setConnectTimeout(5000); + httpClient.setReadTimeout(10000); + httpClient.addRequestProperty("Content-type", "application/x-www-form-urlencoded"); + httpClient.addRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)"); // For not fail++ + httpClient.setRequestMethod("POST"); + httpClient.setDoOutput(true); + + DataOutputStream outputStream = new DataOutputStream(httpClient.getOutputStream()); + outputStream.writeBytes(this.encodedData); + outputStream.flush(); + + InputStream httpStream = httpClient.getInputStream(); + + try + { + StringBuilder readString = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(httpStream)); + + String readLine; + while ((readLine = reader.readLine()) != null) + { + readString.append(readLine).append("\n"); + } + + reader.close(); + this.response = readString.toString(); + } + catch (IOException ex) + { + ex.printStackTrace(); + } + + httpStream.close(); + outputStream.close(); + } + catch (Exception ex) + { + this.response = ex.getMessage(); + LiteLoaderLogger.warning("Error posting log to liteloader.com: %s: %s", ex.getClass(), ex.getMessage()); + } + + this.completed = true; + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/net/PastebinUpload.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/net/PastebinUpload.java new file mode 100644 index 00000000..ccf099db --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/net/PastebinUpload.java @@ -0,0 +1,135 @@ +package com.mumfrey.liteloader.util.net; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import com.mumfrey.liteloader.util.log.LiteLoaderLogger; + +/** + * Thing for doing the whole pastebin malarkey + * + * @author Adam Mummery-Smith + */ +public class PastebinUpload extends Thread +{ + public static int PUBLIC = 0; + public static int UNLISTED = 1; + public static int PRIVATE = 2; + + private static final String PASTEBIN_API_URL = "http://pastebin.com/api/api_post.php"; + + private static final String PASTEBIN_API_KEY = "2eda2d0840d2ab7e1ed036e3e810bfda"; + + private final String encodedData; + + private volatile boolean completed; + + private String pasteUrl = null; + + public PastebinUpload(String author, String pasteName, String content, int privacy) + { + Map data = new HashMap(); + + data.put("api_option", "paste"); + data.put("api_user_key", ""); + data.put("api_paste_private", String.valueOf(privacy)); + data.put("api_paste_name", pasteName); + data.put("api_paste_expire_date", "N"); + data.put("api_paste_format", "text"); + data.put("api_dev_key", PASTEBIN_API_KEY); + data.put("api_paste_code", content); + + StringBuilder sb = new StringBuilder(); + + try + { + String separator = ""; + for (Entry postValue : data.entrySet()) + { + sb.append(separator).append(postValue.getKey()).append("=").append(URLEncoder.encode(postValue.getValue(), "UTF-8")); + separator = "&"; + } + } + catch (UnsupportedEncodingException ex) + { + ex.printStackTrace(); + } + + this.encodedData = sb.toString(); + } + + public boolean isCompleted() + { + return this.completed; + } + + public String getPasteUrl() + { + return this.pasteUrl; + } + + @Override + public void run() + { + try + { + URL url = new URL(PASTEBIN_API_URL); + HttpURLConnection httpClient = (HttpURLConnection)url.openConnection(); + httpClient.setConnectTimeout(5000); + httpClient.setReadTimeout(10000); + httpClient.addRequestProperty("Content-type", "application/x-www-form-urlencoded"); + httpClient.addRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)"); // For not fail++ + httpClient.setRequestMethod("POST"); + httpClient.setDoOutput(true); + + DataOutputStream outputStream = new DataOutputStream(httpClient.getOutputStream()); + outputStream.writeBytes(this.encodedData); + outputStream.flush(); + + int responseCode = httpClient.getResponseCode(); + if (responseCode / 100 == 2) + { + InputStream httpStream = httpClient.getInputStream(); + + try + { + StringBuilder readString = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(httpStream)); + + String readLine; + while ((readLine = reader.readLine()) != null) + { + readString.append(readLine).append("\n"); + } + + reader.close(); + this.pasteUrl = readString.toString(); + } + catch (IOException ex) + { + ex.printStackTrace(); + } + + httpStream.close(); + } + + outputStream.close(); + } + catch (Exception ex) + { + LiteLoaderLogger.warning("Error posting log to pastebin: %s: %s", ex.getClass(), ex.getMessage()); + } + + this.completed = true; + } +} \ No newline at end of file diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/render/Icon.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/render/Icon.java new file mode 100644 index 00000000..38fa57ec --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/render/Icon.java @@ -0,0 +1,14 @@ +package com.mumfrey.liteloader.util.render; + +public interface Icon +{ + public abstract int getIconWidth(); + public abstract int getIconHeight(); + public abstract float getMinU(); + public abstract float getMaxU(); + public abstract float getInterpolatedU(double slice); + public abstract float getMinV(); + public abstract float getMaxV(); + public abstract float getInterpolatedV(double slice); + public abstract String getIconName(); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/render/IconClickable.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/render/IconClickable.java new file mode 100644 index 00000000..3a2609b5 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/render/IconClickable.java @@ -0,0 +1,16 @@ +package com.mumfrey.liteloader.util.render; + +/** + * Icon with an onClicked handler + * + * @author Adam Mummery-Smith + */ +public interface IconClickable extends IconTextured +{ + /** + * @param source Source of the event, usually the outermost gui screen + * @param container Container of this icon, the actual component hosting the + * icon + */ + public void onClicked(Object source, Object container); +} diff --git a/liteloader/src/main/java/com/mumfrey/liteloader/util/render/IconTextured.java b/liteloader/src/main/java/com/mumfrey/liteloader/util/render/IconTextured.java new file mode 100644 index 00000000..e7fb57c2 --- /dev/null +++ b/liteloader/src/main/java/com/mumfrey/liteloader/util/render/IconTextured.java @@ -0,0 +1,31 @@ +package com.mumfrey.liteloader.util.render; + +import net.minecraft.util.ResourceLocation; + +/** + * Icon with a texture and tooltip allocated to it + * + * @author Adam Mummery-Smith + */ +public interface IconTextured extends Icon +{ + /** + * Get tooltip text, return null for no tooltip + */ + public abstract String getDisplayText(); + + /** + * Get the texture resource for this icon + */ + public abstract ResourceLocation getTextureResource(); + + /** + * Get the U coordinate on the texture for this icon + */ + public abstract int getUPos(); + + /** + * Get the V coordinate on the texture for this icon + */ + public abstract int getVPos(); +} diff --git a/liteloader/src/main/java/net/eq2online/permissions/ReplicatedPermissionsContainer.java b/liteloader/src/main/java/net/eq2online/permissions/ReplicatedPermissionsContainer.java new file mode 100644 index 00000000..b86beb04 --- /dev/null +++ b/liteloader/src/main/java/net/eq2online/permissions/ReplicatedPermissionsContainer.java @@ -0,0 +1,129 @@ +package net.eq2online.permissions; + +import java.io.*; +import java.util.Collection; +import java.util.Set; +import java.util.TreeSet; + +import net.minecraft.network.PacketBuffer; + +/** + * Serializable container object + * + * @author Adam Mummery-Smith + */ +public class ReplicatedPermissionsContainer implements Serializable +{ + /** + * Serial version UID to suppoer Serializable interface + */ + private static final long serialVersionUID = -764940324881984960L; + + /** + * Mod name + */ + public String modName = "all"; + + /** + * Mod version + */ + public Float modVersion = 0.0F; + + /** + * List of permissions to replicate, prepend "-" for a negated permission + * and "+" for a granted permission. + */ + public Set permissions = new TreeSet(); + + /** + * Amount of time in seconds that the client will trust these permissions + * for before requesting an update. + */ + public long remoteCacheTimeSeconds = 600L; // 10 minutes + + public static final String CHANNEL = "PERMISSIONSREPL"; + + public ReplicatedPermissionsContainer() + { + } + + public ReplicatedPermissionsContainer(String modName, Float modVersion, Collection permissions) + { + this.modName = modName; + this.modVersion = modVersion; + this.permissions.addAll(permissions); + } + + /** + * Add all of the listed permissions to this container + * + * @param permissions + */ + public void addAll(Collection permissions) + { + this.permissions.addAll(permissions); + } + + /** + * Check and correct + */ + public void sanitise() + { + if (this.modName == null || this.modName.length() < 1) this.modName = "all"; + if (this.modVersion == null || this.modVersion < 0.0F) this.modVersion = 0.0F; + if (this.remoteCacheTimeSeconds < 0) this.remoteCacheTimeSeconds = 600L; + } + + /** + * Serialise this container to a byte array for transmission to a remote + * host. + */ + public byte[] getBytes() + { + try + { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + new ObjectOutputStream(byteStream).writeObject(this); + return byteStream.toByteArray(); + } + catch (IOException e) {} + + return new byte[0]; + } + + /** + * Deserialises a replicated permissions container from a byte array + * + * @param data Byte array containing the serialised data + * @return new container or null if deserialisation failed + */ + public static ReplicatedPermissionsContainer fromPacketBuffer(PacketBuffer data) + { + try + { + int readableBytes = data.readableBytes(); + if (readableBytes == 0) return null; + + byte[] payload = new byte[readableBytes]; + data.readBytes(payload); + + ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(payload)); + ReplicatedPermissionsContainer object = (ReplicatedPermissionsContainer)inputStream.readObject(); + return object; + } + catch (IOException e) + { + // Don't care + } + catch (ClassNotFoundException e) + { + // Don't care + } + catch (ClassCastException e) + { + // Don't care + } + + return null; + } +} diff --git a/liteloader/src/main/resources/assets/liteloader/lang/en_US.lang b/liteloader/src/main/resources/assets/liteloader/lang/en_US.lang new file mode 100644 index 00000000..4d1e45c5 --- /dev/null +++ b/liteloader/src/main/resources/assets/liteloader/lang/en_US.lang @@ -0,0 +1,89 @@ +# This file MUST be saved as UTF-8 + +key.categories.litemods=§eMod Keys + +gui.about.modsloaded=%d mod(s) loaded +gui.about.versiontext=Version %s +gui.about.poweredbyversion=v%s§r powered by LiteLoader v%s + +gui.about.checkupdates=§nCheck for updates + +gui.settings.showtab.label=Show LiteLoader Panel Tab +gui.settings.showtab.help1=If you disable this option, use CTRL+SHIFT+TAB +gui.settings.showtab.help2=to open the LiteLoader panel +gui.settings.notabhide.label=LiteLoader Tab Always Expanded +gui.settings.notabhide.help1=Only applies if the above option is also checked +gui.settings.forceupdate.label=Periodically Check For Updates +gui.settings.forceupdate.help1=This option is §cexperimental§r and may not work properly +gui.settings.forceupdate.help2=yet, it also enables the "force update" capability. + +gui.about.taboptions=§nLiteLoader Panel Options + +gui.about.authors=Authors + +gui.disablemod=Disable mod +gui.enablemod=Enable mod +gui.modsettings=Settings... + +gui.unknown=Unknown + +gui.status.loaded=Loaded +gui.status.active=Active +gui.status.disabled=Disabled +gui.status.pending.enabled=Enabled on next startup +gui.status.pending.disabled=Disabled on next startup +gui.status.missingdeps=Missing dependencies +gui.status.missingapis=Missing required APIs +gui.status.startuperror=Startup errors detected +gui.description.missingdeps=Missing dependencies: %s +gui.description.missingapis=Missing APIs: %s + +gui.mod.providestweak=Tweak +gui.mod.providestransformer=Transformer +gui.mod.providesevents=Injector +gui.mod.providesmixins=Mixins +gui.mod.usingapi=Uses additional API +gui.mod.startuperror=%d startup error(s) + +gui.mod.help.tweak=A tweaker is a special type of mod. Tweakers have almost unlimited control of the game and are usually mods which alter the game engine fundamentally, for example APIs or performance-enhancement mods like Optifine. Because they have the greatest amount of control, they are also the most likely to cause instability when not compatible with each other, if you are experiencing crashes or serious problems, you should always try disabling tweak mods first. +gui.mod.help.transformer=A transformer mod uses bytecode injection to hook extra functionality within the game which LiteLoader does not normally provide. Because transformers access functionality outside of LiteLoader's core, it is possible for them to have unforseen side-effects when combined with other mods. If you are experiencing crashes or other unexpected behaviour, you should disable transformer mods before regular mods to determine whether they are the source of the issue. Forge refers to this type of mod as a §lCoreMod +gui.mod.help.events=An injector mod uses a restricted form of bytecode injection which makes it somewhat safer than a full transformer mod, but can still cause instability under some circumstances. If you are experiencing crashes or other issues, then you should disable injector mods before regular mods to determine whether they are the source of the issue. +gui.mod.help.mixins=A mod with mixins uses a safe form of bytecode injection to hook into Minecraft + +gui.settings.title=%s Settings +gui.saveandclose=Save & Close + +gui.updates.title=Check for updates for %s +gui.updates.status.idle=§7No update check in progress +gui.updates.status.checking=Contacting update site... %s +gui.updates.status.success=§asuccess! +gui.updates.status.failed=§cfailed! + +gui.updates.available.title=§nAvailable Update Info +gui.updates.available.nonewversion=§7No newer version available +gui.updates.available.newversion=§aNew version available +gui.updates.available.version=Version: §a%s +gui.updates.available.date=Release date: §a%s + +gui.updates.forced=Update forced, restart the game to apply update + +gui.checknow=Check now +gui.installupdate=Install now +gui.downloadupdate=Download now +gui.forceupdate=Force update +gui.exitgame=Exit Game + +gui.log.button=LiteLoader Log +gui.log.title=LiteLoader Log Viewer +gui.log.scalecheckbox=Use native resolution +gui.log.postlog=Upload Log +gui.log.uploading=Uploading log please wait... +gui.log.uploadfailed=Upload failed +gui.log.uploadsuccess=Upload succeeded, log available at +gui.log.closedialog=Close + +gui.error.title=Startup errors for %s + +gui.error.tooltip=%d mod startup error(s) detected (%d critical) + +gui.notifications.updateavailable=LiteLoader Update Available! \ No newline at end of file diff --git a/liteloader/src/main/resources/assets/liteloader/lang/pt_BR.lang b/liteloader/src/main/resources/assets/liteloader/lang/pt_BR.lang new file mode 100644 index 00000000..ddb90a75 --- /dev/null +++ b/liteloader/src/main/resources/assets/liteloader/lang/pt_BR.lang @@ -0,0 +1,67 @@ +# This file MUST be saved as UTF-8 + +key.categories.litemods=§eTecla de Mods + +gui.about.modsloaded=%d mod(s) carregados +gui.about.versiontext=Versão %s +gui.about.poweredbyversion=v%s§r powered by LiteLoader v%s + +gui.about.checkupdates=§nVerificar atualizações + +gui.settings.showtab.label=Mostrar aba do Painel do Liteloader +gui.settings.showtab.help1=Se você desabilitar essa opção, use CTRL + SHIFT + TAB +gui.settings.showtab.help2=para abrir o painel LiteLoader +gui.settings.notabhide.label=Aba de LiteLoader Sempre Expandida +gui.settings.notabhide.help1=Só se aplica se a opção acima também é verificada + +gui.about.taboptions=§nPainel de Opções do LiteLoader + +gui.about.authors=Autores + +gui.disablemod=Desativar Mod +gui.enablemod=Ativar Mod +gui.modsettings=Configurações... + +gui.unknown=Desconhecido + +gui.status.loaded=Carregado +gui.status.active=Ativo +gui.status.disabled=Desativado +gui.status.pending.enabled=Ativado na próxima inicialização +gui.status.pending.disabled=Desativado na próxima inicialização +gui.status.missingdeps=dependências não existentes +gui.status.missingapis=Faltando APIs necessárias +gui.description.missingdeps=dependências não existentes: %s +gui.description.missingapis=APIs que faltam: %s + +gui.mod.providestweak=oferece o Tweak +gui.mod.providestransformer=oferece do transformador +gui.mod.usingapi=Utilizar API adicional + +gui.settings.title=%s Configurações +gui.saveandclose=Salvar & Fechar + +gui.updates.title=Verificar se há atualizações para %s +gui.updates.status.idle=§7Nenhuma verificação de atualização em andamento +gui.updates.status.checking=Contacting update site... %s +gui.updates.status.success=§asucesso! +gui.updates.status.failed=§cfalhou! + +gui.updates.available.title=§nAtualização de informações disponível +gui.updates.available.nonewversion=§7Nenhuma versão recente disponível +gui.updates.available.newversion=§aNova versão disponível +gui.updates.available.version=Versão: §a%s +gui.updates.available.date=Data de lançamento: §a%s + +gui.checknow=Verificar agora +gui.installupdate=Instalar agora +gui.downloadupdate=Baixar agora + +gui.log.button=LiteLoader Log +gui.log.title=Visualizador de Logs do LiteLoader +gui.log.scalecheckbox=Usar resolução nativa +gui.log.postlog=Enviar Log +gui.log.uploading=Enviando log para o Pastebin... +gui.log.uploadfailed=Falha ao enviar +gui.log.uploadsuccess=Enviado com sucesso, log disponível em +gui.log.closedialog=Fechar \ No newline at end of file diff --git a/liteloader/src/main/resources/assets/liteloader/textures/gui/about.png b/liteloader/src/main/resources/assets/liteloader/textures/gui/about.png new file mode 100644 index 0000000000000000000000000000000000000000..eee2a893a87dc078e750f78f0423b0a4e6e27ab6 GIT binary patch literal 43567 zcmbTdWmH_jvoE>_cMlMBa1vYt6WrY`KyZRP!DVpQKyZfu!QFy8!9BQ3@ZkRDf9^T= zy$^TY4{z39Ys-{$cUAZ9s`~8+6(wm*G!irb05D}`B-8)^1iJ(QC`hoAzH`YJ*y*jS zq>ih)!xvX~V`pRY+^kx?tz1sQAAH+q?YtK*5HL&BNG{jf0h)&Cc#W z{rWFz7gsg&|F;?cM`{-hPe*e$HFFn-ug<10dwik!A7jq@Z!4CXiqYJV6KWXRqz$5Oy&Hn$4 z7#plHZ2xhM|BtKu&m)-s{pa$3J3j2@e@`8Ad)Rz&hE0qgmi^oS@WWVELR7G(4; zbar-LX!z<++DVAr6_&hadN|tqa1&`s$@My>MhoeycZ^$ALOH7zlFZ?`iy+aYA8_y6 z+1i-(7T4S#6#lKdFrL+!R zW@T?L6tvCvy)JR;_hUxvNv#>fI-4t%?z+W8?DZ5ut`h%$2%@qtBxAyZ!zB;u@#YsN71d3`6;X ze-P~O{G@ihwL}jNwp<|%V&3u%LJWU6dw4T%EX}6Vh(zNac>&guRvkZF*2i`YJ_*< zxS%cu5N+vX4#5jPCSc&dt$5+`zHI6SE_7_4uPWo#$zH4A0un8EDX{=a%G&p#&ShLP9%s8-6wQ z@}cX<87P=bII43TtoYE-bd!=U52E^6S@~mJ@F~B(zTWI$GOuWp7>WQi`##>p&`1wHq{P_0}rmbdO1HTD)ZM8OHNgRaB5m1p8rw zKI&Op|4qEQx;k?r=QJDQ&6#LwC@eHfG}=F+z3~HpaPWHI{_Vlkc2wo(Hb2j0yQbac zlbeNgpUW|V&Bd;VDQU_goVrc_ohFk0^|ySE8*a`oZq8ZgyMrIeVh7JV!Ua1Oh9^YE z`Gb7cCA1L8>`YC!X?9$SJp9|et{*a=VD`7JolmfMO9Nl;xZ&Zg83qGEUV&3dNy_#)nYghTr+==B2}@KbO&Q9?(N;VH0w3X3{g zvHrrfRduIrB{MU#>h>Whw_$P@R92{5p*oa~-;JXS_|VBs3TrnA<=0Xe{>ri z8XternGUrrR?6z2g=>fFJe$nxed94+{;5t41!vm=Y`fL1S|D|jtoG2{+>ETMw|!e6 z9oHdwr^H|kd5`H9I71T5hhJES zR?3E+yUEMrg6=+V2VLCAfAhH=ueJRvdKEBa_zGFQn>b3l-5cLlX5ZAlLJWudWw+d@ zTG*~5_AHGvmd1o%4GuGqmzU>a^tdJ^A@O5?^Tz_k`rQf1=*8i69{s7?mqX3M_ShNb zXHrvFf5c-art)Ls1mM>4QNh<+b@{z&&i>IOTqESsj)^xpUBKk*3W`D8a&AK(BC7BS z9dkxv!gpS$+3-z#*<$$oib*Ymim71oD%`-{PYfo=s1dLDio)rW`^(|a0z#vn`Opha zOmrMFwV=erM8O>|=nF`Ije`R>0JucreZTncO27nSOAA2vuae=Rj-RJ8*p|-9qp5=V zZp{E(dwx^|20XwrqJ82VLNi4#0^#8!!?K18Hxl6{#rZ3zZG2JB*A6~3%M3=GoL~U( zRpbB@8ylM{StTVu&B=Pjv9U4X#2bi2qe0eW}m0VXGd^#M#r7XpF* z%&M&tO)2c~Vrq(y5Z20#XN_Trl?nKP-l3?($S&<#1&sT@92CUGD$2;DirU_O@V+{@ z80!u~z73hFHXe=4Ju?p+ElH)MVwMB}fogu{T|E*wci(hh&U%27ZMiae8ClsMOBC8p zROy(=H3)dlI^QK3QcfRA7laBIj{Ku8>*}dst`K_!W-kd=*jkPP3M2tAdJ4WIE}OINV-_w7apnAZ-Cr&Q zhu|Y673Qx%?0+hmoSdvE9*V(A`Qh|o=S!;Dp3GBFaz#a1t>0LZ;XI86B6fj>Ncu6vFUBy47C!IC}HC&H?=IdaKR z=B!9(vdz7!5SJ#@oy6tg0M{ap)cfVFMJ9(DPGoK-$P6@CbIDd+uHWJucGvRj*Hey~ zF;$?~X0B+^7ugIfv)HNA)i&(p?8_mVI2}?_Qj3DFL|%Kp-DGXTZTHvv34fA|^z@QR z_midT1oPkDuM^Q|aUU!~RA`t#)$YJMy~5r&yy1X~ii%^zT^rYz(Qt%$+~Rx8K$8Nivg@JOhC*rXMVp75Gx}AQ{ zegw-AQ1K_&k|u$xgbev=6ROlBNL)zs)3D zQlTLnV2ln8bxFMGa?~_2p%%mv6^$mqk=@ig<|VqZF`g4WAa-v)*&~f5;?3nAiDlq^ zsQLc=yE}q9BXUTLZAI#4ljr501?~PLEz{?}fB)`L8Ex=4=d0v**)7!{l2AVh%OIZo zvB;^&;dj~N*=g#?6(XT-o_4u`y&On!UnKEzi}r;sN4c6J0h;U}Fm%viz2n&mF?35_ zS=qlOFVDF0P9Zg!@d474Cd!b49}E)U+`iBtY^JR2JZ+^id0%aLY03@s28kw$cF+){ za>C7ydyqXK5`V8)>M$LQ`{;hc>H;BK*0pVXZgV?keDayw^XZNc=3P_ZBSReiUaJ?IQu~zs`uwaAdKv@>k z%Veo@QK!85u_(3U%3I02Gx7YA!>mvKv8*Ob3vt}| zPtk#&7}JC0j+|!bb>o(QTl^@V)_0y=%713VM1Ic!v|J+l!vB*jo^m7Q13Qev_J>WK z9Ub-=%lj<$R@N)_?CE5g6AMEZ`cYbC3<}*wWtElAMdq%KPSuj|8c$>k zjy_x;fd=7?-m}M|f^P(9X=xcb0oA)AfNBgKh3q$Hd}8M8_N+S6ZS^19eI8z(DI61d z`KaFgZ4v>4GyB@P#O;6h9*cfQV;?&H4J@A_3izoi%{tW^3c-);Htk-;#dOgUqp+Z% z;RX|GDB$oIiZGAAE4o06hBGzoxs%*k#_tv#Vyq8wO^%z0C-N5ju`D5v;3?wdgvxB# zk!kIO0L=b_g(@LTmCb>Bzz`8P(Ikcl4o)57+$f?xvlbIAExnl5QMefXJYVv$397y@(Bs|tVq{F@Bh zz^7znWN^H*2*%S(Y{azsO%^gvL&VM--Gw}d7QXR81s){1Dg4}RZmMwcc71D2L{WxL z>!7y1+IhajqUePjRebZ6I&U{lMHmGYH7=*U;obD`pixE%AIPqewp~fNp>t7{V;H&D zM^2YKgi;zKKLQ+!-^IdOvMPBg%1zZ5%N9qb!jLy5qph!|K7+ynCFnLLK3Mti3Jd6d z`#c^5imD)YZPw>U(E73#@zRwAkoCToR3#wHil+Z5iRkgiDc6MLoWrB!o?Qt0`!8-@ zltoSAxBHh{Tt~h7dM8cPA$SF8CyXomI+evgu6fYh{QHVx?C@ChqUh8JD1g65f4v3= z^*CIjIe{r;=!HKwJc07R6kK!^Hj1!*vG+_&hwjahaIw*FRV)vv67$fUB7ajF|NHxV zLd%LQ%KSg}_xsmZkgE0@h<{SBu!ujuC~;v6b|hQ&HX{J-47cR&49N=q++eMFmXt&g z*|Q^{n_9;s9k^Q)%?DZx6%9hNOSZyqVGBStosj;x6lX1veQ5= zkzSO-QwiTNigG(e;HhJy#bt&h*e&q#t7)ct!s5K3Im^i@ z+zd&n_***8G}VAf4es&E5I3#A-YLUx=*x3s)1s|r3}#G!Rp-SL6`YEYCqw7uxYg*< zNsc%U0-V_D6ZnAw3m|ry)o=xJ-wOHM;Ulnzg?x;!KXNd0iP%~5F}9?`2LVYvsiNv( zPMO;H$&K4S`Hku%s3Rqo%=FRXsu9n@oJr?(JfZ{(*b4^JjqzT0*Bp(glzEEOAgHch zNkeh^x@?VNy~O!PjieM&F)`}7oqR26q{$+AqE>94x9KPc1RBm5kxC*7gz`L;XiWrw z3L`AhxO(RPM#ln2)UvXws;c%Q8Qt^4HB(t>nV#E1{W4Ma6zP|8jbL&7T@Noa9!74Y zPenP7=)8e!>D#i8wbX5Z|Duv^V}^FkVXxxLJM4Qz{uU2IS3o2!(2#57EGYR;Cr-Bw ztFi=I3i&(tAx<12DyC_(a=CE>&t_#J7GP*eBKysIMjQiskL!?UH9UYaR4_LD}vjhLo2I|nd|CFqlEg(D!6kOTb#kK>na zcfp0m&4A*LP$4GTL+dJMg^}}3Rj%RP=&UnlVHVfqVM_@JivO|A^%pcr?D9P>k_xU* zPEY#<+H7VFCpAq@Bnz3*$o8}DTi_s8B;%Z;UbYZ|x%s0k2<13(1~2dI#E~xoX#sC@ zHynoqZt3m2R+$;q)TwdlFOwB|P0*~k^}HMD&3>_DY9PE(&>okpZ@VC`e+b91{{ry` zU)jag%wQirU>Pq!W=A~C2faanTWZNS2NGMKI#2+doj0)oAYv-l@9SVizw?dm%dPG0 zM||FexOz`$EpG0*WLLWx%$&bjf1URP;iRuqnq4No;Lt^F7EO!}lUY9HmR9Z3@9t#{ zT7Ts)*IJKL;+D(_v-#(l*(MOXmlf9ddMCdb`N|9ZM~vVZhRwGpwNhgQ<>$AcE}Pop zEa&}z65U?;IdkE&bs5Y3^=(ttGAlfhfh>;n`t7r25Ko%E@&e`Aw|(C+3h9}%`?7ss z7t80t@e*Gk><_(x#qZdL*R*DZB9^2@cv8%xkIw-`G%@qDOuiHgtvT5g&ozzGaTJu; zg1bpVWD^`;8yY^BmHn(;VQn&)eaK&pg?qR2h>R8-uJj%ORX|I?K<=a_-O<$4bc1Y4 zzstD)A7fCWi(=Kxm#=42g5P}V<+-!TR^zUsd8i0QQRNVquzawiNa6On@9pX` zs^h?B^;Wn{U+o2xRynck(w{omj&Li(p`%5HAoWw-mwx38-jmS6!a~x%bPyUwR45&T zV)_+=r$yV%@gFWOHnuaOH-x!wEN!`$l#Y)LWGyVD74-N<)7P#kriZ0VvdX63j?ZP_ z=MJX5LRkwc?|wo2pQD!dUXOAWT%2of^Z73mlC_UmhHtqrG76Rqdd^6ghaFF6GUT%q z=h!MdPgg?F7D7((DP)d0|Nd-|IF}v?;wH(u$b2DM@z=hcDfVmJyT72UuTdlhRfm4! zYF#H+%!u~)lK(bn{`l)W<^GlB;qcmv9~}F5TZL_n>4Y785@&E5`bIg^OLM>OwEBkc zD72|>f7>UUQ*I4)&QGmuj*jO~`d6uTDi$uLj~61{FUBn`kwHgVhGvkm9u@M#l*x6xCay6r?!vlCVtelxSg#%vtWd8ly(0U-Ou(u z72xUCj@Y?6S?_$cfJOC&Py52JY1cd7lzE5hljwB;Kv*5;9p_w$#hA`>{92~RX9cQv z?f3si{Y=@EJ&X=}Z4CL&xZaZH-p-$_JmR&*c04?*h{o*XesNJiean{l-1mU$?eDdp zm)LMN7xf&9A}zKs^Me_Tk?)XtS@EL}>qFN4kaCNiExz0FcIW8yTb7hQ$C-xW3?2T%fLB83eTB~?Khn~B^pCC&Y}Cj^mytr5Yuiv z2?Z(Tw`U?ST4&i$WV*lH?)~iPfr}fJvQ-9$BLT!H6+NVfcd0~h1B)%uIsPx6w=U!S z`*zbx^Nsq0CN=uJz7JP^2mouMwL<_%!W{10uaz_v?PT;&xZvDyb$)=C!TgyrZ|k^~ zXyR2L4age*#gez2TDl%K^N>Iov*zkn<5KCt*}`NUuzru)DFiYbHWJ^0I}x^(Hmmwn;7(2_TKO32uIadTsvlC z%BLl#q^xJ8rQNp{p46*80=n`EM@XM<2XMq%s)SZoKR#qy-Y7ju}%Deih7q7aT_l2+}ZiXJ7V5TaV-RqdliLP23-#(H*d5JeH-ffX2=`xRoJ~t*1<)Wx#4eg!$0eW*c5K#)jcZU=TrR1 z`_SmH!rQ6LpmDjU7@bsbqpX-)4r!c3#)j_=zxxtR zAk89iOvDe`Cp@HF&7T@U*Y4@%sHn&+7Z()N!S--~LC(Svjo46>CkPGeJoyeVQUQ*ap163g62Sp1tWO8QBPoQtj{i{1*rTVX7y6{5bN9Ir7GP7x zrK?cpUaIbMPFl>nLAK{@E2!*M3JX5?J$1n|W%FI%$n;1YJWO_^j7drV98O$1%#>MR zS#Es#lamcOS#Ln*Z6{RavUVjuZ-uY&^pxOMUey0oeNbF1J;A(Mdlf>wBr$W;Gw6-G z@s|R%xmvLLGqu#v=!FFe0L8zxUS3mv9=EOtl+Idiepz5?{QjK@9}jN^34n}O zA?5=?(=rbgD{EyKpR3OMq^Z8AU3vh5TqG8=pdc?_P&5QrM}R&q%|RQnkee1HaNzBB zl<7Q0mjWT@&B!`-`Iw1i;rqGWdm?*v&CLx<%4gP+2X-UqG+>0#{Wh5H8r9uaU42H> zdGy=H1PGjZ+)g)r4NFF820rgC82Ufg*(_8N1%LbwaJjK^9HN=P>y z>FwY#NNJa`sJ%gV_i-)`ut-qE%{y$T{_MSDCutWw4aD97Jg4QKq@sy@kC zYvQ+NsA!Gov8Lh{?a|g#4^k3!gfRWoda5QkiNUjWP->k#ar3rkVq&VxC6ulbw07sl z-D!X?I2W6_+?_Qnt%do`t{sQ@NKy_;9ixMNjV7)mBNexK+sy8o$NotZOUNWfz3Epz z>+;y&A|VtpQurL&dKmJ~svgm@dRR!ZSmNBnveVw24CZc8YFua=WN&HTY6T@=3L3+& z7BpjfbryAwmmRvl_^R^#jldjpRBaH%7Qo+b%LhbKrF_gj?G&M^o#OSCFuoTQ(ZIZive5&?&OF*!L51Ln1jN$NSe!Es6jZMsJn>yW#xQ8EK3vfip zjN|l3;wOEV{YG!TbaJCmJi{NKibJHKsqH}3mJ;<$DTe%;3^Tyqz0R~d5K%P*3(p;PQFHjGU&ys~3^o{!`Q3aQ->iO4_``G4|f&1!O<^_r~X&>ZlfjmnIq}-`A&ZSb8hLb5?)KBe?gNzb2sYx{wL9_IJn)=c6^)K~` z`Qx)1{uC7w>T+e@8DT-Y0ZlLXau$IR!U;v`HMM#HAkT#)6j_0mXX>OfoDh|Fc_^J_ zB_+B0+n%W@xqn^Fh=13&oeg_DC#RHw`9A2TOMWfnLS86LPt)dm#)AWcKj8ulE8}{tvN$Y#qH*bc94KML> zg~lZh_aS(ZZJ8xy3I0Hnex_gh$T1T1Hhg-b`|gZq-daetA!03a();PSg0fx@dE4iv z$B;;%!~f8w4UzW@N2`hiU=MtYV;g7H;1;&pt*NP1i$v|XMGWem*YM+`p51B&HFyQ$ zZWKMhj)=W2h%!NfTt%1sN%g^ElUSSYiOlf+?{YJ)h7rkDMBfO9*(7abRCscSLw zI$G)WCw9{fIvhwMr{&YjyANaJr0aQupAx9i(dgB1}=rX{?$+FQq!xyho^qJ z4^(KQE7N8wNZ#u86g6I{*DBtQLPO83FORl1;>f_(F1$ZLkr3@ai~ms|B}Mf{@I@^z z?kl8pC&}z*A__`C05mexmjByM5m6-p-o zaIn5qyeBXyEcBe`_)QYw##Ddk&=t4)HgL>-kQ|gj!GJFrAZ-_oDxB{&kk?Bi6`kuJ z#SD5Iz_(nTeh4=7x;oZe{NS*mmA$P-hUG0Vcv%|!J&ly~@3(6&GZ5CnQpIC;db)13 zDJj)c>OteLQ8z>=u_Cm9>PbLy#T3!ZjArw}@ZQHjx5=33a2rpWV$Y8K1O+(RGl|?4 za`O}Njgq(%oYp+LeApN+-kXdPyuWju`b4kgjQT>Az5n;G^aw)n&;! z6AzyDo&7icDO7b5Ruoq57FA#%#P>-Wg>v?=uKB0DGqc_OhwjQ`r7Eg(ZV` z&i#1yUF_nk)6ReotgKr~qJ^K6RuJdXwt|Q*oMlJ_0vfPrtFqjGC6~t{vYY5uziiaZ zQFPP8qxprc6|d({WwgE^-cGT8nNnpZpQ8`GurA`{yFBE7!jD4?5Q%U9mXyhP`y@-` zFlo)5dXcQFr}ypX#)DHXDyj`QkDGD38!ayT z<1L$^q;4XXvOJ!rVPaFLdph}Tqi?ZaJ>ocoShl*zVbj`L-6V`FHX`LsNZMxOhtqq{ zo7CbrN%neVadJ^h(W-d`5j{|#&avBvpx*8R6?JbmY1Sh~>)Vf+Lv`s`62a2uk`3M2 ze5YX~%wzhlU z4nG+GnYKVV!XZ^+-tI%yV?)q(yB5WWU>tlDgK9$>Qt^PHkQ^JPIlH5-AB>C;myt2b zO!AoX4@sw)O`Bt7DX}^2*U^j)4?5K|$#PH-z>4e@3EI-S-@1vn1!ZySFNZDvit`nhy_ zl(_vu-k@w<`>WZqYZfKLBmxZ8gG0zfOS}u`_N;lII7kPCau=Dm;`)&EKg9cZH2T93jh~SR!TiZz<*3j3yU{TV

        W8kA#-ikCzT) z3X?^yFxuD(?GS^Bs#!ouRz6w5GvDI_{IV zlR=zd=#g}C;8EiTGFxpRTx+`#4wtm|tEttu#ei@JE+$j~7Y!hH3mYYv@aFW-XYu(M zu*Q2_t>Ipg6}*cM*x^`wen+Z+03pUM9vE*`8w+SD&`5O;2!If9Ir?=DM%5~4tl$3&tCE*f zQzJoGz|=?&Vr|}j;M!U0O~Y^a#`^AE=^3mtIeWk6ea6z&3Bzi*$2XFao@Q?LhQ(23 zVE!i+cP{fL_-9#}e=^{MLB-rj)OORgcM`$BIk;(wxu>)`{;X|bq5f6Ad0Kl*t^Vbb ze9C%Nso2070)k5RI^+m*v@U<~XsV!kul~MZ-wNLi{`kOm+|EMk5B=<`MvM2@&1_|{ zg#EV*Y}e9vi#l&pGVwW9J~i64sJy#BmyU1jM`i55$cdcCc)}UHq)1sV>KE|MbS^I{ zN`u@

        P6(f}Wl(Cz24s$;CmQ%!KTz+pZTE7rH%U2jLQJtD$iHe{6XPqvFBg;dxRB zAt%dCGbI6had|qn2t)~=!#AMBMiDL+A~v=00ygZ^JCRdxX7y3T1#Hm-1%tb=JeOxc z`PF6gTQ2)-7=%g(kWp3^POiW1v9#ZLJnJ;Xm@cg&{vM?iYiV(NYIyVX!b($B61h`z z<~Jam9kJIMJ1~-yfi};(^?bJy3s2JijyWjeZ1TDGqOQq+uzk13F@)*G8?c~@tOv(1 zEEwSQHq=mh$oprxmnd^Wd7>=W{na4J{U14OV8 ze%7b$H~%dx-0{iM%29lo^!Cj(DA>#mu(Zu|431rA2xOZl5;2xUf^I&SA0P9<0eq^c z%3ZyZ^(jq7MUrtw0b*>m0KrXuK6R>)FGP^i=8cIfn1LbeCB~GXuUq!Og`N0Hgc0-y&I}QtClx$)elq*=$TWl|jZ2$qs!NtWTdiKpmnnm`z&ilQL&eN5aOUW>s zAQ-BXx#RFh#!Y~k94q}&YCQlfDq$A2?J>AhxSkm=OJq*CZ7d*Ob_+v8Zu#`ks6is6 zqh7xp;uh@KrvHv*#T^%Y$}ysFLH5(TQ$2IBryQ?Nu;G)k{ubCk*-j1|9=IUwq-EDg zgT9Qt2*0dzQG5Y&XZ}FIsz;5C#5z`zlk*W0q$yy+{Wlx=FS5M8UZb6V!i13(!rrre zLA2{=#*BERKoQqkef567k8>aTy*p%MoADQk0*Q7F5)@N^Y zw0pn*P;U6Tzr<74Y4Flp8`osF{BL?BS-9isWWn%xfh0$TMG2msg=1j*+=N2M1TOp< zttChj8O!x#&2D%|wSX6cgwxd}$M5;Z_3v~MGY3aYnu@QNGUI{FBS&yJQ!K5-N!%q) zcmV+}qsq_A+mn;D`1n-n7#$HCa#5{r;V!HAR*9PUZN`7-3gGk*tSS9wN8{CBo|mWM z()t)x!B~gCE(S=?q%qgND`{$L-~O&>TJvf;UY*J-U@p7vM**&$>X=xzegTOf7hoz zF*F9vjR)7qek4SXgovo=5OmT69@tox6gLV(K&o6(A)+jVv-2eKDV3|W{p%&Am6e7~ zU&i3z=qHE>34cw_F5Ve7EnogErIG5856+k;Bq~FY{T(A~=?R!c_pLB8v7Am9>D_9a zPhij>16EX1U0jKemy)Q!R#e<~P&nJHB7!HwCk7bW`jMx{tr3S%9T(A~nUGmWMV|YP zDOggWw7mR@iUfzM-1;b;$A*FdxF3Q=p!!Vh*iLUCPCMhLXW;?v`fa9GV?Qkh$A7NW z-PpsDMV~du=8j71=f3;g?OSkFF1;N3l7gFE-?ZQS^P9911( zG*#aT=-1~|zI4{@FIqoI z7U!P&j|I3su-JgepVh+^4HTb4Yp0;S`BLyD4F|#lVdu)p#xF`6OjMeYLEbWwbrdjU zq=mzQ0MeNRs%+&*f2>DI*^MffcSj^KSy;{_i?NjD2EsCKhX*NNF#fvq�)`7st8? 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 literal 0 HcmV?d00001 diff --git a/liteloader/src/main/resources/liteloader.properties b/liteloader/src/main/resources/liteloader.properties new file mode 100644 index 00000000..6b8768a7 --- /dev/null +++ b/liteloader/src/main/resources/liteloader.properties @@ -0,0 +1,5 @@ +search.mods=true +search.jar=false +search.classpath=true +log=stderr +brand= \ No newline at end of file diff --git a/liteloader/src/main/resources/mixins.liteloader.core.json b/liteloader/src/main/resources/mixins.liteloader.core.json new file mode 100644 index 00000000..51b3a199 --- /dev/null +++ b/liteloader/src/main/resources/mixins.liteloader.core.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.4.10", + "package": "com.mumfrey.liteloader.common.mixin", + "refmap": "mixins.liteloader.core.refmap.json", + "mixins": [ + "MixinMinecraftServer", + "MixinServerConfigurationManager", + "MixinNetHandlerPlayServer", + "MixinItemInWorldManager", + "MixinC15PacketClientSettings", + "MixinS02PacketChat" + ] +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 8ca604a6..74919bb1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ rootProject.name = 'MineLittlePony' -include 'voxellib', 'forge' +include 'voxellib', 'forge', 'LiteLoader' diff --git a/voxellib/build.gradle b/voxellib/build.gradle index b0d10922..1d49bdf7 100644 --- a/voxellib/build.gradle +++ b/voxellib/build.gradle @@ -13,7 +13,7 @@ repositories.flatDir { dir '../liteloader' } dependencies { - deobfCompile 'com.mumfrey:liteloader:1.8-SNAPSHOT:srgnames' + compile rootProject.project('LiteLoader') } jar { manifest.attributes.remove 'TweakClass' diff --git a/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateClasses.java b/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateClasses.java index af555dae..9b54e915 100644 --- a/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateClasses.java +++ b/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateClasses.java @@ -1,7 +1,7 @@ package com.voxelmodpack.common.runtime; import com.mumfrey.liteloader.core.runtime.Obf; -import com.mumfrey.liteloader.util.ModUtilities; +import com.mumfrey.liteloader.util.ObfuscationUtilities; import net.minecraft.inventory.Container; import net.minecraft.inventory.Slot; @@ -25,9 +25,9 @@ public class PrivateClasses { */ private final String className; - @SuppressWarnings({ "unchecked", "deprecation" }) + @SuppressWarnings("unchecked") private PrivateClasses(Obf mapping) { - this.className = ModUtilities.getObfuscatedFieldName(mapping); + this.className = ObfuscationUtilities.getObfuscatedFieldName(mapping); Class reflectedClass = null; diff --git a/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateFields.java b/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateFields.java index c38bbb9e..3428afc8 100644 --- a/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateFields.java +++ b/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateFields.java @@ -33,7 +33,7 @@ import net.minecraft.world.storage.WorldInfo; import paulscode.sound.SoundSystem; import com.mumfrey.liteloader.core.runtime.Obf; -import com.mumfrey.liteloader.util.ModUtilities; +import com.mumfrey.liteloader.util.ObfuscationUtilities; /** * Wrapper for obf/mcp reflection-accessed private fields, mainly added to @@ -65,10 +65,9 @@ public class PrivateFields { * @param mcpName * @param name */ - @SuppressWarnings("deprecation") private PrivateFields(Class

        owner, Obf mapping) { this.parentClass = owner; - this.fieldName = ModUtilities.getObfuscatedFieldName(mapping); + this.fieldName = ObfuscationUtilities.getObfuscatedFieldName(mapping); } /** diff --git a/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateMethods.java b/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateMethods.java index c29e7504..bb91dd5c 100644 --- a/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateMethods.java +++ b/voxellib/src/main/java/com/voxelmodpack/common/runtime/PrivateMethods.java @@ -11,7 +11,7 @@ import net.minecraft.inventory.Container; import net.minecraft.inventory.Slot; import com.mumfrey.liteloader.core.runtime.Obf; -import com.mumfrey.liteloader.util.ModUtilities; +import com.mumfrey.liteloader.util.ObfuscationUtilities; /** * Wrapper for obf/mcp reflection-accessed private methods, added to centralise @@ -48,10 +48,9 @@ public class PrivateMethods { * @param mcpName * @param name */ - @SuppressWarnings("deprecation") private PrivateMethods(Class owner, Obf mapping, Class... parameterTypes) { this.parentClass = owner; - this.methodName = ModUtilities.getObfuscatedFieldName(mapping); + this.methodName = ObfuscationUtilities.getObfuscatedFieldName(mapping); Method method = null; diff --git a/voxellib/src/main/java/com/voxelmodpack/common/runtime/Reflection.java b/voxellib/src/main/java/com/voxelmodpack/common/runtime/Reflection.java index 6dc0eaf0..be8d2cf3 100644 --- a/voxellib/src/main/java/com/voxelmodpack/common/runtime/Reflection.java +++ b/voxellib/src/main/java/com/voxelmodpack/common/runtime/Reflection.java @@ -3,7 +3,7 @@ package com.voxelmodpack.common.runtime; import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import com.mumfrey.liteloader.util.ModUtilities; +import com.mumfrey.liteloader.util.ObfuscationUtilities; public class Reflection { private static Field MODIFIERS = null; @@ -26,12 +26,11 @@ public class Reflection { * @param fieldName Name of the field to set * @param value Value to set for the field */ - @SuppressWarnings("deprecation") public static void setPrivateValue(Class instanceClass, Object instance, String fieldName, String obfuscatedFieldName, String seargeName, Object value) throws IllegalArgumentException, SecurityException, NoSuchFieldException { Reflection.setPrivateValueRaw(instanceClass, instance, - ModUtilities.getObfuscatedFieldName(fieldName, obfuscatedFieldName, seargeName), value); + ObfuscationUtilities.getObfuscatedFieldName(fieldName, obfuscatedFieldName, seargeName), value); } /** @@ -57,12 +56,12 @@ public class Reflection { * @param fieldName Name of the field to get the value of * @return Value of the field */ - @SuppressWarnings({ "unchecked", "deprecation" }) + @SuppressWarnings("unchecked") public static T getPrivateValue(Class instanceClass, Object instance, String fieldName, String obfuscatedFieldName, String seargeName) throws IllegalArgumentException, SecurityException, NoSuchFieldException { return (T) Reflection.getPrivateValueRaw(instanceClass, instance, - ModUtilities.getObfuscatedFieldName(fieldName, obfuscatedFieldName, seargeName)); + ObfuscationUtilities.getObfuscatedFieldName(fieldName, obfuscatedFieldName, seargeName)); } /**