From ec1ab8ece31743ea7ee79c5629b4823d85adcb3c Mon Sep 17 00:00:00 2001 From: lax1dude Date: Sat, 21 Sep 2024 20:17:42 -0700 Subject: [PATCH] Update #37 - Touch support without userscript, many other feats --- CODE_STANDARDS.md | 306 +++ CREDITS | 56 +- README.md | 47 +- buildtools/BuildTools.jar | Bin 198863 -> 201837 bytes buildtools/production-index-ext.html | 16 +- buildtools/production-index.html | 16 +- .../gui/CompileLatestClientGUI.java | 22 +- .../v1_8/buildtools/gui/ES6Compat.java | 43 + .../v1_8/buildtools/gui/TeaVMBinaries.java | 2 +- .../headless/CompileLatestClientHeadless.java | 25 +- .../buildtools/task/diff/PullRequestTask.java | 112 +- .../buildtools/task/init/SetupWorkspace.java | 70 +- .../buildtools/task/teavm/TeaVMBridge.java | 20 +- client_version | 2 +- patches/minecraft/delete.txt | 3 +- .../client/LoadingScreenRenderer.edit.java | 26 +- .../net/minecraft/client/Minecraft.edit.java | 923 +++++-- .../client/audio/SoundCategory.edit.java | 6 +- .../client/audio/SoundHandler.edit.java | 13 +- .../entity/AbstractClientPlayer.edit.java | 48 +- .../client/entity/EntityPlayerSP.edit.java | 9 +- .../minecraft/client/gui/ChatLine.edit.java | 28 + .../client/gui/FontRenderer.edit.java | 49 +- .../minecraft/client/gui/GuiButton.edit.java | 8 + .../minecraft/client/gui/GuiChat.edit.java | 88 +- .../client/gui/GuiCommandBlock.edit.java | 19 +- .../client/gui/GuiControls.edit.java | 12 +- .../client/gui/GuiCreateFlatWorld.edit.java | 7 +- .../client/gui/GuiCreateWorld.edit.java | 18 +- .../gui/GuiCustomizeWorldScreen.edit.java | 27 +- .../client/gui/GuiDownloadTerrain.edit.java | 7 +- .../client/gui/GuiEnchantment.edit.java | 3 +- .../client/gui/GuiFlatPresets.edit.java | 27 +- .../minecraft/client/gui/GuiIngame.edit.java | 402 ++- .../client/gui/GuiIngameMenu.edit.java | 142 +- .../client/gui/GuiKeyBindingList.edit.java | 32 +- .../client/gui/GuiLanguage.edit.java | 13 +- .../client/gui/GuiMainMenu.edit.java | 5 +- .../client/gui/GuiMultiplayer.edit.java | 44 +- .../minecraft/client/gui/GuiNewChat.edit.java | 4 + .../client/gui/GuiOptionSlider.edit.java | 18 + .../minecraft/client/gui/GuiOptions.edit.java | 79 +- .../client/gui/GuiOptionsRowList.edit.java | 78 +- .../client/gui/GuiOverlayDebug.edit.java | 107 +- .../client/gui/GuiPageButtonList.edit.java | 53 +- .../client/gui/GuiPlayerTabOverlay.edit.java | 11 +- .../client/gui/GuiRenameWorld.edit.java | 17 +- .../minecraft/client/gui/GuiRepair.edit.java | 16 +- .../minecraft/client/gui/GuiScreen.edit.java | 352 ++- .../client/gui/GuiScreenAddServer.edit.java | 52 +- .../client/gui/GuiScreenBook.edit.java | 38 +- .../gui/GuiScreenCustomizePresets.edit.java | 26 +- .../gui/GuiScreenOptionsSounds.edit.java | 7 + .../gui/GuiScreenResourcePacks.edit.java | 11 +- .../client/gui/GuiScreenServerList.edit.java | 17 +- .../client/gui/GuiSelectWorld.edit.java | 38 +- .../minecraft/client/gui/GuiSlider.edit.java | 7 + .../minecraft/client/gui/GuiSlot.edit.java | 48 +- .../client/gui/GuiTextField.edit.java | 24 +- .../gui/GuiUtilRenderComponents.edit.java | 7 + .../client/gui/GuiVideoSettings.edit.java | 71 +- .../client/gui/ScaledResolution.edit.java | 20 + .../client/gui/ScreenChatOptions.edit.java | 28 +- .../gui/achievement/GuiAchievement.edit.java | 6 +- .../gui/achievement/GuiAchievements.edit.java | 9 +- .../client/gui/achievement/GuiStats.edit.java | 20 +- .../gui/inventory/GuiContainer.edit.java | 100 +- .../inventory/GuiContainerCreative.edit.java | 49 +- .../gui/inventory/GuiEditSign.edit.java | 45 +- .../multiplayer/ChunkProviderClient.edit.java | 19 +- .../multiplayer/GuiConnecting.edit.java | 215 +- .../multiplayer/PlayerControllerMP.edit.java | 7 +- .../client/multiplayer/ServerData.edit.java | 45 +- .../client/multiplayer/ServerList.edit.java | 10 +- .../client/multiplayer/WorldClient.edit.java | 24 +- .../network/NetHandlerPlayClient.edit.java | 134 +- .../network/NetworkPlayerInfo.edit.java | 54 +- .../client/particle/EffectRenderer.edit.java | 31 +- .../client/renderer/EntityRenderer.edit.java | 263 +- .../client/renderer/RenderGlobal.edit.java | 70 +- .../client/renderer/entity/Render.edit.java | 6 +- .../renderer/entity/RenderPlayer.edit.java | 6 +- .../entity/RendererLivingEntity.edit.java | 6 +- .../texture/AbstractTexture.edit.java | 6 +- .../renderer/texture/TextureMap.edit.java | 39 +- .../renderer/texture/TextureUtil.edit.java | 32 +- .../tileentity/RenderItemFrame.edit.java | 6 +- .../TileEntityBannerRenderer.edit.java | 13 +- .../TileEntityChestRenderer.edit.java | 9 +- .../TileEntitySignRenderer.edit.java | 36 +- .../resources/DefaultResourcePack.edit.java | 10 +- .../client/resources/Locale.edit.java | 2 +- .../client/settings/GameSettings.edit.java | 294 ++- .../net/minecraft/entity/Entity.edit.java | 89 +- .../minecraft/entity/EntityLiving.edit.java | 37 +- .../entity/EntityLivingBase.edit.java | 30 +- .../entity/ai/EntityAITasks.edit.java | 16 +- .../entity/ai/EntitySenses.edit.java | 12 + .../entity/item/EntityItem.edit.java | 2 +- .../entity/item/EntityItemFrame.edit.java | 2 +- .../entity/item/EntityMinecart.edit.java | 22 +- .../entity/passive/EntityHorse.edit.java | 17 +- .../entity/passive/EntityOcelot.edit.java | 17 + .../entity/passive/EntityTameable.edit.java | 19 +- .../entity/passive/EntityVillager.edit.java | 18 +- .../entity/player/EntityPlayer.edit.java | 14 +- .../entity/player/EntityPlayerMP.edit.java | 11 +- .../net/minecraft/item/Item.edit.java | 8 +- .../net/minecraft/item/ItemBlock.edit.java | 2 +- .../net/minecraft/item/ItemEgg.edit.java | 7 + .../net/minecraft/item/ItemEnderEye.edit.java | 7 + .../minecraft/item/ItemEnderPearl.edit.java | 7 + .../minecraft/item/ItemExpBottle.edit.java | 7 + .../net/minecraft/item/ItemFirework.edit.java | 7 + .../net/minecraft/item/ItemSnowball.edit.java | 7 + .../net/minecraft/item/ItemStack.edit.java | 58 +- .../network/NetHandlerPlayServer.edit.java | 129 +- .../client/C00PacketLoginStart.edit.java | 31 +- .../server/S02PacketLoginSuccess.edit.java | 28 +- .../pathfinding/PathNavigate.edit.java | 10 +- .../net/minecraft/profiler/Profiler.edit.java | 21 - .../scoreboard/ScoreObjective.edit.java | 23 +- .../server/MinecraftServer.edit.java | 89 +- .../ServerConfigurationManager.edit.java | 55 +- .../network/NetHandlerLoginServer.edit.java | 97 +- .../tileentity/TileEntitySign.edit.java | 33 +- .../util/ChatComponentStyle.edit.java | 7 + .../net/minecraft/util/ChatStyle.edit.java | 23 +- .../net/minecraft/util/MouseHelper.edit.java | 8 +- .../util/MovementInputFromOptions.edit.java | 34 +- .../net/minecraft/util/Session.edit.java | 6 +- .../minecraft/util/StringTranslate.edit.java | 10 +- .../net/minecraft/util/StringUtils.edit.java | 2 +- .../net/minecraft/util/Timer.edit.java | 16 +- .../net/minecraft/world/GameRules.edit.java | 3 +- .../net/minecraft/world/World.edit.java | 79 +- .../net/minecraft/world/WorldServer.edit.java | 55 +- .../world/WorldServerMulti.edit.java | 9 +- .../world/border/WorldBorder.edit.java | 23 +- .../net/minecraft/world/chunk/Chunk.edit.java | 10 +- .../world/demo/DemoWorldServer.edit.java | 9 +- .../world/storage/MapStorage.edit.java | 2 +- .../world/storage/SaveFormatOld.edit.java | 19 +- .../world/storage/SaveHandler.edit.java | 33 +- .../world/storage/WorldInfo.edit.java | 10 +- .../assets/minecraft/lang/en_US.edit.lang | 205 +- .../lwjgl/java/fi/iki/elonen/NanoHTTPD.java | 2333 +++++++++++++++++ .../v1_8/internal/OpenGLObjects.java | 36 + .../v1_8/internal/PlatformApplication.java | 59 +- .../v1_8/internal/PlatformAssets.java | 26 +- .../v1_8/internal/PlatformAudio.java | 8 +- .../v1_8/internal/PlatformFilesystem.java | 123 +- .../v1_8/internal/PlatformInput.java | 414 ++- .../v1_8/internal/PlatformNetworking.java | 130 +- .../v1_8/internal/PlatformOpenGL.java | 292 ++- .../v1_8/internal/PlatformRuntime.java | 142 +- .../v1_8/internal/PlatformScreenRecord.java | 59 + .../v1_8/internal/PlatformUpdateSvc.java | 10 + .../v1_8/internal/PlatformWebRTC.java | 883 +------ .../v1_8/internal/PlatformWebView.java | 93 + .../v1_8/internal/WebSocketServerQuery.java | 173 -- .../buffer/EaglerLWJGLByteBuffer.java | 111 +- .../buffer/EaglerLWJGLFloatBuffer.java | 84 +- .../internal/buffer/EaglerLWJGLIntBuffer.java | 86 +- .../buffer/EaglerLWJGLShortBuffer.java | 86 +- .../v1_8/internal/lwjgl/DebugFilesystem.java | 34 +- .../lwjgl/DesktopClientConfigAdapter.java | 60 +- .../lwjgl/DesktopWebSocketClient.java | 109 + .../lwjgl/DesktopWebSocketFrameBinary.java | 64 + .../lwjgl/DesktopWebSocketFrameString.java} | 58 +- .../internal/lwjgl/FallbackWebViewHTTPD.java | 41 + .../lwjgl/FallbackWebViewProtocol.java | 299 +++ .../internal/lwjgl/FallbackWebViewServer.java | 190 ++ .../internal/lwjgl/FallbackWebViewWSD.java | 273 ++ .../v1_8/internal/lwjgl/JDBCFilesystem.java | 38 +- .../lwjgl/JDBCFilesystemConverter.java | 6 +- .../v1_8/internal/lwjgl/LWJGLEntryPoint.java | 12 +- .../WebSocketClientImpl.java} | 72 +- .../internal/ClientPlatformSingleplayer.java | 12 +- .../internal/ServerPlatformSingleplayer.java | 33 +- .../internal/lwjgl/MemoryConnection.java | 4 +- .../com/google/common/base/CharMatcher.java | 24 +- .../net/lax1dude/eaglercraft/v1_8/Base64.java | 8 + .../v1_8/ClientUUIDLoadingCache.java | 152 ++ .../lax1dude/eaglercraft/v1_8/Display.java | 41 +- .../lax1dude/eaglercraft/v1_8/EagRuntime.java | 101 +- .../lax1dude/eaglercraft/v1_8/EagUtils.java | 9 + .../eaglercraft/v1_8/EaglerOutputStream.java | 5 + .../v1_8/EaglerXBungeeVersion.java | 11 +- .../v1_8/EaglercraftSoundManager.java | 6 +- .../eaglercraft/v1_8/EaglercraftUUID.java | 16 +- .../eaglercraft/v1_8/EaglercraftVersion.java | 13 +- .../lax1dude/eaglercraft/v1_8/Filesystem.java | 158 ++ .../lax1dude/eaglercraft/v1_8/Gamepad.java | 115 + .../lax1dude/eaglercraft/v1_8/HashKey.java | 54 + .../lax1dude/eaglercraft/v1_8/IOUtils.java | 2 +- .../lax1dude/eaglercraft/v1_8/Keyboard.java | 5 + .../net/lax1dude/eaglercraft/v1_8/Mouse.java | 17 + .../v1_8/PauseMenuCustomizeState.java | 257 ++ .../v1_8/PointerInputAbstraction.java | 227 ++ .../net/lax1dude/eaglercraft/v1_8/Touch.java | 134 + .../boot_menu/GuiScreenEnterBootMenu.java | 54 + .../v1_8/cache/EaglerLoadingCache.java | 2 +- .../cookie/GuiScreenInspectSessionToken.java | 84 + .../cookie/GuiScreenRevokeSessionToken.java | 146 ++ .../cookie/GuiScreenSendRevokeRequest.java | 177 ++ .../v1_8/cookie/HardwareFingerprint.java | 294 +++ .../v1_8/cookie/ServerCookieDataStore.java | 396 +++ .../v1_8/crypto/AESLightEngine.java | 527 ++++ .../v1_8/futures/ListenableFutureTask.java | 4 +- .../internal/AbstractWebSocketClient.java | 227 ++ .../EaglerMissingResourceException.java} | 29 +- .../v1_8/internal/EnumFireKeyboardEvent.java | 20 + .../v1_8/internal/EnumFireMouseEvent.java | 20 + .../v1_8/internal/EnumPlatformANGLE.java | 5 + .../v1_8/internal/EnumPlatformAgent.java | 5 +- .../v1_8/internal/EnumPlatformOS.java | 6 + .../EnumTouchEvent.java} | 42 +- .../v1_8/internal/EnumWebViewContentMode.java | 20 + .../v1_8/internal/GLObjectMap.java | 7 + .../v1_8/internal/GamepadConstants.java | 134 + .../v1_8/internal/IClientConfigAdapter.java | 22 +- .../internal/IClientConfigAdapterHooks.java | 2 + .../v1_8/internal/IEaglerFilesystem.java | 46 + .../v1_8/internal/IServerQuery.java | 12 +- .../IWebSocketClient.java} | 74 +- .../v1_8/internal/IWebSocketFrame.java} | 28 +- .../v1_8/internal/QueryResponse.java | 4 +- .../v1_8/internal/RamdiskFilesystemImpl.java | 131 + .../v1_8/internal/ScreenRecordParameters.java | 37 + .../VFSFilenameIteratorNonRecursive.java} | 50 +- .../v1_8/internal/WebViewOptions.java | 67 + .../v1_8/internal/buffer/Buffer.java | 8 +- .../v1_8/internal/buffer/ByteBuffer.java | 12 +- .../buffer/EaglerBufferInputStream.java | 1 - .../v1_8/internal/buffer/FloatBuffer.java | 12 +- .../v1_8/internal/buffer/IntBuffer.java | 12 +- .../v1_8/internal/buffer/ShortBuffer.java | 12 +- .../vfs2/VFSFilenameIteratorImpl.java | 7 +- .../vfs2/VFSListFilesIteratorImpl.java | 7 +- .../v1_8/internal/vfs2/VFile2.java | 85 +- .../v1_8/internal/vfs2/VFileOutputStream.java | 3 +- .../v1_8/json/JSONTypeProvider.java | 6 +- .../v1_8/json/impl/SoundMapDeserializer.java | 2 +- .../eaglercraft/v1_8/log4j/LogManager.java | 2 +- .../eaglercraft/v1_8/log4j/Logger.java | 2 +- .../v1_8/minecraft/ChunkUpdateManager.java | 13 +- .../minecraft/EaglerFolderResourcePack.java | 2 +- .../v1_8/minecraft/EaglerFontRenderer.java | 19 +- .../minecraft/EaglerTextureAtlasSprite.java | 11 +- .../v1_8/minecraft/EnumInputEvent.java | 20 + .../v1_8/minecraft/FontMappingHelper.java | 525 ++++ .../minecraft/GuiButtonWithStupidIcons.java | 132 + .../GuiScreenGenericErrorMessage.java | 6 +- .../minecraft/GuiScreenVisualViewport.java | 144 + .../v1_8/minecraft/TextureAnimationCache.java | 6 +- .../CachedNotifBadgeTexture.java | 46 + .../v1_8/notifications/ClickEventZone.java | 41 + .../notifications/GuiButtonNotifBell.java | 69 + .../notifications/GuiScreenNotifications.java | 172 ++ .../notifications/GuiSlotNotifications.java | 338 +++ .../v1_8/notifications/NotificationBadge.java | 171 ++ .../v1_8/notifications/NotificationIcon.java | 51 + .../ServerNotificationManager.java | 277 ++ .../ServerNotificationRenderer.java | 539 ++++ .../eaglercraft/v1_8/opengl/DrawUtils.java | 58 +- .../v1_8/opengl/EaglerMeshLoader.java | 22 +- .../v1_8/opengl/EaglercraftGPU.java | 457 +++- .../v1_8/opengl/EffectPipelineFXAA.java | 38 +- .../v1_8/opengl/FixedFunctionPipeline.java | 90 +- .../v1_8/opengl/FixedFunctionShader.java | 1 - .../eaglercraft/v1_8/opengl/GLSLHeader.java | 99 + .../v1_8/opengl/GameOverlayFramebuffer.java | 45 +- .../v1_8/opengl/GlStateManager.java | 31 +- .../eaglercraft/v1_8/opengl/ImageData.java | 50 + .../v1_8/opengl/InstancedFontRenderer.java | 122 +- .../opengl/InstancedParticleRenderer.java | 101 +- .../v1_8/opengl/RealOpenGLEnums.java | 2 +- .../v1_8/opengl/SoftGLBufferArray.java | 225 ++ .../v1_8/opengl/SoftGLBufferState.java | 31 + .../v1_8/opengl/SpriteLevelMixer.java | 38 +- .../eaglercraft/v1_8/opengl/StreamBuffer.java | 6 +- .../v1_8/opengl/TextureCopyUtil.java | 123 +- .../v1_8/opengl/TextureFormatHelper.java | 81 + .../v1_8/opengl/VSHInputLayoutParser.java | 91 + .../v1_8/opengl/WorldRenderer.java | 13 +- .../opengl/ext/deferred/BlockVertexIDs.java | 2 +- .../ext/deferred/CloudRenderWorker.java | 12 +- .../ext/deferred/DebugFramebufferView.java | 15 +- .../ext/deferred/DeferredStateManager.java | 4 +- .../ext/deferred/DynamicLightInstance.java | 4 +- .../ext/deferred/DynamicLightManager.java | 8 +- .../ext/deferred/EaglerDeferredPipeline.java | 184 +- .../ForwardRenderCallbackHandler.java | 2 +- .../ext/deferred/LensFlareMeshRenderer.java | 20 +- .../opengl/ext/deferred/ShaderPackInfo.java | 2 +- .../ext/deferred/gui/GuiShaderConfig.java | 5 + .../ext/deferred/gui/GuiShaderConfigList.java | 9 +- .../PipelineShaderAccelParticleForward.java | 2 +- .../program/PipelineShaderGBufferCombine.java | 2 +- .../program/PipelineShaderGBufferFog.java | 2 +- .../program/PipelineShaderLightingPoint.java | 2 +- .../program/PipelineShaderLightingSun.java | 2 +- .../PipelineShaderPostExposureAvg.java | 2 +- .../program/PipelineShaderReprojControl.java | 2 +- .../program/PipelineShaderShadowsSun.java | 2 +- .../program/PipelineShaderSkyboxRender.java | 2 +- .../ext/deferred/program/ShaderCompiler.java | 4 +- .../ext/deferred/program/ShaderSource.java | 2 +- .../texture/EaglerTextureAtlasSpritePBR.java | 3 +- .../ext/deferred/texture/EmissiveItems.java | 2 +- .../texture/PBRMaterialConstants.java | 2 +- .../DynamicLightBucketLoader.java | 163 +- .../DynamicLightsStateManager.java | 22 +- .../GuiScreenContentWarning.java | 63 + .../LookAlikeUnicodeConv.java | 1028 ++++++++ .../profanity_filter/ProfanityFilter.java | 534 ++++ .../eaglercraft/v1_8/profile/CapePackets.java | 48 - .../v1_8/profile/EaglerProfile.java | 110 +- .../v1_8/profile/EaglerSkinTexture.java | 26 +- .../v1_8/profile/GuiAuthenticationScreen.java | 15 +- .../v1_8/profile/GuiScreenEditCape.java | 9 +- .../v1_8/profile/GuiScreenEditProfile.java | 21 +- .../v1_8/profile/GuiScreenImportProfile.java | 4 +- .../v1_8/profile/RenderHighPoly.java | 3 +- .../v1_8/profile/ServerCapeCache.java | 63 +- .../v1_8/profile/ServerSkinCache.java | 80 +- .../eaglercraft/v1_8/profile/SkinModel.java | 2 +- .../eaglercraft/v1_8/profile/SkinPackets.java | 100 +- .../v1_8/profile/SkinPreviewRenderer.java | 3 +- .../recording/EnumScreenRecordingCodec.java | 152 ++ .../recording/GuiScreenRecordingNote.java | 52 + .../recording/GuiScreenRecordingSettings.java | 201 ++ .../v1_8/recording/GuiScreenSelectCodec.java | 92 + .../v1_8/recording/GuiSlotSelectCodec.java | 58 + .../recording/ScreenRecordingController.java | 99 + .../v1_8/socket/ConnectionHandshake.java | 196 +- .../socket/EaglercraftNetworkManager.java | 120 +- .../v1_8/socket/GuiHandshakeApprove.java | 2 +- .../v1_8/socket/RateLimitTracker.java | 14 +- .../v1_8/socket/ServerQueryDispatch.java | 4 +- .../v1_8/socket/ServerQueryImpl.java} | 102 +- .../v1_8/socket/WebSocketNetworkManager.java | 152 ++ .../client/ClientV3MessageHandler.java | 130 + .../client/ClientV4MessageHandler.java | 222 ++ .../client/GameProtocolMessageController.java | 199 ++ .../client/IPluginMessageSendFunction.java | 24 + .../client/PacketBufferInputWrapper.java | 303 +++ .../client/PacketBufferOutputWrapper.java | 316 +++ .../v1_8/sp/SingleplayerServerController.java | 62 +- .../eaglercraft/v1_8/sp/SkullCommand.java | 9 +- .../sp/gui/GuiIntegratedServerStartup.java | 58 - .../v1_8/sp/gui/GuiScreenAddRelay.java | 13 + .../GuiScreenDemoIntegratedServerStartup.java | 43 +- .../sp/gui/GuiScreenIntegratedServerBusy.java | 10 +- .../gui/GuiScreenIntegratedServerCrashed.java | 4 +- .../gui/GuiScreenIntegratedServerFailed.java | 12 + .../gui/GuiScreenIntegratedServerStartup.java | 35 +- .../v1_8/sp/gui/GuiScreenLANConnect.java | 11 + .../v1_8/sp/gui/GuiScreenLANConnecting.java | 10 +- .../v1_8/sp/gui/GuiScreenNameWorldImport.java | 14 +- .../sp/gui/GuiScreenRAMDiskModeDetected.java | 55 + .../v1_8/sp/gui/GuiScreenRelay.java | 12 +- .../gui/GuiScreenSingleplayerConnecting.java | 18 +- .../v1_8/sp/gui/GuiShareToLan.java | 12 + .../eaglercraft/v1_8/sp/gui/GuiSlider2.java | 36 +- .../v1_8/sp/ipc/IPCPacket14StringList.java | 6 +- .../v1_8/sp/ipc/IPCPacket16NBTList.java | 6 +- .../v1_8/sp/ipc/IPCPacket17ConfigureLAN.java | 2 +- ...age.java => IPCPacket1ALoggerMessage.java} | 10 +- ...ing.java => IPCPacket1BEnableLogging.java} | 8 +- .../v1_8/sp/ipc/IPCPacket1CIssueDetected.java | 57 + .../v1_8/sp/ipc/IPCPacketManager.java | 7 +- .../v1_8/sp/lan/LANClientNetworkManager.java | 60 +- .../v1_8/sp/lan/LANClientPeer.java | 21 +- .../v1_8/sp/lan/LANServerController.java | 54 +- .../v1_8/sp/lan/LANServerList.java | 18 +- .../v1_8/sp/relay/RelayLoggerImpl.java | 54 + .../v1_8/sp/relay/RelayManager.java | 28 +- .../eaglercraft/v1_8/sp/relay/RelayQuery.java | 1 + .../v1_8/sp/relay/RelayQueryImpl.java | 225 ++ .../sp/relay/RelayQueryRateLimitDummy.java | 75 + .../v1_8/sp/relay/RelayServer.java | 36 +- .../sp/relay/RelayServerRateLimitTracker.java | 98 + .../v1_8/sp/relay/RelayServerSocket.java | 9 +- .../v1_8/sp/relay/RelayServerSocketImpl.java | 173 ++ .../RelayServerSocketRateLimitDummy.java | 80 + .../v1_8/sp/relay/RelayWorldsQuery.java | 5 +- .../v1_8/sp/relay/RelayWorldsQueryImpl.java | 197 ++ .../relay/RelayWorldsQueryRateLimitDummy.java | 64 + .../v1_8/sp/relay/pkt/ICEServerSet.java | 55 - .../v1_8/sp/relay/pkt/IPacket.java | 165 -- .../v1_8/sp/relay/pkt/IPacket00Handshake.java | 57 - .../sp/relay/pkt/IPacket01ICEServers.java | 53 - .../sp/relay/pkt/IPacket03ICECandidate.java | 49 - .../sp/relay/pkt/IPacket04Description.java | 49 - .../relay/pkt/IPacketFEDisconnectClient.java | 67 - .../v1_8/sp/relay/pkt/IPacketFFErrorCode.java | 82 - .../v1_8/sp/server/EaglerChunkLoader.java | 4 +- .../server/EaglerIntegratedServerWorker.java | 49 +- .../v1_8/sp/server/EaglerMinecraftServer.java | 21 +- .../v1_8/sp/server/EaglerSaveFormat.java | 20 +- .../v1_8/sp/server/EaglerSaveHandler.java | 2 +- .../eaglercraft/v1_8/sp/server/WorldsDB.java | 32 + .../v1_8/sp/server/export/EPKCompiler.java | 97 +- .../sp/server/export/WorldConverterEPK.java | 5 +- .../sp/server/export/WorldConverterMCA.java | 25 +- .../v1_8/sp/server/skins/CustomSkullData.java | 29 +- .../server/skins/IntegratedCapePackets.java | 67 +- .../server/skins/IntegratedCapeService.java | 27 +- .../server/skins/IntegratedSkinPackets.java | 208 +- .../server/skins/IntegratedSkinService.java | 59 +- .../IntegratedServerPlayerNetworkManager.java | 5 +- .../protocol/ServerV3MessageHandler.java | 90 + .../protocol/ServerV4MessageHandler.java | 104 + .../server/voice/IntegratedVoiceService.java | 149 +- .../voice/IntegratedVoiceSignalPackets.java | 198 -- .../socket/NetHandlerSingleplayerLogin.java | 22 +- .../v1_8/touch_gui/EnumTouchControl.java | 579 ++++ .../v1_8/touch_gui/EnumTouchControlPos.java | 20 + .../v1_8/touch_gui/EnumTouchLayoutState.java | 28 + .../v1_8/touch_gui/TouchControlInput.java | 27 + .../v1_8/touch_gui/TouchControls.java | 164 ++ .../v1_8/touch_gui/TouchOverlayRenderer.java | 197 ++ .../v1_8/update/GuiUpdateDownloadSuccess.java | 60 + .../v1_8/update/GuiUpdateInstallOptions.java | 84 + .../v1_8/update/GuiUpdateVersionList.java | 13 +- .../v1_8/update/GuiUpdateVersionSlot.java | 5 +- .../v1_8/update/RelayUpdateChecker.java | 10 +- .../v1_8/update/UpdateDataObj.java | 28 + .../UpdateResultObj.java} | 53 +- .../v1_8/update/UpdateService.java | 23 +- .../eaglercraft/v1_8/voice/ExpiringSet.java | 6 +- .../eaglercraft/v1_8/voice/GuiVoiceMenu.java | 60 +- .../v1_8/voice/GuiVoiceOverlay.java | 11 +- .../v1_8/voice/VoiceClientController.java | 70 +- .../v1_8/voice/VoiceSignalPackets.java | 142 - .../v1_8/voice/VoiceTagRenderer.java | 2 +- .../v1_8/webview/GuiScreenPhishingWaring.java | 104 + .../webview/GuiScreenRecieveServerInfo.java | 203 ++ .../v1_8/webview/GuiScreenServerInfo.java | 128 + .../webview/GuiScreenServerInfoDesktop.java | 92 + .../v1_8/webview/PermissionsCache.java | 64 + .../v1_8/webview/ServerInfoCache.java | 130 + .../webview/WebViewOverlayController.java | 92 + sources/main/java/org/json/JSONArray.java | 330 ++- sources/main/java/org/json/JSONObject.java | 425 ++- .../org/json/JSONParserConfiguration.java | 26 + sources/main/java/org/json/JSONPointer.java | 12 + .../java/org/json/JSONPointerException.java | 11 + .../java/org/json/JSONPropertyIgnore.java | 6 +- .../main/java/org/json/JSONPropertyName.java | 7 +- sources/main/java/org/json/JSONString.java | 1 + sources/main/java/org/json/JSONTokener.java | 45 +- .../java/org/json/ParserConfiguration.java | 126 + .../v1_8/sp/relay/pkt/RelayPacket.java | 4 +- .../sp/relay/pkt/RelayPacket01ICEServers.java | 2 +- .../relay/pkt/RelayPacket07LocalWorlds.java | 2 +- sources/resources/SignedClientTemplate.txt | 229 +- sources/resources/assets/eagler/CREDITS.txt | 56 +- .../assets/eagler/audioctx_test_ogg.dat | Bin 0 -> 3980 bytes .../assets/eagler/audioctx_test_wav16.dat | Bin 0 -> 2106 bytes .../assets/eagler/audioctx_test_wav32f.dat | Bin 0 -> 4144 bytes .../eagler/boot_menu/boot_menu_markup.html | 88 + .../eagler/boot_menu/boot_menu_style.css | 328 +++ .../conf_template_eaglercraftX_1_8.json | 4 + ...conf_template_eaglercraftX_1_8_signed.json | 4 + .../conf_template_eaglercraft_1_5.json | 4 + .../conf_template_eaglercraft_1_5_legacy.json | 5 + .../conf_template_eaglercraft_b1_3.json | 5 + .../conf_template_peytonplayz585_a1_2_6.json | 4 + .../conf_template_peytonplayz585_b1_7_3.json | 4 + .../conf_template_peytonplayz585_indev.json | 4 + .../conf_template_standard_offline.json | 8 + .../eagler/boot_menu/meta_opts_templates.json | 192 ++ .../offline_template_eaglercraftX_1_8.html | 86 + ...template_eaglercraftX_1_8_fat_offline.html | 85 + ..._template_eaglercraftX_1_8_fat_signed.html | 268 ++ ...line_template_eaglercraftX_1_8_signed.html | 267 ++ .../offline_template_eaglercraft_1_5.html | 78 + ...fline_template_eaglercraft_1_5_legacy.html | 59 + .../offline_template_eaglercraft_b1_3.html | 59 + .../offline_template_peytonplayz585_a_b.html | 40 + ...offline_template_peytonplayz585_indev.html | 38 + .../offline_template_standard_offline.html | 77 + .../opts_template_eaglercraftX_1_8.txt | 66 + .../opts_template_eaglercraftX_1_8_demo.txt | 66 + ...template_eaglercraftX_1_8_html5Cursors.txt | 66 + .../opts_template_eaglercraft_1_5.txt | 52 + .../opts_template_eaglercraft_1_5_legacy.txt | 32 + ...ts_template_eaglercraft_1_5_livestream.txt | 63 + .../opts_template_peytonplayz585_a1_2_6.txt | 6 + .../opts_template_peytonplayz585_b1_7_3.txt | 6 + .../boot_menu/web_cl_eagleiii_8x16.woff | Bin 0 -> 9876 bytes .../assets/eagler/glsl/accel_font.fsh | 16 +- .../assets/eagler/glsl/accel_font.vsh | 23 +- .../assets/eagler/glsl/accel_particle.fsh | 16 +- .../assets/eagler/glsl/accel_particle.vsh | 29 +- sources/resources/assets/eagler/glsl/core.fsh | 70 +- sources/resources/assets/eagler/glsl/core.vsh | 28 +- .../eagler/glsl/deferred/forward_core.fsh | 16 +- .../glsl/deferred/reproject_control.fsh | 4 +- .../glsl/deferred/shader_pack_info.json | 2 +- .../eagler/glsl/deferred/ssao_generate.fsh | 2 +- .../glsl/dynamiclights/core_dynamiclights.fsh | 29 +- .../assets/eagler/glsl/gles2_compat.glsl | 98 + .../assets/eagler/glsl/hw_fingerprint.fsh | 55 + .../resources/assets/eagler/glsl/local.vsh | 14 +- .../assets/eagler/glsl/post_fxaa.fsh | 34 +- .../assets/eagler/glsl/texture_blit.fsh | 14 +- .../assets/eagler/glsl/texture_blit.vsh | 14 +- .../assets/eagler/glsl/texture_mix.fsh | 14 +- .../assets/eagler/gui/eagler_gui.png | Bin 8564 -> 11256 bytes .../assets/eagler/gui/notif_bk_large.png | Bin 0 -> 5089 bytes .../resources/assets/eagler/gui/touch_gui.png | Bin 0 -> 13590 bytes sources/resources/plugin_download.zip | Bin 256139 -> 540037 bytes sources/resources/plugin_version.json | 2 +- sources/resources/profanity_filter.wlist | 740 ++++++ sources/resources/relay_download.zip | Bin 267938 -> 234512 bytes sources/setup/eclipseProjectFiles/.classpath | 3 + sources/setup/eclipseProjectFiles/.project | 25 +- .../workspace_template/.gitignore.default | 3 +- sources/setup/workspace_template/build.gradle | 57 +- .../desktopRuntime/RTWebViewClient.html | 514 ++++ .../{libGLESv2.so => libGLESv2.so.2} | Bin sources/setup/workspace_template/gradlew | 294 ++- .../javascript/ES6ShimScript.txt | 31 + .../javascript/OfflineDownloadTemplate.txt | 29 +- .../javascript/SignedBundleTemplate.txt | 10 +- .../javascript/SignedClientTemplate.txt | 230 +- .../javascript/UpdateDownloadSources.txt | 19 + .../workspace_template/javascript/index.html | 26 +- .../v1_8/boot_menu/teavm/BootMenuAssets.java | 35 + .../boot_menu/teavm/BootMenuConstants.java | 53 + .../v1_8/boot_menu/teavm/BootMenuDOM.java | 203 ++ .../boot_menu/teavm/BootMenuDataManager.java | 464 ++++ .../boot_menu/teavm/BootMenuDatastore.java | 362 +++ .../boot_menu/teavm/BootMenuEntryPoint.java | 178 ++ .../teavm/BootMenuFatOfflineLoader.java | 93 + .../v1_8/boot_menu/teavm/BootMenuMain.java | 304 +++ .../boot_menu/teavm/BootMenuMetadata.java | 224 ++ .../boot_menu/teavm/BootableClientEntry.java | 366 +++ .../teavm/CheckboxListController.java | 108 + .../boot_menu/teavm/ClientBootFactory.java | 725 +++++ .../v1_8/boot_menu/teavm/ClientDataEntry.java | 152 ++ .../teavm/ConfirmationPopupController.java | 191 ++ .../boot_menu/teavm/EPKClientFactory.java | 100 + .../v1_8/boot_menu/teavm/EPKClientParser.java | 136 + .../v1_8/boot_menu/teavm/EPKDataEntry.java | 30 + .../boot_menu/teavm/EnumClientFormatType.java | 67 + .../boot_menu/teavm/EnumClientLaunchType.java | 86 + .../boot_menu/teavm/EnumOfflineParseType.java | 53 + .../teavm/FatOfflineDownloadFactory.java | 207 ++ .../teavm/IBootMenuConfigAdapter.java | 30 + .../boot_menu/teavm/IProgressMsgCallback.java | 22 + .../boot_menu/teavm/InputPopupController.java | 111 + .../v1_8/boot_menu/teavm/KeyCodes.java | 34 + .../boot_menu/teavm/LaunchConfigEntry.java | 143 + .../teavm/MenuPopupStateConfirmation.java | 141 + .../teavm/MenuPopupStateEditInteger.java | 114 + .../teavm/MenuPopupStateEditString.java | 109 + .../teavm/MenuPopupStateFileChooser.java | 106 + .../teavm/MenuPopupStateLoading.java | 98 + .../teavm/MenuPopupStateSelection.java | 141 + .../v1_8/boot_menu/teavm/MenuState.java | 135 + .../v1_8/boot_menu/teavm/MenuStateBoot.java | 583 ++++ .../teavm/MenuStateClientMultiSelect.java | 137 + .../teavm/MenuStateEditBootOrder.java | 268 ++ .../teavm/MenuStateEditingLaunch.java | 674 +++++ .../boot_menu/teavm/MenuStateEnterSetup.java | 261 ++ .../teavm/MenuStateImportMultiSelect.java | 144 + .../teavm/MenuStateSelectExportClients.java | 151 ++ .../teavm/OfflineDownloadFactory.java | 613 +++++ .../teavm/OfflineDownloadParser.java | 990 +++++++ .../boot_menu/teavm/RelayRandomizeHelper.java | 71 + .../teavm/SelectionListController.java | 240 ++ .../boot_menu/teavm/SignatureCheckHelper.java | 48 + .../teavm/SignedClientInstaller.java | 77 + .../v1_8/boot_menu/teavm/TemplateLoader.java | 77 + .../v1_8/boot_menu/teavm/TemplateParser.java | 260 ++ .../teavm/UnsignedBootException.java | 23 + .../teavm/java/com/jcraft/jogg/Buffer.java | 293 +++ .../teavm/java/com/jcraft/jogg/Packet.java | 45 + sources/teavm/java/com/jcraft/jogg/Page.java | 130 + .../java/com/jcraft/jogg/StreamState.java | 538 ++++ .../teavm/java/com/jcraft/jogg/SyncState.java | 273 ++ .../teavm/java/com/jcraft/jorbis/Block.java | 126 + .../java/com/jcraft/jorbis/CodeBook.java | 471 ++++ .../teavm/java/com/jcraft/jorbis/Comment.java | 240 ++ .../teavm/java/com/jcraft/jorbis/Drft.java | 1319 ++++++++++ .../java/com/jcraft/jorbis/DspState.java | 369 +++ .../teavm/java/com/jcraft/jorbis/Floor0.java | 332 +++ .../teavm/java/com/jcraft/jorbis/Floor1.java | 584 +++++ .../java/com/jcraft/jorbis/FuncFloor.java | 52 + .../java/com/jcraft/jorbis/FuncMapping.java | 45 + .../java/com/jcraft/jorbis/FuncResidue.java | 45 + .../java/com/jcraft/jorbis/FuncTime.java | 45 + .../teavm/java/com/jcraft/jorbis/Info.java | 468 ++++ .../java/com/jcraft/jorbis/InfoMode.java | 34 + .../com/jcraft/jorbis/JOrbisException.java | 40 + .../teavm/java/com/jcraft/jorbis/Lookup.java | 122 + sources/teavm/java/com/jcraft/jorbis/Lpc.java | 185 ++ sources/teavm/java/com/jcraft/jorbis/Lsp.java | 102 + .../java/com/jcraft/jorbis/Mapping0.java | 361 +++ .../teavm/java/com/jcraft/jorbis/Mdct.java | 249 ++ .../teavm/java/com/jcraft/jorbis/PsyInfo.java | 74 + .../teavm/java/com/jcraft/jorbis/PsyLook.java | 42 + .../java/com/jcraft/jorbis/Residue0.java | 326 +++ .../java/com/jcraft/jorbis/Residue1.java | 44 + .../java/com/jcraft/jorbis/Residue2.java | 41 + .../com/jcraft/jorbis/StaticCodeBook.java | 436 +++ .../teavm/java/com/jcraft/jorbis/Time0.java | 52 + .../teavm/java/com/jcraft/jorbis/Util.java | 30 + .../java/com/jcraft/jorbis/VorbisFile.java | 1348 ++++++++++ .../v1_8/internal/OpenGLObjects.java | 113 +- .../v1_8/internal/PlatformApplication.java | 307 ++- .../v1_8/internal/PlatformAssets.java | 66 +- .../v1_8/internal/PlatformAudio.java | 315 ++- .../v1_8/internal/PlatformFilesystem.java | 331 +-- .../v1_8/internal/PlatformInput.java | 2154 +++++++++++++-- .../v1_8/internal/PlatformNetworking.java | 182 +- .../v1_8/internal/PlatformOpenGL.java | 337 ++- .../v1_8/internal/PlatformRuntime.java | 1028 ++++++-- .../v1_8/internal/PlatformScreenRecord.java | 286 ++ .../v1_8/internal/PlatformUpdateSvc.java | 43 +- .../v1_8/internal/PlatformVoiceClient.java | 265 +- .../v1_8/internal/PlatformWebRTC.java | 1009 ++----- .../v1_8/internal/PlatformWebView.java | 639 +++++ .../buffer/EaglerArrayByteBuffer.java | 118 +- .../buffer/EaglerArrayFloatBuffer.java | 87 +- .../internal/buffer/EaglerArrayIntBuffer.java | 87 +- .../buffer/EaglerArrayShortBuffer.java | 87 +- .../teavm/AdvancedHTMLIFrameElement.java | 120 + .../teavm/ArrayBufferInputStream.java | 37 +- .../internal/teavm/Base64VarIntArray.java | 129 + .../v1_8/internal/teavm/ClassesJSLocator.java | 94 + .../v1_8/internal/teavm/ClientMain.java | 334 ++- .../internal/teavm/DebugConsoleWindow.java | 91 +- .../v1_8/internal/teavm/ES6ShimStatus.java | 82 + .../v1_8/internal/teavm/ES6ShimStatusJS.java | 52 + .../v1_8/internal/teavm/EarlyLoadScreen.java | 220 +- .../internal/teavm/EnumES6ShimStatus.java | 56 + .../v1_8/internal/teavm/EnumES6Shims.java | 58 + .../internal/teavm/FixWebMDurationJS.java | 20 + .../teavm/IFrameSafetyException.java} | 33 +- .../internal/teavm/ImmediateContinue.java | 26 + .../internal/teavm/IndexedDBFilesystem.java | 365 +++ .../v1_8/internal/teavm/InputEvent.java | 29 + .../teavm/JOrbisAudioBufferDecoder.java | 420 +++ .../teavm/LegacyKeycodeTranslator.java | 328 +++ .../v1_8/internal/teavm/MessageChannel.java | 37 + .../v1_8/internal/teavm/OffsetTouch.java | 43 + .../v1_8/internal/teavm/PCMToWAVLoader.java | 118 + .../v1_8/internal/teavm/SortedTouchEvent.java | 85 + .../internal/teavm/TeaVMBlobURLHandle.java | 28 + .../internal/teavm/TeaVMBlobURLManager.java | 188 ++ .../teavm/TeaVMClientConfigAdapter.java | 243 +- .../teavm/TeaVMClientConfigAdapterHooks.java | 19 + .../internal/teavm/TeaVMDataURLManager.java | 87 + .../teavm/TeaVMEnterBootMenuException.java | 20 + .../v1_8/internal/teavm/TeaVMFetchJS.java | 44 + .../teavm/TeaVMRuntimeDeobfuscator.java | 242 ++ .../internal/teavm/TeaVMUpdateThread.java | 20 +- .../v1_8/internal/teavm/TeaVMUtils.java | 320 +-- .../internal/teavm/TeaVMWebSocketClient.java | 125 + .../internal/teavm/TeaVMWebSocketFrame.java | 114 + .../v1_8/internal/teavm/Touch.java | 67 + .../v1_8/internal/teavm/TouchEvent.java | 44 + .../v1_8/internal/teavm/TouchList.java | 28 + .../v1_8/internal/teavm/VisualViewport.java | 45 + .../teavm/WebGLANGLEInstancedArrays.java | 28 + .../v1_8/internal/teavm/WebGLBackBuffer.java | 300 +++ .../teavm/WebGLOESVertexArrayObject.java | 28 + .../TeaVMRuntimeDeobfuscatorGenerator.java | 108 + .../generators/TeaVMUtilsUnwrapGenerator.java | 166 ++ .../teavm/opts/JSEaglercraftXOptsHooks.java | 3 + .../teavm/opts/JSEaglercraftXOptsRoot.java | 72 + .../teavm/opts/JSEaglercraftXOptsServer.java | 3 + .../internal/ClientPlatformSingleplayer.java | 179 +- .../internal/ServerPlatformSingleplayer.java | 207 +- .../internal/teavm/SingleThreadWorker.java | 44 + .../sp/server/internal/teavm/WorkerMain.java | 2 +- .../org/teavm/backend/javascript/long.js | 693 +++++ 683 files changed, 62074 insertions(+), 8996 deletions(-) create mode 100644 CODE_STANDARDS.md create mode 100644 buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/ES6Compat.java create mode 100644 patches/minecraft/net/minecraft/client/gui/ChatLine.edit.java create mode 100644 patches/minecraft/net/minecraft/client/gui/ScaledResolution.edit.java create mode 100644 patches/minecraft/net/minecraft/entity/ai/EntitySenses.edit.java delete mode 100644 patches/minecraft/net/minecraft/profiler/Profiler.edit.java create mode 100644 sources/lwjgl/java/fi/iki/elonen/NanoHTTPD.java create mode 100644 sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformScreenRecord.java create mode 100644 sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformWebView.java delete mode 100644 sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/WebSocketServerQuery.java create mode 100644 sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketClient.java create mode 100644 sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketFrameBinary.java rename sources/{main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket70SpecialUpdate.java => lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketFrameString.java} (50%) create mode 100644 sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewHTTPD.java create mode 100644 sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewProtocol.java create mode 100644 sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewServer.java create mode 100644 sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewWSD.java rename sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/{WebSocketPlayClient.java => lwjgl/WebSocketClientImpl.java} (57%) create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/ClientUUIDLoadingCache.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/Filesystem.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/Gamepad.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/HashKey.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/PauseMenuCustomizeState.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/PointerInputAbstraction.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/Touch.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/boot_menu/GuiScreenEnterBootMenu.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenInspectSessionToken.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenRevokeSessionToken.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenSendRevokeRequest.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/HardwareFingerprint.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/ServerCookieDataStore.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/crypto/AESLightEngine.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/AbstractWebSocketClient.java rename sources/{teavm/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformBufferFunctions.java => main/java/net/lax1dude/eaglercraft/v1_8/internal/EaglerMissingResourceException.java} (63%) create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumFireKeyboardEvent.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumFireMouseEvent.java rename sources/main/java/net/lax1dude/eaglercraft/v1_8/{sp/relay/pkt/IPacket05ClientSuccess.java => internal/EnumTouchEvent.java} (55%) create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumWebViewContentMode.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/GamepadConstants.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IEaglerFilesystem.java rename sources/main/java/net/lax1dude/eaglercraft/v1_8/{sp/relay/pkt/IPacket07LocalWorlds.java => internal/IWebSocketClient.java} (50%) rename sources/{lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformBufferFunctions.java => main/java/net/lax1dude/eaglercraft/v1_8/internal/IWebSocketFrame.java} (62%) create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/RamdiskFilesystemImpl.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/ScreenRecordParameters.java rename sources/main/java/net/lax1dude/eaglercraft/v1_8/{sp/relay/pkt/IPacket69Pong.java => internal/VFSFilenameIteratorNonRecursive.java} (53%) create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/WebViewOptions.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EnumInputEvent.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/FontMappingHelper.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiButtonWithStupidIcons.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiScreenVisualViewport.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/CachedNotifBadgeTexture.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ClickEventZone.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiButtonNotifBell.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiScreenNotifications.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiSlotNotifications.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/NotificationBadge.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/NotificationIcon.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ServerNotificationManager.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ServerNotificationRenderer.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GLSLHeader.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SoftGLBufferArray.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SoftGLBufferState.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/TextureFormatHelper.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/VSHInputLayoutParser.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/GuiScreenContentWarning.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/LookAlikeUnicodeConv.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/ProfanityFilter.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/EnumScreenRecordingCodec.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenRecordingNote.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenRecordingSettings.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenSelectCodec.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiSlotSelectCodec.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/ScreenRecordingController.java rename sources/{teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMServerQuery.java => main/java/net/lax1dude/eaglercraft/v1_8/socket/ServerQueryImpl.java} (64%) create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/WebSocketNetworkManager.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/ClientV3MessageHandler.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/ClientV4MessageHandler.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/GameProtocolMessageController.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/IPluginMessageSendFunction.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/PacketBufferInputWrapper.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/PacketBufferOutputWrapper.java delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiIntegratedServerStartup.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenRAMDiskModeDetected.java rename sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/{IPCPacket20LoggerMessage.java => IPCPacket1ALoggerMessage.java} (86%) rename sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/{IPCPacket21EnableLogging.java => IPCPacket1BEnableLogging.java} (87%) create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket1CIssueDetected.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayLoggerImpl.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQueryImpl.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQueryRateLimitDummy.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerRateLimitTracker.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocketImpl.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocketRateLimitDummy.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQueryImpl.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQueryRateLimitDummy.java delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/ICEServerSet.java delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket.java delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket00Handshake.java delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket01ICEServers.java delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket03ICECandidate.java delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket04Description.java delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacketFEDisconnectClient.java delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacketFFErrorCode.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/WorldsDB.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/protocol/ServerV3MessageHandler.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/protocol/ServerV4MessageHandler.java delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/voice/IntegratedVoiceSignalPackets.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchControl.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchControlPos.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchLayoutState.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchControlInput.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchControls.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchOverlayRenderer.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateDownloadSuccess.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateInstallOptions.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateDataObj.java rename sources/main/java/net/lax1dude/eaglercraft/v1_8/{sp/relay/pkt/IPacket06ClientFailure.java => update/UpdateResultObj.java} (53%) delete mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceSignalPackets.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenPhishingWaring.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenRecieveServerInfo.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenServerInfo.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenServerInfoDesktop.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/PermissionsCache.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/ServerInfoCache.java create mode 100644 sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/WebViewOverlayController.java create mode 100644 sources/main/java/org/json/JSONParserConfiguration.java create mode 100644 sources/main/java/org/json/ParserConfiguration.java create mode 100644 sources/resources/assets/eagler/audioctx_test_ogg.dat create mode 100644 sources/resources/assets/eagler/audioctx_test_wav16.dat create mode 100644 sources/resources/assets/eagler/audioctx_test_wav32f.dat create mode 100644 sources/resources/assets/eagler/boot_menu/boot_menu_markup.html create mode 100644 sources/resources/assets/eagler/boot_menu/boot_menu_style.css create mode 100644 sources/resources/assets/eagler/boot_menu/conf_template_eaglercraftX_1_8.json create mode 100644 sources/resources/assets/eagler/boot_menu/conf_template_eaglercraftX_1_8_signed.json create mode 100644 sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_1_5.json create mode 100644 sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_1_5_legacy.json create mode 100644 sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_b1_3.json create mode 100644 sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_a1_2_6.json create mode 100644 sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_b1_7_3.json create mode 100644 sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_indev.json create mode 100644 sources/resources/assets/eagler/boot_menu/conf_template_standard_offline.json create mode 100644 sources/resources/assets/eagler/boot_menu/meta_opts_templates.json create mode 100644 sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8.html create mode 100644 sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_fat_offline.html create mode 100644 sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_fat_signed.html create mode 100644 sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_signed.html create mode 100644 sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_1_5.html create mode 100644 sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_1_5_legacy.html create mode 100644 sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_b1_3.html create mode 100644 sources/resources/assets/eagler/boot_menu/offline_template_peytonplayz585_a_b.html create mode 100644 sources/resources/assets/eagler/boot_menu/offline_template_peytonplayz585_indev.html create mode 100644 sources/resources/assets/eagler/boot_menu/offline_template_standard_offline.html create mode 100644 sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8.txt create mode 100644 sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8_demo.txt create mode 100644 sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8_html5Cursors.txt create mode 100644 sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5.txt create mode 100644 sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5_legacy.txt create mode 100644 sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5_livestream.txt create mode 100644 sources/resources/assets/eagler/boot_menu/opts_template_peytonplayz585_a1_2_6.txt create mode 100644 sources/resources/assets/eagler/boot_menu/opts_template_peytonplayz585_b1_7_3.txt create mode 100644 sources/resources/assets/eagler/boot_menu/web_cl_eagleiii_8x16.woff create mode 100644 sources/resources/assets/eagler/glsl/gles2_compat.glsl create mode 100644 sources/resources/assets/eagler/glsl/hw_fingerprint.fsh create mode 100644 sources/resources/assets/eagler/gui/notif_bk_large.png create mode 100644 sources/resources/assets/eagler/gui/touch_gui.png create mode 100644 sources/resources/profanity_filter.wlist create mode 100644 sources/setup/workspace_template/desktopRuntime/RTWebViewClient.html rename sources/setup/workspace_template/desktopRuntime/{libGLESv2.so => libGLESv2.so.2} (100%) create mode 100644 sources/setup/workspace_template/javascript/ES6ShimScript.txt create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/BootMenuAssets.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/BootMenuConstants.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/BootMenuDOM.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/BootMenuDataManager.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/BootMenuDatastore.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/BootMenuEntryPoint.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/BootMenuFatOfflineLoader.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/BootMenuMain.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/BootMenuMetadata.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/BootableClientEntry.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/CheckboxListController.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/ClientBootFactory.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/ClientDataEntry.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/ConfirmationPopupController.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/EPKClientFactory.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/EPKClientParser.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/EPKDataEntry.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/EnumClientFormatType.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/EnumClientLaunchType.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/EnumOfflineParseType.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/FatOfflineDownloadFactory.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/IBootMenuConfigAdapter.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/IProgressMsgCallback.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/InputPopupController.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/KeyCodes.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/LaunchConfigEntry.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuPopupStateConfirmation.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuPopupStateEditInteger.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuPopupStateEditString.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuPopupStateFileChooser.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuPopupStateLoading.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuPopupStateSelection.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuState.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuStateBoot.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuStateClientMultiSelect.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuStateEditBootOrder.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuStateEditingLaunch.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuStateEnterSetup.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuStateImportMultiSelect.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/MenuStateSelectExportClients.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/OfflineDownloadFactory.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/OfflineDownloadParser.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/RelayRandomizeHelper.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/SelectionListController.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/SignatureCheckHelper.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/SignedClientInstaller.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/TemplateLoader.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/TemplateParser.java create mode 100644 sources/teavm-boot-menu/java/net/lax1dude/eaglercraft/v1_8/boot_menu/teavm/UnsignedBootException.java create mode 100644 sources/teavm/java/com/jcraft/jogg/Buffer.java create mode 100644 sources/teavm/java/com/jcraft/jogg/Packet.java create mode 100644 sources/teavm/java/com/jcraft/jogg/Page.java create mode 100644 sources/teavm/java/com/jcraft/jogg/StreamState.java create mode 100644 sources/teavm/java/com/jcraft/jogg/SyncState.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Block.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/CodeBook.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Comment.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Drft.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/DspState.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Floor0.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Floor1.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/FuncFloor.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/FuncMapping.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/FuncResidue.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/FuncTime.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Info.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/InfoMode.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/JOrbisException.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Lookup.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Lpc.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Lsp.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Mapping0.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Mdct.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/PsyInfo.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/PsyLook.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Residue0.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Residue1.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Residue2.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/StaticCodeBook.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Time0.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/Util.java create mode 100644 sources/teavm/java/com/jcraft/jorbis/VorbisFile.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformScreenRecord.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformWebView.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/AdvancedHTMLIFrameElement.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/Base64VarIntArray.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/ClassesJSLocator.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/ES6ShimStatus.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/ES6ShimStatusJS.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/EnumES6ShimStatus.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/EnumES6Shims.java rename sources/{main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket02NewClient.java => teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/IFrameSafetyException.java} (64%) create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/ImmediateContinue.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/IndexedDBFilesystem.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/InputEvent.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/JOrbisAudioBufferDecoder.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/LegacyKeycodeTranslator.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/MessageChannel.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/OffsetTouch.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/PCMToWAVLoader.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/SortedTouchEvent.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMBlobURLHandle.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMBlobURLManager.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMDataURLManager.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMEnterBootMenuException.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMFetchJS.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMRuntimeDeobfuscator.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMWebSocketClient.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMWebSocketFrame.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/Touch.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TouchEvent.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TouchList.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/VisualViewport.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/WebGLANGLEInstancedArrays.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/WebGLBackBuffer.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/WebGLOESVertexArrayObject.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/generators/TeaVMRuntimeDeobfuscatorGenerator.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/generators/TeaVMUtilsUnwrapGenerator.java create mode 100644 sources/teavm/java/net/lax1dude/eaglercraft/v1_8/sp/server/internal/teavm/SingleThreadWorker.java create mode 100644 sources/teavmc-classpath/resources/org/teavm/backend/javascript/long.js diff --git a/CODE_STANDARDS.md b/CODE_STANDARDS.md new file mode 100644 index 00000000..7f14d0ed --- /dev/null +++ b/CODE_STANDARDS.md @@ -0,0 +1,306 @@ +# Eaglercraft Code Standards + +**These are some basic rules to follow if you would like to write code that is consistent with the Eaglercraft 1.8 codebase. If you are already familiar with Eaglercraft 1.5 or b1.3, please abandon whatever you think is the best practice as a result of reading that code, those clients should be considered as obsolete prototypes.** + +## Part A. Coding Style + +### 1. Tabs, not spaces + +Tabs not spaces, it makes indentation easier to manage and reduces file size. Other popular projects that are also known to use tabs instead of spaces include the linux kernel. We prefer to set tab width to 4 spaces on our editors. + +Format code like the eclipse formatter on factory settings + +### 2. Avoid redundant hash map lookups + +Don't retrieve the same value from a hash map more than once, that includes checking if an entry exists first before retrieving its value. If you do this, you are a horrible person! + +**Incorrect:** + +```java +if(hashMap.containsKey("eagler")) { + Object val = hashMap.get("eagler"); + // do something with val +} +``` + +**Correct:** + +```java +Object val = hashMap.get("eagler"); +if(val != null) { + // do something with val +} +``` + +### 3. Cache the return value of a function if you plan to use it multiple times + +This is somewhat an extension of rule #2, don't repeatedly call the same function multiple times if there's no reason to, even if its a relatively fast function. Everything is slower and less efficient in a browser. + +**Incorrect:** + +```java +while(itr.hasNext()) { + if(!Minecraft.getMinecraft().getRenderManager().getEntityClassRenderObject(SomeEntity.class).shouldRender(itr.next())) { + itr.remove(); + } +} +``` + +**Correct:** + +```java +Render render = Minecraft.getMinecraft().getRenderManager().getEntityClassRenderObject(SomeEntity.class); +while(itr.hasNext()) { + if(!render.shouldRender(itr.next())) { + itr.remove(); + } +} +``` + +### 4. Iterators aren't that great + +Avoid using iterators when possible, this includes a `for(Item item : list)` type loop, since this may compile into bytecode that uses an iterator. If the list is a linked list or some other type of data structure that can’t perform random access efficiently, then it is recommended to use an iterator, but if the collection is guaranteed to be something similar to an ArrayList then implement it via a traditional for loop instead. + +**Recommended way to iterate an ArrayList:** + +```java +for(int i = 0, l = list.size(); i < l; ++i) { + Item item = list.get(i); + // do something +} +``` + +### 5. Don't shit on the heap + +Avoid creating temporary single-use objects in performance critical code, since the overhead of doing so is larger in a browser where there’s no type safety to predefine object structures. This includes using lambdas or using most of the stuff in the google guava package. Also this is partially why I prefer not using iterators whenever possible. + +**Incorrect, creates 5 temporary objects:** + +```java +List list1 = Arrays.asList("eagler", "eagler", "deevis"); +List list2 = Lists.newArrayList( + Collections2.transform( + Collections2.filter( + list1, + (e) -> !e.equals("deevis") + ), + (e) -> (e + "!") + ) +); +``` + +**Correct, creates no temporary objects:** + +```java +List list1 = Arrays.asList("eagler", "eagler", "deevis"); +List list2 = Lists.newArrayList(); +for(int i = 0, l = list1.size(); i < l; ++i) { + String s = list1.get(i); + if(!s.equals("deevis")) { + list2.add(s + "!"); + } +} +``` + +(note: we are ignoring the StringBuilder instances that the compiler generates from ` + "!"`) + +### 6. Don't base game/render logic off of the system time + +Use `EagRuntime.steadyTimeMillis()` instead to access a monotonic clock, as in a clock that is guaranteed to only run forwards, and is not affected by changes in the system time. `System.currentTimeMillis()` should only be used in situations where you want to know the actual wall time or are measuring elapsed time across multiple page refreshes. + +### 7. Prefer multiplication over division + +If you're always gonna divide a number by some constant, it is better to multiply it by one-over-the-constant instead. + +**Incorrect** + +```java +float b = a / 50.0f; +``` + +**Correct** + +```java +float b = a * 0.02f; +``` + +### 8. Shaders should take advantage of compiler intrinsics + +Although you may think these two pieces of code are identical, its more than likely that the "Correct" example will compile to a more efficient shader on almost any hardware. The functions in GLSL are not a library, they are compiler intrinsics that usually compile to inline assembly that can take advantage of different acceleration instructions in the GPU's instruction set. Vector math should be done in ways that promotes the use of SIMD instructions when the code is compiled to a shader. + +**Incorrect:** + +```glsl +float dx = pos1.x - pos2.x; +float dy = pos1.y - pos2.y; +float dz = pos1.z - pos2.z; +float distance = sqrt(dx * dx + dy * dy + dz * dz); +float fogDensity = pow(2.718, -density * distance); +``` + +**Correct:** + +```glsl +float fogDensity = exp(-density * length(pos1.xyz - pos2.xyz)); +``` + +### 9. Flatten the control flow of shaders + +Modern GPUs are able to execute multiple instances of a shader on a single core, but if one of those shaders encounters a branch (if statement, or related) that causes it to begin executing different code from the other instances of the shader running on that core, that instance of the shader can no longer be executed at the same time as the other instances, and suddenly you've significantly increased the amount of time this core will now be busy executing shader instructions to account for all of the branches the different shader instances have taken. + +**Incorrect:** + +```glsl +float lightValue = dot(lightDirection, normal); +if(lightValue > 0.0) { + color += lightValue * lightColor * diffuseColor; +} +``` + +**Correct:** +```glsl +float lightValue = max(dot(lightDirection, normal), 0.0); +color += lightValue * lightColor * diffuseColor; +``` + +### 10. Use textureLod unless mipmapping is necessary + +This will prevent the shader from wasting time trying to determine what mipmap levels to read from when the texture is sampled. + +**Incorrect:** + +```glsl +float depthValue = texture(depthBuffer, pos).r; +``` + +**Correct:** + +```glsl +float depthValue = textureLod(depthBuffer, pos, 0.0).r; +``` + +### 11. Divide complex and branch-intensive shaders into multiple draw calls + +You can use a variety of different blending modes to mathematically combine the results of shaders. This is done for the same reason as flattening the control flow, to try and keep instruction pointers in sync by periodically resetting their positions, and also to allow for the driver to multitask better on GPUs with insane numbers of cores. It also allows the shader’s execution to be distributed across multiple frames in the case of something that doesn’t need to update often (like clouds). + + +### 12. Don't abuse `@JSBody` in TeaVM code + +TeaVM provides lots of ways of interacting with JavaScript, using `@JSBody` is not the only way, consider using an overlay type. + +**Incorrect** + +```java +@JSObject(params = { "obj" }, script = "return obj.valueA;") +public static native JSObject getValueA(JSObject obj); + +@JSObject(params = { "obj" }, script = "return obj.valueB;") +public static native JSObject getValueB(JSObject obj); + +@JSObject(params = { "obj" }, script = "return obj.valueC;") +public static native JSObject getValueC(JSObject obj); + +@JSObject(params = { "obj" }, script = "obj.dumbFunction();") +public static native void callDumbFunction(JSObject obj); +``` + +**Correct** + +```java +public interface MyObject extends JSObject { + + @JSProperty + JSObject getValueA(); + + @JSProperty + JSObject getValueB(); + + @JSProperty + JSObject getValueC(); + + void dumbFunction(); + +} +``` + +### 13. Don't fall for TeaVM's threads + +It is impossible to have multithreading in JavaScript, only worker objects can be used to execute code concurrently, which can't share javascript variables. Therefore, when you create a thread in TeaVM, you're creating a virtual thread that isn't capable of running at the same time as any other virtual thread in the TeaVM context. This means it's impossible to speed a TeaVM program up through the use of multiple Java threads, instead it is more than likely that it will just slow the program down more to implement multithreading through TeaVM's threads due to the additional time required for synchronization and context switches. Its more efficient to just program the entire application to be single threaded to begin with, just put everything in the main loop and realize that if it was in a different thread it would just periodically interrupt the main loop. + +### 14. Always use try-with-resources + +For any code that deals with streams to be considered safe, it should either use a try-with-resources or try/finally in order to release resources when complete, since otherwise the stream might not close if an IO error causes the function to return early. This is especially important for plugin code since its supposed to be able to run on a large server for weeks at a time without the underlying JVM being restarted. If hackers discover a bug in the code to cause a function to return early like this without closing a stream, they might exploit it to fatally crash the server by spamming whatever corrupt packet causes the function to leak the stream, so all code must be written so it can fail at any time without leaking resources. + +**Incorrect** + +```java +InputStream is = new FileInputStream(new File("phile.txt")); +is.write(someArray); +is.close(); +``` + +**Correct** + +```java +try(InputStream is = new FileInputStream(new File("phile.txt"))) { + is.write(someArray); +} +``` + +Notice that the `.close()` can be omitted completely when using a try-with-resources + +### 15. Always close compression/decompression streams + +In the desktop runtime, the default oracle JDK uses native code to implement the compression/decompression streams (InflaterInputStream, GZIPInputStream, etc) and therefore if you forget to close the compression/decompression stream it will cause a memory leak when the code isn't running in a browser. This is a common issue when using byte array input/output streams since you might believe when decompressing data from a byte array that there's no reason to close the stream when you're done since its not a file, but that will still cause a memory leak due to the decompression stream not being cleaned up. + +## Part B. Project Structure + +### 1. Code decompiled from Minecraft goes in `src/game/java` + +Don't add any new classes to `src/game/java`, and ideally any significant additions to the game's source (functions, etc) should be done through creating new classes in `src/main/java` instead of adding it directly to the decompiled classes. + +### 2. Do not put platform-dependent code in `src/main/java` or `src/game/java` + +One of the objectives of Eaglercraft is to make Minecraft Java edition truly cross platform, why stop at just a desktop and JavaScript runtime? There are plans to create an Android runtime and several WebAssembly runtimes, all of which will be compatible with any pre-existing eaglercraft clients that only depend on the EaglercraftX runtime library and don't directly depend on components of TeaVM or LWJGL. Ideally, all core features of the client should be implemented in the `src/main/java` and `src/game/java` and any platform-dependent features should be stubbed out in some abstract platform-independent way in classes in the `src/teavm/java` and `src/lwjgl/java` and any other future runtime you want your client to support. Ideally, every source folder of platform-dependent code should expose an identical API for access to the platform-independent code as all the other platform-dependant code folders currently expose. + +### 3. Don't mix JavaScript with Java + +Don’t implement features in the JavaScript runtime by requiring additional JavaScript files be included on index.html, if you must access browser APIs then use the TeaVM JSO to write your code in Java instead so it’s baked directly into classes.js. Certain browser APIs may be missing from the default TeaVM JSO-APIs library but it is not difficult to create the overlay types for them manually. Clients that violate this rule may also not possible to automatically import into the EaglercraftX boot menu depending on how fucked up they are. There aren't any limitations to the TeaVM JSO that give you a good enough excuse not to follow this rule. + +### 4. Don't access the classes named "Platform\*" directly from your platform-independent code + +Much like the Java runtime environment itself, Eaglercraft's runtime library consists of two layers, the internal classes full of platform-dependent code that expose an intermediate API not meant to be used by programmers directly, and the platform-independent API classes that provide a platform-independent wrapper for the platform dependent classes and also provide all the miscellaneous utility functions that don't require platform dependent code to be implemented. Chances are if you are directly using a function on a class that has a name that starts with "Platform\*", that there is a different class in `src/main/java` that you are meant to use in order to access that feature, that may perform additional checks or adjust the values you are passing to the function before calling the function in the Platform class. + +## Part C. Compatibility Standards + +### 1. Target minimum JDK version is Java 8 + +Its difficult to find a platform where its not possible to run Java 8 in some capacity, therefore the desktop runtime of EaglercraftX and the BungeeCord plugin should target Java 8. The Velocity plugin is an exception since Velocity itself doesn't support Java 8 either. + +### 2. Target minimum supported browser is Google Chrome 38 + +Released on October 7, 2014, we think its a good target for the JavaScript versions of EaglercraftX. This is the last version of Chrome that supports hardware accelerated WebGL 1.0 on Windows XP. All base features of the underlying Minecraft 1.8 client must be functional, however things such as EaglercraftX's shaders or dynamic lighting are not required to work. The client cannot crash as a result of any missing features on an old browser, you must either implement fallbacks or safely disable the unsupported features. + +### 3. Target minimum supported graphics API is OpenGL ES 2.0 (WebGL 1.0) + +The most widely supported graphics API in the world is currently OpenGL ES 2.0, so ideally that should be the target for EaglercraftX 1.8. We can guarantee the client will be on an OpenGL ES 3.0 context 99% of the time, however its not that hard to also maintain support for GLES 2.0 (WebGL 1.0) as well with slightly reduced functionality so we might as well make it a feature in case of the 1% of the time that functionality is not available. The client cannot depend on any GL extensions in order to run in GLES 2.0 mode, however its reasonable to assume there will be VAO support via extensions in most GLES 2.0 contexts so the client includes an abstraction layer (via EaglercraftGPU.java) to seamlessly emulate VAO functionality even when the client is running in GLES 2.0 mode with no VAO extensions. The only core feature of Minecraft 1.8 that is completely unavailable in GLES 2.0 mode is mip-mapping for the blocks/items texture atlas due to being unable to limit the max mipmap level. + +### 4. Use preprocessor directives to make portable shaders that can be compiled for both OpenGL ES 2.0 and 3.0 contexts + +Most of the shaders in the base "glsl" directory of the resources EPK file use a file called "gles2_compat.glsl" to polyfill certain GLSL features (such as input/output declarations) via preprocessor directives to allow them to be compiled on both OpenGL ES 3.0 and 2.0 contexts. This is the preferred way to implement backwards compatibility over creating seprate versions of the same shaders, since future developers don't need to waste time maintaining multiple versions of the same code if they don't really care about backwards compatibility in the first place. + +### 5. Target minimum version of the JavaScript syntax is ES5 strict mode + +A shim is included to provide certain ES6 functions, however you should always program with syntax compatible with ES5, so the script doesn't crash immediately due to syntax errors even if the functions that use unsupported syntax aren't actually being called. `build.gradle` currently patches out all the ES5 strict mode incompatible syntax in the output of TeaVM 0.9.2, but this will probably break if you try to update TeaVM. Don't worry though because future WASM versions of EaglercraftX will use the latest versions of TeaVM. **Some common incompatible syntax to avoid includes `const`, `let`, `async`, `( ) => `, and using named functions! You can't do any of these things in your JSBody annotations.** + +### 6. You cannot depend on any deprecated browser features + +The same way we want EaglercraftX to work on browsers from over 10 years ago, we want it to still work in browsers 10 years from today, therefore the client cannot depend on any deprecated browser features in order for all the base Minecraft 1.8 game's features to work properly. However it is okay to use deprecated features as fallback if any modern non-deprecated feature (such as keyboard event handling) that the game needs if the game is running in an old browser. + +### 7. Always use addEventListener to register event handlers + +Always use addEventListener to register event handlers for browser APIs, never through the use of assigning the legacy "on\*" (onclick, onkeydown, onmessage, etc) variables, the TeaVMUtils class has a universal helper function for accessing addEventListener on any JSO objects that don’t already implement the function. + +### 8. JavaScript should be executed in strict mode + +Always make sure your JavaScript files start with `"use strict";`, be careful when adding this to your code retroactively because it will probably break hastily written code unless you haven’t made a single typo that’s not forbidden in strict mode. Be aware that in Chrome 38 this means you can't use stuff such as `const` and `let` or named functions in any of your JSBody annotations! diff --git a/CREDITS b/CREDITS index 94adfa4c..c1ec6e60 100644 --- a/CREDITS +++ b/CREDITS @@ -3,24 +3,26 @@ ~~~~~~~~~~~~~~~~~~~~~~~ lax1dude: - + - Creator of Eaglercraft - Ported the Minecraft 1.8 src to TeaVM - Wrote HW accelerated OpenGL 1.3 emulator - Wrote the default shader pack - Made the integrated PBR resource pack + - Added touch and mobile device support - Wrote all desktop emulation code - Wrote EaglercraftXBungee - Wrote EaglercraftXVelocity - Wrote WebRTC relay server - Wrote voice chat server - Wrote the patch and build system - + ayunami2000: - + - Many bug fixes - WebRTC LAN worlds - WebRTC voice chat + - Worked on touch support - Made velocity plugin work - Added resource packs - Added screen recording @@ -410,7 +412,7 @@ Project Author: The Legion of the Bouncy Castle Project URL: https://www.bouncycastle.org/java.html - Used For: MD5, SHA-1, SHA-256 implementations + Used For: MD5, SHA-1, SHA-256, and AES implementations * Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) * @@ -668,23 +670,23 @@ Project Author: ymnk, JCraft Inc. Project URL: http://www.jcraft.com/jorbis/ - Used For: Audio in desktop runtime + Used For: Audio in desktop runtime and browsers that don't support OGG * JOrbis * Copyright (C) 2000 ymnk, JCraft,Inc. - * + * * Written by: 2000 ymnk * * Many thanks to * Monty and * The XIPHOPHORUS Company http://www.xiph.org/ . * JOrbis has been based on their awesome works, Vorbis codec. - * + * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * as published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. - + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the @@ -696,6 +698,44 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + Project Name: NanoHTTPD + Project Author: NanoHTTPD + Project URL: http://nanohttpd.org/ + + Used For: HTTP server in the desktop runtime + + * Copyright (c) 2012-2013 by Paul S. Hawke, + * 2001,2005-2013 by Jarno Elonen, + * 2010 by Konstantinos Togias All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of the NanoHttpd organization nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + Project Name: sqlite-jdbc Project Author: Taro L. Saito (xerial) Project URL: https://github.com/xerial/sqlite-jdbc diff --git a/README.md b/README.md index d8b5a8a2..5fcdc4b9 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,13 @@ 5. Type `./CompileLatestClient.sh` and hit enter, a GUI resembling a classic windows installer should open 6. Follow the steps shown to you in the new window to finish compiling +## Browser Compatibility + +EaglercraftX 1.8 is currently known to work on browsers as old as Chrome 38 on Windows XP, the game supports both WebGL 1.0 and WebGL 2.0 however features such as dynamic lighting and PBR shaders require WebGL 2.0. The game also supports mobile browsers that don't have a keyboard or mouse, the game will enter touch screen mode automatically when touch input is detected. The game also includes an embedded OGG codec (JOrbis) for loading audio files on iOS where the browsers don't support loading OGG files in an AudioContext. + ## Singleplayer -As of January 2024, singleplayer and shared worlds have been added to EaglercraftX 1.8. - -Worlds are saved to your browser's local storage and are available even if your device does not have an internet connection. You can also import and export worlds in EaglercraftX as EPK files to copy them between devices and send them to your friends. +EaglercraftX 1.8 fully supports singleplayer mode through an integrated server. Worlds are saved to your browser's local storage and are available even if your device does not have an internet connection. You can also import and export worlds in EaglercraftX as EPK files to copy them between devices and send them to your friends. You can also import and export your existing vanilla Minecraft 1.8 worlds into EaglercraftX using ZIP files if you want to try playing all your old 1.8 maps in a modern browser. The glitch that caused some chunks to become corrupt when exporting worlds as vanilla in Eaglercraft 1.5.2 no longer happens in EaglercraftX 1.8, its perfect now. Beware that the inventories of LAN world players are not saved when the world is converted to vanilla, and pets (dogs, cats, horses, etc) might sometimes forget their owners due to the UUID changes. @@ -57,7 +59,7 @@ If you would like to host your own relay, the JAR file and instructions can be d ## PBR Shaders -EaglercraftX 1.8 includes a deferred physically-based renderer modeled after the GTA V rendering engine with many new improvements and a novel raytracing technique for fast realistic reflections. It can be enabled in the "Shaders" menu in the game's options screen. Shader packs in EaglercraftX are just a component of resource packs, so any custom shaders you install will be in the form of a resource pack. EaglercraftX also comes with a very well optimized built-in PBR shader pack and also a built-in PBR material texture pack to give all blocks and items in the game realistic lighting and materials that looks better than most vanilla Minecraft shader packs. The default shader and texture packs were created from scratch by lax1dude, shaders packs made for vanilla Minecraft will not work in EaglercraftX and no shaders in EaglercraftX were taken from vanilla Minecraft shader packs. +EaglercraftX 1.8 includes a deferred physically-based renderer modeled after the GTA V rendering engine with many new improvements and a novel raytracing technique for fast realistic reflections. It can be enabled in the "Shaders" menu in the game's options screen. Shader packs in EaglercraftX are just a component of resource packs, so any custom shaders you install will be in the form of a resource pack. EaglercraftX also comes with a very well optimized built-in PBR shader pack and also a built-in PBR material texture pack to give all blocks and items in the game realistic lighting and materials that looks better than most vanilla Minecraft shader packs. The default shader and texture packs were created from scratch by lax1dude, shaders packs made for vanilla Minecraft will not work in EaglercraftX and no shaders in EaglercraftX were taken from vanilla Minecraft shader packs. The shaders are not available in WebGL 1.0 mode or if floating point HDR render targets are not fully supported. ## Voice Chat @@ -75,6 +77,12 @@ To make a server for EaglercraftX 1.8 the recommended software to use is Eaglerc There is an experimental velocity plugin available in `gateway/EaglercraftXVelocity` but it is still in development and not recommended for public servers, so be sure to check for updates regularly if you use it. Configuration files are basically identical to EaglercraftXBungee so its safe to just directy copy in your old EaglercraftXBungee config files to the `plugins/eaglerxvelocity` folder and they should work with a minimal number of edits if you are migrating your network from BungeeCord to Velocity. +### Detailed READMEs + +- [**EaglerXBungee README**](README_EAGLERXBUNGEE.md) +- [**EaglerXVelocity README**](README_EAGLERXVELOCITY.md) +- [**EaglerXBukkitAPI README**](README_EAGLERXBUKKITAPI.md) + ### Installation Obtain the latest version of the EaglerXBungee JAR file (it can be downloaded in the client from the "Multiplayer" screen) and place it in the "plugins" folder of your BungeeCord server. It's recommended to only join native Minecraft 1.8 servers through an EaglerXBungee server but plugins like ProtocolSupport have allowed some people to join newer servers too. @@ -164,10 +172,35 @@ The default eaglercraftXOpts values is this: - `localStorageNamespace:` can be used to change the prefix of the local storage keys (Default: `"_eaglercraftX"`) - `enableMinceraft:` can be used to disable the "Minceraft" title screen - `crashOnUncaughtExceptions:` display crash reports when `window.onerror` is fired +- `openDebugConsoleOnLaunch:` open debug console automatically at launch +- `fixDebugConsoleUnloadListener:` close debug console beforeunload instead of unload +- `forceWebViewSupport:` if the server info webview should be allowed even on browsers without the required safety features +- `enableWebViewCSP:` if the `csp` attibute should be set on the server info webview for extra security +- `enableServerCookies:` can be used to disable server cookies +- `allowServerRedirects:` if servers should be allowed to make the client reconnect to a different address +- `autoFixLegacyStyleAttr:` if the viewport meta tag and style attributes on old offline downloads and websites should be automatically patched +- `showBootMenuOnLaunch:` if the client should always show the boot menu on every launch +- `bootMenuBlocksUnsignedClients:` if the boot menu should only be allowed to launch signed clients +- `allowBootMenu:` can be used to disable the boot menu entirely +- `forceProfanityFilter:` if the profanity filter should be forced enabled +- `forceWebGL1:` if the game should force the browser to only use WebGL 1.0 for the canvas +- `forceWebGL2:` if the game should force the browser to only use WebGL 2.0 for the canvas +- `allowExperimentalWebGL1:` if the game should be allowed to create an `experimental-webgl` context +- `useWebGLExt:` can be used to disable all OpenGL ES extensions to test the game on a pure WebGL 1.0/2.0 context +- `useDelayOnSwap:` if the game should `setTimeout(..., 0)` every frame instead of using MessageChannel hacks +- `useJOrbisAudioDecoder:` if OGG vorbis files should be decoded using the JOrbis Java OGG decoder instead of using the browser +- `useXHRFetch:` if the game should use XMLHttpRequest for downloading resources instead of the fetch API +- `useVisualViewport:` if the game should resize some GUIs relative to `window.visualViewport` (needed on mobile browsers when the keyboard is open) +- `deobfStackTraces:` can be used to disable the runtime stack-trace deobfuscation, reduces micro stutters if the game is logging errors +- `disableBlobURLs:` if the game should use `data:` URLs instead of `blob:` URLs for loading certain resources +- `eaglerNoDelay:` can be used to disable "Vigg's Algorithm", an algorithm that delays and combines multiple EaglercraftX packets together if they are sent in the same tick (does not affect regular Minecraft 1.8 packets) +- `ramdiskMode:` if worlds and resource packs should be stored in RAM instead of IndexedDB +- `singleThreadMode:` if the game should run the client and integrated server in the same context instead of creating a worker object - `hooks:` can be used to define JavaScript callbacks for certain events - * `localStorageSaved:` JavaScript callback to save local storage keys - * `localStorageLoaded:` JavaScript callback to load local storage keys - * `crashReportShow:` JavaScript callback when a crash report is shown + * `localStorageSaved:` JavaScript callback to save local storage keys (key, data) + * `localStorageLoaded:` JavaScript callback to load local storage keys (key) returns data + * `crashReportShow:` JavaScript callback when a crash report is shown (report, customMessageCB) + * `screenChanged:` JavaScript callback when the screen changes/resizes (screenName, scaledWidth, scaledHeight, realWidth, realHeight, scaleFactor) ### Using Hooks diff --git a/buildtools/BuildTools.jar b/buildtools/BuildTools.jar index fb427642813bf47fbc725d8f1930c606789300a3..dd772284b5c0ea8729f6040cf84ee7af5be61a59 100644 GIT binary patch delta 37334 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�b8t!S!Rnn9{;5^>mVJ@}v z&D8^7b$SG$=B~E{9a-~7V4euv0^@zI#t^&-+xH~40KplJP*B=~Q}hQIlfyOUyB@RK zPRbdJ+z_DHQ*Nh!)#3SG9diS!jf&~PfAtt?Ope&NBWj;I{BEHTXqPq5cv2Mlm#Qf`zlkn2Gxw9S;zaHf=RXwI|i zMQ*hrxYu4_^3Jw}#<%y5&t;r|Z`SmD2xL3F1|6>nmcdSZ!1{HT6ND6kTKL5=q?Cy?Gc~jA~;wzO#HD!nInq#IoyB~xGvP>Ev1mEI9=Pv>zfjDYK`7p|np0!blwK^Zyu8^+HHpko8`NAJpyB#lX6`L^^Dbq4^ z^N&ac{WPA#26LFSjlim<)C=i-uux0p5Wue^Mb6~MrXWk}Fi~5{@9M}Kg4iLM5+m#P zQ`CyL?u}Whv8BB~W68HGTIV%&b|fFWFGPP`DYBezS0kKH_EOO7jHaf#y`NXGM?fYy9=PTCwJQ=s8 zl8*uvU@W@Vi7u+wY07x@{b1tl#uD%SMQfazr6=z=(mdud3z1lnqO^#&mM2wmv< z!FUD7g+8o@;UO8kTtrWuIJl~b&*0Sw&a)|fN$r(VyD-=|GHEdLlXuF1!SM3d+N!fg z=cr!84;?rA;U(4%xf{Jv+OQ#!xxG4iMhq6VEI*UQTrYxbX3zIqOL}NC42`}(=>mlF zLfCjojIt_e-ZAu?xWTlC6F zwCMW97EM17`_@F%06R26{EUl2JAg6QKlbc7QOK%hXIyiqU-L&)T_Au>?;W%4rOy5# z&i-L@?KEfow7q%xs{h%YYNbi$vb5WUtKpZT9>bIt=`%C~09mbWP5tFNK%hjJow44$ zX7j1DMVS7f9S0)I?Ac}OCrMlL)c|&rNHA|pn4nDw;5)^4nQgrr4}P8g(B@*!b(6Z& zK4D5;dA6Xkpi8S~Bw|2b8%vkNn7i*dZ#g9IFnE*hV`Esc?eevxk6yu;zfeItT4?9q zhh-pJGMx>S*T)5)w*l%x;`)szKW%Z?ffv@;Rmu2%Hb+jAtoUItniVBF98F&%@gcDq zY>w!MI7*7mcUsfvefd|X&itId+UXbJ2&*r5iLLq^+ZLz)@FpGm8t3;oN&r3r&! zwWo=IYW~Z_wO`7C8vNJ#I;O|Jg&8uw*i7Cw9neGUe;4}a>fcT2TtVahrRg%hNI0R4 zAW$&8c8n0vq<^`>|H8V60LA@}5rs_$h4?T038w$%XDFtB9nd-pRP^6n+P`pkf&ynQ z_@eTvNIu6z#o-wnRck)%p|E1=*tB6jyV^AgjbCAeFr#N6{Dri>aO-_N5fsm zD5#J?4t&JXt!l2e2?A&cb+6vyQL?zbKjqrPXKLVIzYm9d#~ajq<(%#|i;{gc6nm=M z;}PiNeH=R!dJK~vrci9=GR6>iqK+QKrAxz`da2f-?G%;7v5{rOE5xho!I&n zznu92CJcsLW&{jhCfUY$-+AhXmmpu}5D~E%`l*2wW79@uZEdMkxZcI2__hcwGR%fbDzd}(!+^I4LP@jJ~8I2)HM-8cV|F_Op*%fq#NslS_iFx5L;S@fTNkH z;)*q+2hq^5He5wk$##PS)QS>I%f(CFpS~TQS`(M9)ToSXDjq6C zn1VzjwbndQV-HK9v#4bx9p9Cw(k>(0z?{*h1GTvUnhBZNiE767z#qdhQw_ub z!aGdF*n84uWJ1u`>0*pox%% zYPLa`R&%y$hfwgR5kyj)cnGG-HKNmw(}%XC=;XUd@-Bj^lj3u^>$34v&-H-4Nbpg z1n3e}%WQcluvOVcTt%|&o}@_BTe4Kk1=WpIk#ka%Qxtg!HSA?vn3da;V%!z3C}-Zm z^IPSJ#r9y~w9IBQe5Q?J?#SAzU3Yj`olTHw5IF-X(wj5V7u&d;L!~1$Ru`Y;~0p5EC38Mi@R` z=8vyUPixvtZ9K4A9Ku>65zS7cNP6oUCLk+ngIMGnC+d%2#Nwwo1u4t;{{YH_+A z771W;)E*)vuT%)ZwE^pC=4o7Y)9ANNF-aqRs=>~~tJa*{q9CQLG+F~IyG)My8d`Ks z9fNFC{aMqrrS6G!%)fKUd;UO+w2z=udR+Lr89|2BF3!=xBGMJYGe$yyi*MdXP&wGb z$QRqd!A%H=dZJHCp`_f<}%_iC`x^ybW^l@(J!J06{6RzwA%3+ zxLeI<<^y^S~*q_|K$t~Q7`7orIPRD>E6 zBW~@DV+XC+=ZJ9tEYxv{?9WXTdVcvmeKG9~RK%LH3t4K%TK;HeMZKG}mym62lyXOB zv{l|4Qhv!)3Iu?n7YWUHp7{B}s5&ZvlB=Pl+623h!!*U+1ALff{?zc4oB3ZFyNS1+ z{snnrxq}le=oCoJh`lq5!CfuWoF!*qtt7(FPMMm9@+W+NpZS(tM$*~2usG02ZFe`+ zw9lHj*!F{V&b6DBFKnKjC1c8~ScBAiWVk4;$q}S{-OspC}oIww^- zn!8wBqu2!S!t(3UeI6j4VZ)3Z?Q|e_)PU*_PPF(So@79A7ex}QB&pssfLHfhp7~I% zCFaNp{MY3}m<&<=|&yYOx_ed3`eZRX+sanp_|jtKq2T?~-=MhSFC`PO1Ru zA4(0Dlgr?~r+lkzUPwne_Kr)UKM_@mDN$?^e+L7Y=NqbfsYQhu1Kofh!<=N#ljuXZ zF!|AJp2HSAg=4*}nvf_;Q?)9^i6KYMG@35%jJn6n(_X_+2)QA;S2?I3KqvA8lj}iF zQk)z>w6%XrSGVn*2%G+Fo&GaK^J3O2j|0f2Z*nG%j;yHEJ?M^XCBkd@3m5V?9QSpZ z;e`y~2C*3#)3Pn#S3$DBAy*3syZcpSX56kU*=&dWxWvirt*uSqZ4yrwdM@*=i6R3s z3zo?M3JpP|T^2M!l=Is>B0)&dth@4n|1jHPiBb8GR7zrk=aX}0BrGjM1|c3ss0$|= z4r|Eo%n9u1=s(yA${y}|6WA?mNo+|#KY34pGkC1{vNtRKgE&{8Fbcd?3@&*a1U5N@(Mdr8 z0qT1b=*77ztTzo_%P`h28Z2+v+{k7%<4D#3Uqp*TG2>)Hs^ncI;Y`lA(L-?j-xa%n zrSB8d+*qpN$F5WixXH~2r2I1dXjS4bkA}ZG3y4|ZDfGUj3| ze44`Pk#rsVu&>W2%v~F2o91J*{2Kj$G;_yKkdJER*yQ}Q;k)mRN`tng{0tGG_(mrW z+2P0=p9DAWJKjrjRH$9t4!0uBH~y%SF<~JMV_GmPUB^L zWMQm+3g{nSt%S<59T`Tk^^B3`rNRzyn@L9*7yc#bpwrffmH#p=OTwmMz&(C+va;ho zWtTn}PPSPL8XqB(v|dfC<`A+|R_dc+A?9PFG2=R0I+u3M z9~d;Zr#d#Z-22xu7zVRjk#8BdR2?V?91&w+!?WAex!^Y*1aT z2}m3K)d4jys6~|}bKKq^JaK4-sWGEHA!We0hw&ET@BlHyQcW>=Eeq2ySQEw2uwz7& zAU?ii77e&AV|f6{H9}>HT$S}EFjPXEhM;*aptQf88IEzzCdFh~IwJK{Nh&ziBi)vE ztFolqpMeew&ujISF%R?vWVx>=zrIJ({zw;&?vI<0{H{sE$Y~@nWoh%h~C#3QUMH zK&K=RzOa^2Y+H^GV%(342^QZ6qQYRo&fp4;lI|*@F-2xOPnDoQVA#igt1{i_iG*OG5K)5dOOJhAR{|rR2(;8i11~VNh6_|!j5-1=d<^bR zZ^P9*ov)w5fC+9%P7X!*MfrRAsn<0A*|H9_o}n8^#8csQOA&Ruix!!2bGvS!f^u6( zu9NbzPyBtt(Uf=qg4@~JPv**zawC;+^^V>yTN?u(12s1Z51)YEfe3S{F?aB=LNF5)AL8F5+@{~A}z!0Hl?WaoBw8~3_tiW&UQBKlNlzkHWJGcmy(S1LW zR{&7FdzGVUH)&WqMW{cVU#oAcvyWf8qK}VSm#1jYJR_d~d=7yU&()Jl&t4N@aP1BC zH4c>yibn5b2G~){U6ZG`Ve4k%++Bze457_!O?p=Kyc z4>3-!UrP)Z!RyEGKv5tHuohl^1B8(@;|Hem;-tL;1=4Xpb|&1xE;x0N4>Q8C7!7@? z22va#=y>M6Wj+>kx!!`iKxrgM!NM@PX1)OOjvsTu^ZU84G4Kf?EOT4lsb9;5+}861JYm3?mzCfgcPrS zjK4Seeo|acg-09i4R8nSaa<-SQ3Fmr4CWP&-!M;N$uv<|0(N#tXf>8xWrPE1Q$^8qfpEY`mA z@4QCxWgxu}QzHeo`BHz#M6jolbret}$cIjubtGl&n#+*cBtaI;xh3=SA_SrjCCow> zq<{;B|8_lOvAH(G36z(7+9H2);m@E;PzWvPzcynVOpp#K7oWxvJ+TYttU6h?ufxNU5Qf`0&%FQcdCNLNOLvlVfGh1pGM( z56icqc?<(nviweUX2|QTGrs8SOoOKM{k=SC5Lc7^!L-aOTc6ktY6jrG>-sm%Ccs-3 zD;Br^q#xHn6t|FH!t+$T){+z!fi&H_+=|q6Qkk@z3XKHov~*lefV1uAPGjXXP4^f7Q8LlSHQEcn6pjbzqvkh z?+;EIAzbM6a4HfC!EXRuXspMOnz8m);ClqUDeM|m1&xE#*S&w!Xe8-Q|ME!(Q;&Mf0ksA+~Kg!}Za_IeNh}O(} z#%}Y&R_f5|?rP<#42%tmTT#v!&svu*+YCqQW-MwN86sFW-`fBc4s)uWY=)V1sY8S& z`Hk9kUeH*u8OpEK*KOB)#d300(-7C}6hc&LliG-8CNyPY7|x{hC59BWz@tW7&HfW* ziLG4OH}xUR$}j?UKbBm`3=sl-xQEmOj(&&6aBcyD%OJ{qFM=&NtA04}2iV$()k27E z2qJ&bz$dI0Qc(b&VmJaf0_Q&d8n8kHnM(|tk2vJ_faMm1%9aAn%W@-xQ&63HxYJNT zBf(Xe+cnBs@SJkeGp|3=PN>_B8aL99usely=U^N9vZcjc(M9`?ad%XG`|1!mc{}5b z3Al{x4U_h&&=zMBeakLXMy_A*TA6{MTG{#GcJfU9t>pl%UEakC=iU>xKdb?8T$xn; zamS_|6a09NoJ>HX?5RD@JK8n7_fl1}xWjvN@j zl@1bmudEnET#RUUq1)p&i;9PU&&n4ZVWhk zuYL7R1MXj*LbHtmacmBAW>FUyxfZYBat=mCQ!5FM1JPq}ptC67UFs8K?tuQeOtE7U zqA-Ab>gkabpihUmb}SVfhBl)=R0b@GEtgqX;l8U0lqL+;zU-bVzs6d`@}N@H#gN9J zh&}`U;3AN!QPCT76}n;nWN>qhyeH-iIOH8py~oRfHHUuJq=TPe6~%ZTeb*$R9|@a* zvIgabIHY|}mC|;a2Chnh^4rxEsY9{oY8Ako2QOt*BBXOUhMLLT;q?JHQsQgeWk#Rh z3yUz}S2&8H&|ufm9xL#`5q!B-zt}|THrW8=P!}Ib->2s}W5;7PnhJPeJ*cf^-G7RG zTed&YL`|`Hc$--6Q}4KK0F#l}CS<&or=#9KN;EV$PHBX!LLW3<6oKcMIUkW`k_0@r z`Nc6J$9ja+;pOjLVkY#H0O&LroP3D194cv^>T2r`tVLDan^(l|W052P*aKl1Ci zrh6J;y2(A3v8llDT>JO=e!rg@r4IdRCRHu(Y?VU)cVB`q>QdCkobY~cq~eObh+Y5& zj)rJts>`HpY*tJKZ}P8LzWw?K`WoQKKA{&+Qq?iHH$igge)rKPi&3nne8SAH8qj4Q zfI8{@23AdA=S^-~vLHq%ccPX^p=aAg(yrci9!md$>VHxM^C}bKxAzU7RL4`*9}v9J zbWNdKg~3d8SG6qn(8`=$iuVo8k-Qyd(Az$u3OnQkJ6JXPFB~&0RS9f`DLw$#1&;kd zvkRw9W%w$n#)a>E#BG;w3*@U5$~DUXAnNx4eUC8RiI$&`QodDMg%W5OBzpA#bt}jt2o66ilk`4*Ix!LFE4NrTa09c%t z@UOfw&T$GIe*09OJahD#4k5r)FqiU=a`onjWYrmbSSZw_oN-*)MOLKVNd}Kyq}K0w zM$pjOi<1lnGBN~Wcczlgvc!+59bka1i1xE1L4^yV-0wJQUPy{Cqf5P^YQ-!b#?G{7 z#Dc;Twh3c`HBf#6>&HH-T+>O-QgRmg_>BJB&btGI{-tQ6DKs1SPuKvZ`tzZJ$6Yj} zeI|vU9cqK~$+_=;7~bb5HKF>}BA6!r3i_vU{wYmAUAiO9d&c|Q)ea?h1!CVN^x@fv zO!WQsb>P--e3A$<=7xms3L*Vgh7MANo_@O&g@X55>2RurUezZn?f2z?klyS*Gs#3U zn9zcGB9ok@=cdf9VruY|Ghlz zDac0PZziW{gALu^EXGkNg)fQ038Me-Ju=|}=m;iqUob`&8d3O&E_)FB(tX4n zG$kA8H0#3<)>xZN4jaf*_@|Te#ccB(6aFaJA9QBBxndo*yIJ$lr?>5;N7Xk7W!TX# z^6vxd9}=o9b~XFutK7pg`~Q$o|8D{pcsFPcBuaS5|DAMZkl~UI!jkb50*omGc@Pv0 zV>7yFSEQ0!8y$E$knWq>f@kNRQ+SH^CO)ivAO>u?gb{P8ywEbgeWdz|%szM7RKd_Gn68 z*>!nWqpio0Xyg+Ps$fE9A^UiyCjUAMR4T?B_w5`?bOiQIn?SCES|$NZ zs5#TpGj0m$6gI1{H33yxm)vtbcZWExb>M-sY^>-DuMSXr;3>$I{U7GEbSvf;zyP(ZD>G>g8S zjSq3ShR|P%jAAV5MUMTITakz3ATLvlCrdRV4G+*&{spJ4nf%#5aH5UtcFrW$NBkc+ztBug z&v5xcrZDw|X2lX+d!E&{&nUZjQwq4i4;ZT{$%*arBAtA=IH8p$u^Y z1p~B|fl3xHRiHI#G+{4ilt+)&`Z|HhrEDJYg93|`=x!E}DT?INZjE@`f;BA<1vceH zOSKP0S6Yy^IMp2cmjpf8E7(5|tLp}#uD}Q9%mGCQr(M6 zOD$^b$1Ki39TqrFk{N%uRQz}?`P>}Mdo_13boEu70x__;oZjl(nfCYceyG!`2kAfp z99J5zC1Gl6t0mMWSONA)>6d_%v6)4P+IvY45nK_kY%&avCta4|49obidZGj}VclUH zi+zjIqi7*R6Usi~1S%79yeQk70P$h^@Guqyj+w~%(XavJ$Ti&C+V@w|x{*3d$hnow zba|?}3;6hKWu(w%jf5BoVj@_5XEyqH^2K7Zlv6Cp_KgM@NWi1!<{%stCY?4oQYh-8 z+K`OGS`*8Yt*ew}Z7HUsRz!{%6n0N{{K#-)+R`HB7;E_U=gqC%#Z}lDdXF2u)!HY( zrH?74Ety8ed8VaaVh!vBi4R6#Mi> z`m1msv~d%Z1^}L}9Hg~jwMzFZzy44q6Rrtz8DAt^!orHK1MjL3pAx+6b31$ecerH? z_oYck$sb9{Mr6AtMG*v1by&+=7?V{AJShT-bxnoSyh_5hoUBiF8^`XSv)}ip zMfN2aILE7fGFo!tv@4~2w=0d&E-RXsz5T-tKLnwA)&QWqrBOvEKNLMlQ*virS?gr^ zG3-Y;ZBYgpv=n*P5bz#N2J=j06e^iieIG2DJPX4Pg5MsZWyk@1^Rf)Z*{BNPezq^i zGZG;8mOe)Yu$~7bHn<7x*0>|;x`uUKPXDA<&i2`ssZDA{zA- zpe){xIKbd`j1&*=l*1$aFc2o|5}v4y*bc68B|oo`|n_<;YXtZM;>YWw124|y~p9F3WX`cmFS|JLZ01jly1d8lt=gGk%mO?x*kQQ7a2`)^XR|NnWJWI ze7%b4cDz?UVVr3*UAW!7~aaELa&x;l@=uIu`(y0BGsIIbSg5lBd=iB3Ee*L zJ()IL^lam=l!D0spK$N|&QPgi!>d^(HWTA9JDpmSr1wxn7N~;^7C${Wv7;bm`o%ok zPFzt;e(s3i zN&h3P6Dfaals2@F$az#wGv(4L)-p;5yjB^XilM0-TBiN~@zSNkGt^tv6^zp^^o$gjI}J@zUN`ieD3;|(h2Bkn^0PM^KIqk+QQYWKc1K3pdBpyz zI#-&3N5?>o{4XD^3d|b1#s8^s^B=Kv6dS7$yR=3r@=@RHtT`yZmpxu>vlWqu4rk

A;8{&*|L?VTw$PI$R@)Y7S2Em!HnZ!VyUTP=SLJ>QG5knT8>)XJlcNwk z;kZPOc}Vrh`$=W(nOwyez6MSvVN*BKN~8YKqc`sDcbjx?au^m#wPOUtFE_1f(9t$1 zKUZizBX_G~e{O&Fh^51AT2fTmq2E>Qem?qUBH?Usz~v+L=XWI^4v3yJq$@`@h6=P6 z5Q)kKYT5g~E=gvviX>iLYu2dwY?@Hserx?ft1HiGLHt$qj=i!5szXuN1MEjA4r{mC z?0v&#-(UM{mS^|69sT3XVo_J8pxV@qgB$Gc-eva)i&5t*7tRKF)ZN*nw_k2B>sp0X zX@pOrNs^wn`$;#iaoLGeRr2celMXuNwTag*rVJWw^1L@Ka`D7ij;+1J8dK9ui(g1_ zHsMKePCH@+B6jS(5p-~#eS$%6A@%GxjxIN4Z;Aj0`ttYvkK)QRv83Om)`J?GD`;NNEI@baE(- zN=4K(rKs6|(^tX%5$%GBnpdgoMaYvz>s|JZG5Ji^&+WdMppc;%t^L;2ApN*+Z`!Eo z8B-JHM3B?Oq36?8Ivdv=^~j&3+Y%@4SZ_;I&0>yz{umU`x-K=_vZ}_vE=h_mP~4rc zBIkVNwKZ9hcE{^48xCL3jHx_NYKR#2l^!CeF%kx&TVOSSI7eTv|*{=wBPn%(3IsHQPrtGew zUD^Xy_XE~dJh9OnFRLY&O*f|9rYrdWtyj^P5iPg3A=5lQd+he(z8sUj!>zwP9O}!Y z-}^wnW3YTc%1O+9*Mv`I=M(KM+bE)!H?Xgqp>D}l3OQp`tF)y!OkC;(!8DuSntHor zv&5gYU%ZBkDsK8J?xsHK53q^wqC2??N?9xBtG|EM;O1^%JwUs5>&@Z6e7x^Q7msNB zSjsa(ckAc88&~XIP*D9cD&!LQqImmA)`EtIqWc5inDKzu>Pq2%6s1ils?-Iq^mLCM zv|5oA!XiF>5bD~DUo1i-!I9YRUbFEvCcdo zALm+~O5ICrbM0$qrCY6PtuOlGGH!&?1TXX@0$&Q(oA-ivPHtDjJut}0wZc$+A)-Y6tNY-J2>%W~p>0Uv zAV}m$@%(1_$_k&=Ec_KrEK6b^iqj#{`R)APN&|UxF}9P#yvC zm{2$Z5->q}6eI{km%YX5{2SAevJtQXsTl?S7%2J*XB}M>mW<(=ltp1-Q8>WFq0fuL zzSsEG#$V%5*c)7UwI~F>1qZQf4J`_O?{Lo3MZx1euIYP^8~kY;7djUOs|j4wyeLpV z;F{V+L2nZ5=MP6)ZJnP4aU5wS3&e7yViw2}MbYm~^bpDv9=P%;pa5bu5mh!=i;Pc! zl`3e=sHLMG+hHRwBM|~&BX@NVg47xg753FrQb9ZFargZ)(ydCyvj(k9h z!0F$Iwg=;`K?2K=aQ(y>ajwPq2#hheLK^J+ghUAhSvdlM%0+}u`l3`DEa2g6IHil> zoUvNLd~!%H8<4PKB*+jO$b-Tt4z^gzgv`NwK0)mMzHiuSp;vOS4}>ApFE*(fJr+IRhvN zEJm^%8A#_7LR6*!2{XHP8k(It{tcHwPC*C@%{{d)M@9H0pox z#Q%#2A{*6Sx`b*eXh9TBj4S*FYFk=3TC+yzSL7FbipeM>|2GGM^-*Nd$|%TNR2$NU z510jNDrlJ$?o?Gfo;ifQ98;snwmJ$dNk_5sgo=MR( zaW2fLCH*ift#0IW&@uv{2<|0oo^fM__&At#OUAB2Ph*Oa;pc(7*nhcA7C86uBKK@y zU7;C#+pv^?f#$L6WY6KAbRf!MW-o-^hF)+vr&4sAP&9( zK8Ehu^SIWQiO>Mi3o}#=(}amvkK=}5g0RdFVWt)qH)le}FG6FkJccAo@NqDAn2c>q z%qA>Kq(D?C|Bgu@Ft1nwIW-ALSSSoVySAhX5(q)?!`Xqy;P(2|Ga-n4{i@vxPn$ZT$Of2Uw}wcL7x+GKSA>2VYM)jG6CXl zjK%$2#D|A6Ww3)}Pq34`I@W`?XTZ?T%(A3Sr dZ(0dOPLo)gqX3K%fe;9PQ*;P~hfHXX@PD;CCk+4q delta 34472 zcmZ6zWl$c?7A=gsySux)yF0<%9fC`k01xgE+=IKj1$Wor65JhveeZeCz4ugoKW3`; zUVGN=+A}rMt9vfWkY*B*>e?Bho(p1h8@m}(!2fL}KXDtRwVp!YG>$NTf&4E>&OQ(I z-_{fNG!*1N2PFIF2_&=ekT+5a=A-=UVColWP+)=nPrH#_l?LJ8_%!1g2>vwH3kbBd z$qNXi#yY(hg#SdOku!n|q_JK?U^l*6vO@k#fS#b!2(o4VuQ3;(+lb^?0`V{MkC9e) z1p(WL<#O^rSl7Mu|G|y=UdR7Ka{So;MH=SfQfC(gGhK;2M+CMgL3VpQqo*lBMu( zp&I@3ivKUUPNUyXnEx@X6*K?0mU96*X_rs`FxXYN{x|kNwcxBS`k%pGxBTC1{~&3m zFaLDWwt?z@@L?0(zeXc^>%@N({D*<3v*3Rr>^;x_B8^P_BEtWt;OdP%pA`QMVd__J z<$?fL{O@`D=OW-wios1|d50ixy+;B!`!{*oTqK0Rf85>g5b!{?0KBQzQuEQ|qcIKZ zdJH@gR63Y~*eDo8KS&xb5(-03{yWTC*vOVZ~BObdG6be@O{D9yWQS1!cQCSGl7@Sld!0wFksP4Lm0Yf z_7wUXxL;mzpfCaPsnT*{vne<{d;g7<#(bx87aa;LlwnU{qB~D2i5`M(K>`E^WP8Kt zA%}i)W(-k(#cn7b(B7tfyuuF(;vVHrFNYw$;Om;m5l2X}TJBaF-AoinPQ{ z3f%JeZv%8kZa=jNNKgRu`HmQ!J)Cq?Y~-a;=G@OqgazO-?!Y`0vr4T<41Bby!T?zs zZJfcI%d*`$j<7&Z_TjwRcPC5(UIpK?=1ISEidU^oB=(=?57;^3qbEf6?S-AD!^{C@bQ zI*H%|I6)B<^`c*%_H@~7O=~^habm;oBOn;fu!9>Vqj~M?)7Qj@eH0GPEmB5es=t^b zFD5is%$4E5Q%n-7PKrmhNrtN1TTBi@f}W66 zGHBBg69b=oAR8?#0&26+B104Q^$mg6rm_4tMAoMqrrFx(r#fB1A?IL z$*>4AAf1*yqjEx#+cnCp+3)FRXUyp)q6*YezXtGT%H_x0>OqT)&=u?bc&O;Z(p8G7 z*9n^845N*%7rj*P(NzHXNGcPBk+q8oE$vgQ-d0(FfUe%AWRomg@i2k<(Ocq!ZL z@1^*TV~+1M8b+5o&Heb$ao61Y19fQzh~XNCS?*7{h!~+uT1daR z#uQp@ll6`_AmtVcHo>?}eg`x^G`KG2$6tTT@eC{^$rD2=*!2 zGG9YWA&x0UK){Oa^qm&lCyHB3iC`S&Cvz6zvzVyXP6m5lg?ce)28clR6GP^lZ$uCp z`I_rE{`xRcxze)Nm?ljE)9>Bawh8CJj=?_IWz8Zf>Eu+L@rLmE{O~f zT00yjpX%6JW^c%5W9(t`4xmvLjUsvWEuXc$hWg@Q$q9j$S%Bdh<&9JtiL#B61mEcd zE?Gu4X3Czes8)V(-w#A|B8)=tNwF_N5{L9z64foO*~;U$QoBV|_?3d_@p8WFoMezm zarP=@mrlDM*f)jNjQi1lZ>`D1N>CtSIUJL^wq!t9sZWq)J?TpEQ(IzUj1LMo!2F22 zAi^Hhlpf5}r0FGhWh6_8SYWAX;mpNP6P_>S$*aNNaZ-54*2s<(jqZsf!?T~C9F;Am zTWm?1tX_bY$llVDj&5tA672U~Ihk6$%oH6b`Ky!>AhlSI9vR-zOi+S`&8=fcV{$|y zFN2#ST0$EGM#%M$C^DRwxF)SbK(w-$>kRH~j<50~!7nA0)^qQnsYON{RfCjX^ObU# zswLTV-z}X~6Hv?S`wA-8JIc@LioqCd%EzXdN<#gz&&3ct8_jmqmPuk1(}=p~@fENZ zSzovXU~L}qc~jvMn#?aEUE55CGqkJ#K3Xj@Hy5JCwv>T|GK+paxPz33k*jFSqGc|i zhWA%{CQ+s2@Pge~8C$XiiPOy`-2qW{scFh6|4$mqkW5wcaV-0NDWtFbh7qpzMu%YK z^HyEQ6#Eok-)O$-ruaTo$jJW8BA~B$@fbh?;0*T@Gg#EGl@156dH)iQZY#;MN%Wy8 z4_m<`L(Q8cUtO;N?JWZ!Jg=iI{7ZbEnz_kly+{9r+rnj9~CVN z5KaL-$fS^FhB2 zvfaNPoypXy1y$gL?b~te%o>|z6Zu@BVRHswI61Sh9Q#$T=S^Kb)91gJa==#0pt_^W zu}4mTJC;;S>+UHu@?adnrN$e$W`PXo0V$i6L^iB9DvcTY*xQTwc{dafLl}2)4vjPe zx$(k7_0n8`(({M|YvLLIRF=;)5|4IYWjqcmxniEZoIz)fD7nJ6-dbwWH6_AEJN9>T zXF662`Ney{)OT^mG&RLD9k#|WiHs5%taGubVvTqL72B+}Ax|-u8G0^rKVIe)fIihb z{4s*1Q~YeMpCifVV9BAiW=mJI^NY+lr?eY~3UjIE0NsmhIL;hWL#H)*kbLT3k+?Zq zBL5mZI~n|P#yEy{r-jOSp9s7^`j;F-Xls~fB0`-y(kHc%M1(KqVXNcZNahN(a{m{0yG2&yQ$q_Dkutkm6jWdb7jt2Wms4YUEIIfo}eov zl{*aa@Q7Rr!&J=L1yfPNmA>rchSzcAbe8Fn3MKp%WrlALrk{(CeFN!y;4{% zmC*xN;k5SagPHPS0-H;2d%>-mLS-L$sB6=D_>jyOz5j=kRQc;}7M9k5GUlrifpBtWkG|VeB5yH|qnSO0XBD z0E(_*bbxB#$jz(A7aoMA9YmeEPps0~B+eCiIpRRy$6Kl;jH5+543Uge+4<sQRW#19j8@q@qMiKT8uL~~j&RpE?{2kSopT9}q z&e(e*hVmGn5d#wON`-!BN63s(Sp=X9*5C3iQ7n8G2lq;xu3@KsU&h7#S)fwM!Hazq zRh_BRO-A`}Qd)AMz*|ee!JNdKLgFEb2iAOSljqpxpw-#L&I{mKZ@j)oqFF3kfKy_{ zz7C_=$rnW6;F%PQRPdZdpRE}3JiSvDWijv0Xyt!azvaPIgT2Bb zxgb2?P?$BTY`}y-V7jx*Xj-SpoO3YK3#?)Q&TK@gAVH%1$qdxyorCeE4ps@EvH|Jf!|10 zz$DIlj6uOop|AYM`5>^{OC2C;BzJpd=%9?Keho*$4IIjge+@hw>#MXTpp1fk9$u2^ z6Qd;w30w3y8%Yz>uCQK^Fh zM#+tT2k=6344(d50IC#upo*zpK6t43-Vi2qC!C`!?5i$HX`eBL5bMB%XdeyFF@9Ku z_-Yq<_e?NkUD;BzC0y#Xe6T3I4Ye7wlUzvS6=dlYR822$^p!nFDQ{^>>owOmQ7{8o zQwAzYBANH2|2$s%qyK=L~Th$zSw5Q z$Tt1lbr3b7`fVtuyMDGowf#7&uz5k?aQ%UB1F;F;v+R(h7QWDmH?xS}E!yq;$qTnD z<;mQ=<85S$`hb36Ll*0K1Uq-cuDVi?@WSAOfZtcwd?cOte{~1alFF(Si^O#=+YJ#lm)jmF>na726VaMiyG7I9yG&izc$qM*p^NQUx`#uWP{Arr3r?gRPw z;2LP-w?IiC+BcQ>JNonl{*Q%>eHYAqQuI0Mm%Y1@w{kYsG}K>)fxT>CoSaCXLPKn% z(+}pUXA8pfY;Rw|tS#99+4NGKLFM4<~O!vvf%qyDFImp`cguWC%qm{ z5qdn^2sA!Y^3T%m-d^9t8jAgro|r95ghS;$*uqb2`!?FyQXN#wph6Nz?D*@{IRLVz;MriMhe8mKJJm^LtWah!Xj z2QP-9k$zGGPgtaH>Gak(9XCk9$oqTX>11lDG{1=}BQs6#pjljq`%|bG3;}7<%J-pM z^Lf~fTeO<`Up%VpWBzz6!XQFltv|z4xqIPtL-r^woj01l=SnwMQ;Ex4%#bX@$$7o3 zWH*TvoyX!H=0=a&*sbFsw6dSO!4Q31FVvhvY3LM1eUf2GZt=5!2tT9y%zd20fn2XJ<%Kaic$t&+l?_^0KJCHbX-pF7)KiTig#;Fo!x5CeREFVVXu$_{3AuDs;) z58#vx%csOor9L&4=EiPDYKC4upv6cbKZ=Xt`HF1n!fEgy&~ zeWQ2vD|w!PEg00i1TwzT#vZ2(=FK(Pm}8(e{}on^UG$^jz$W+%EB|q$h9jx^Tl^DB z6<;%Bht-0?Di>ZU)L5yQiJ0g1sjpcghV=kTKq92qg8zr_coZ>yGur*ZoIZKR$UNGs zE^({Ce1gtA8a?uhBmqS!IWTVf#N9W$izGSlJj~WJRV5I11Gp5AgE67w0XCKO(W`D1 zy-eE5?KgQ<_^)MPnmhuTZ+Q_kSBbBCupy@VSazPmWDLa=6=5iSwPw9m^~H1ZQdi^? zn5m`e4MZ)|b-6P81IyjZ-C8opdZhIvp!XIbgMw<%mdpB9bGM6l;QAfYml4Le!ldNW zDaj<++u2tkpj^iOr+4Qfjk+||%J|uV%qr_u3TqOpijHL}?yh)6s&k0ujsqLIb9lQe zY&NvF#?pf)oO;VikV}92H8US+k8*ZkntfB}4on-NbErSunej8shOEicKqHqoT-XjZ zUf9zPdecDRad1c*SWo!Gj+-I;_Q=vTWK{^Q8>LVzFm)|?{@rHo`_Z+b4+(2X-j2~V zCsAQe2y)~GHzfBOJk0`_Mfo!8EPOlZf@$!V99=)MlKg4=2k0(UXU4eHfH=@U%d?)Og}a2>p5A!=URfjraVGtU91Tvn)fosp?* zM!#4FrjF|_#m4^P|ETt#vwMd)G+>n-@)O3nwVfC+Hy}?ZEUp?HnT*qK)t?s99c{pz7-uGB9bq!+% zaXNMTR9rx0TXhmJI<@XOu5a>QsA;Qr zH%hNk_v@vm&EIfXT6OjqUd;`iOoANF*Xx&iFwf4(4Q)h8&n|d(VO}p*O+Qhbn?%27 zowJ%Jzt7wUhHS~YQF_v35u-SIVeR6KKy93+$F37i9Q*UP2rTb-jlgW|+777-dYNYd z`$Ag8xO*%kR(P%(An_66GJcX@UL%OBCqB@UaiV+9BL?>(QD%1Z6&lE=^TtB59bgvo zLwIR5!ypgHM>%ivS`XYudDwarKkz5H+C7_M?`!u4(9t+%olY*ul7hEq>US!(n#EnKf4r|)VKK3m@)1|Q$t1~8GOiO$1=gWGT%-RqB!swG6fE;!C zO8{a*X4KRQ-Yf3ipic<-s?njCsYf6cg^9$oN%?rU==9fPpftY|R z_8X2{lvc9W7xcF6`4bUNbZX^QjY~rOU{H@RD228uF)~Lua8CYQonNB9;t$ju^#+9* z@`ZU{Y|@Y)6hi$m98-P$l*i&|5*q!XS#)f8djfDv|y6kIScY<4g( zHAP4$EHF4YII#bSG{2DlGu#i8M)?7O-MT^xF8i;5(E5*dGXVWxc_)Yq+yT&9dbsB2 z9qZ!dWSy9#`h~bY0iic4Ax5F13qoU)0Mm=j=c7utB+H28#3a+LtZrK__JqYA`eI@5 z+2Sk@hTe>@TzuKGv|Ma&@YK=75cM+M?$W}P-?Cc!;d<$H*?yG!zL6%|yXHv>Mw9|V zfCFK^ZMphZ+Gt8!$QHJ4oB?*u4={wmxh1YIVNu+g6DwZN`h){PVh&|p?E^YndkZr(y=3wM(79FdxJEm#Rf-MFR{im4hrnF`l`|B>iVDEGA7}Ij!U|rWuEKul<~8 zNeIzj$f$H+3A%aYASz+#O~vI*cJBOfy_%{YVA6b-Tm0aQruOeBkpM=+X0kYEJHF#P zULAG>gQ*f27ynh<>xF_7xEj92hE!8iN{jknNJ9j^*TkkKPOQfky99VWDV6+?vI&m( zYv^%Y%n!Dle@11rf1C0hBg91PUG`Rq^I+CA-Dy>tgsQ_8;`j)>Au~(;oFn^f&Xrz; zkeZUh-of7Gz(lOew+Iw#jf%5XKwjjCo46~k(mB#`&?skfmH!B&AynrtPY5Sd5OiWg zLBPV>{``uO1{10#b+@%3AXHtjW(|-XS3iQf!ug)54i})16{uvp`tlL1;F43rg{F*K z`>th91XO3!r~cN(JQT!)S1Jgq@|18xy(#xt7e+es56Jk?*aK?d$#Q{P$|B}YU(KB6 z5TN5E@I+{|6#lM^CQq2RrhjQ8OI)jv%;On!a&m=W(l&+fKbE3GgxqZp zLIEr$XSsA~oSHYAAXEqbT8XPd(oWIk>EUb*TL^34EKEMpQmB3t3{*4%I}L zk(w?;@o3^1uf7PNy=!YG;JS80Aa0#qkkLx)SVuro9W^!N=dlgLM`%h=Jn@!0_hP7` z+_bm1cL1JvDrXsfZ;-&f^aq(9ZG0~;|C3M`18={w!v#m73wg0yhE#%55VtKQUa3VU zIkyH8dyX9v9a$SN2!s5Od?FCcf+cMJ#yjsL(UyUpq?oex=7XB}Aq>??JD90==5p_> zvHMCza2t3*yvyH1c9T(4{?EXk%5B_5P(JM{eimT!HE@)-#K}Oe)XLPfByVEamqcPK z;dEuYv%a@OECg{^cJS~^Z7=@@#ejG<0#M2AOjl9 zU;kujA5)wm_^m?_b|orSUb~y8&&;4Te@H7US5w(x2g(OlPc`1oC5Qb!!;c)Z7oo2+(n&7wPAFZ-@(uPXml_B(YrrP^RP zCr>z)Mm$=c=A>w;={E)?5)pUB&h4O`+S=wD7M2$_H!oE8LrRsY+cQP#RhnfojsWp) z*M!Ir z^EFrdAK{{zTBGjD{+gsql~)&cN9tx(%ILtf^+y9~ZEryyX9e;Cazu)xATR}>Ig6Z8 z&P!-+lo)f6=cV7*TE25DLDN{b-p%(aEzi)Vq&+3EvkfxlsiT=)*gA4iluR_jD&eyv z(h!@}^`%{>K)|9i^t$Qy{94(02=Ah5Y4Q3>^NoYj={_`~HRp?zl19S(n?(zSm}k#{m;v0waY!UBti*! z)!#2mBWThiT?frCi6`wOCJwT*gs6wEloxT$54? zKy~$>)M7CDH!Pg$Q$QU%oRX;577V8_GzlqV$Q5Ry+@Wl6m2Tz|3Gl$H?s+h#Bw{1z zt~UNjzOm<&Wmk2|^pvN>K^oT))<7edXqOVKC5seIBG6e_K1O)Y!%_g6A=Gk33MMhC ze85DvW;SQh#EB0J<4;%go1{T!b-*yKbmSwrqj%lq`>GZy8LU<}oT&RtEVQV~jqEOe z)UQ4a(~@KAgl@%!0Z7#dTr4-|hwn)?6#0?>F6YESAY>U;UA;BTflW(E#2uc|cYZp< zTNtU8p~oes)exK+ugO(4SV;etv7w~KJ@TCh#2KgO6BFiK?nBcRynr#@z9tQiAURad zS;I!dr$_xo{V}o;4FQIQ0Luy{k>1<1Im|tsn`eCUluk>X94MwpLDn~tNGjcP6T(Dh zPz*u-B1D2q$M?CO?zoDnZ!$gP`LfaO8SQXrryGtqhLpwU$y<7CF4-n%s5GvHi3CHZ zz?Jx-7C=oMi>FW%rG*k<$dO>xCee2QcL{_P>HjUSG_KlJ5K6=fJxZOU&=!!SiD941 zDcY0&aUoOY07S6oqCX2&_e}i>sKm`0PjVPo@`(!85?OpBq?*2wvA<6z<1!4=*lmvvQxX;dgcz7H;si%p4z!Tvy|_ zK-Jl!VjpblqaD*pcX+3+;67yC>Kz%7^ry(_e!CWU@+?nUV;f)=7x0D$6%}NPbT;-l#2iy+KjGhth`qG1W`{G8(3I91=Sx9eSSw-;T31oyeP{#JR5~LtpbB zTp<0q&9U7=fvsLm*vm^!Hrd+(d@(me@brXvRAlw-ZyNY=!*=HOxzK*n{QV z6vIbM%l(}w{AoSS#`3h;3l4IprEcnO9##~?8hd@VDV&Z(B&SA@ptJKi#GHpcTx%hP zcCXf?&$_Tu&N=~oJ2v6Y)8h}_srM(Jw$mUVt+yk9EI>=on1F+ZPTWhO)l_?OR#_3>Ri1TAq zQM~i~;=QHKN*%ck9mmFm zBGEI9&xSvqZtq7SqrzCpw717$R2{;-)iHa8?kf{1O{J)@_u{}plXG zr7Oml^xVHp>gyVD%1Sc~EDRL!ZGm6~142D`|5jo8>l6@n%RKP}9cZ!u+k_^iAg;Z> ziDmQym%VMQA_V`#R>dr(qpW;@;DdPkrxwqi+#iI*lrng!A6h})XeG2|4KuwWNqD1L z;f&vjEaGOWvLuYZkl1k$hJbUB1ZR}*`TwCmd~~jo7FmZh44o)+dXCh*w4Fk8cnm{2@q1`+qcp*7dEdI8j8Gt~uYbv+3pO znDT6Ot)5@ynOT8?-L$uUTV&5FWO>}G9UVCBZCx)7lqw`^^wt32w;>@3)-U!WTj_`( zllDJ~vTEpKpUe1%&H|FyIP)FPR!$W1-<9)sSb_fK+72$>4Amit1u2F!wK0zs9_=E{ zAfecY&=sOg*IQT?g9t4lBK;G@)-sA@SLp8Q1$Rkisv8)F@#lAjwB2w!^EN#4Mq?nI zDb4;hF+6~Uf*Tw7J!oS>sO@xc@nJp8rhr-ava4UL(HqLhr>!=Y+mu@Pn=i32(lBtO zIh}MvD^b;9s)t59rW7Ty0HLBVdAXUl@@INZW}X1`o5#>LffWA3Kbn6_r8&i&LasxN z5=AA)NS4CET`M1hOva!IJ|PBuMLNN{%bvR?oiV?|SpOOjddKIT#_ET4FrdYBO=uFt zP)5YUYRY|aUHha-p)cXqJhm`rKfL=LeLq-T$3VvdB)EQZ%GuV#Ri84}dCe&15sc4a z5E2emtm)n}R%i%ZqTcvMdAP?glm)ODo+4+tKqpqp#ipAzL3JxDIXeG<(N?p|Nj1Qr z%}g?L6XFME4Oz7JPRpX8|0GV9whaf_TCI_UGZ#Sd!v#wSa7QM`hS zmA+gLXGQSoDVD-$iQ<56b!74oebagL-z;)bv_?n(qm-j6+zWR(lSQ5@K@lUCXxSe1 zW=WFE?q|jY4Zjq(pz1SMy-JnM-E|o3Hu=V0viJd^`e z)y7hRangK&we^r6hW>>6GqxcM?&92;;_3r4UsYq!s3`6nhofHA== z@cX=c&-`QBTI1Ns#04`gDvvIBysCV=Vp5?EXrN)iOdDwgndZE`#=Z5|GFPO>YDr@g z1;-+-iF=_lte-oiZFF^z^1fDA2A@_gK4TdvV{1voRv&t^4wD$1J>8cfROBZ4Dx+h8 zWQRBjCG-Q6YK3=Sw9o~A0vBlQ+WkM3H%4z<1P|8-r^@K|QjgtTD0o!j1mVG!HR`E= zz?w)CIEFwCJ?C7q;>N=XWtn?bT=SWQ;L}|75aVvlC_3r4s6}qKPs)0OE0%l znoA5VucA>Ml3Uc!O6%mXn{tKyINSBD@rMsc?ME8n{?E@jENrTlXQ&ZsBW%?K zPD(z4g6W=pdgO+og(UW+s*V|o$=8+_s~ns6U-3;Gb67K!f0hQFE(f{)`IuRe0?1Z; z-W^g|V?o>%f;2=Jb<0fFZS(n2!ybNs9^?~K6 zpM2mLGEP{OCHO~)j~-@iU8%A&f?xN!m9e#k@fT=pMvIDeA#)<)3wX~SCd?v=E_s%l zEFri$zRxhW^*2Y$Q9&x9I4Q+&pg=@$6c@5WhGHp_p=@@n%2=zl&8x4HYdNQ2~eY$4d@H8N*lP z-Dqs_VwJz&ZCIq=l6$A8zYLacXl&VVDylCi7f zNMXkiN%!vZ1IS6=QWXhN`ur8N!z_vZfMtkY1HpdurUfRUGKJ;R_L@=tV%;GEgIIr? zN5?FFA$V1EK^4u@WgO1?Xx_+j2||X@7Q9L|-YO?Sg^C@! z>CWjS16_pHOKJCF5B>9kW-6>iS1Omm78~?_3cO4_>a(>Q-PvB4dnuq_z8VmztL&X- zp53_{kfm*!9i}Dg7@KXw8^^n`v5-9TwazDDB_HY8BCTPUS{|hZfFSWE{;^m7dZaUM zM`IF0IIT{cX~P57k)MqLFH}T8D>Vn^X&4F#W1^dlWtGfusqFCb=yS~Is?0lQ=Ps9 zN~dSayssyB?5?C_=w%r6Ve9n>He^e`JGTqP_;ok%X8UylN$tEp5u-N!v?4F5spmb* z=|i~VV-I@L6=av*1Ggl`n>j2zWqf&s(#u{jOi3Z>!oht&;EMxbLgQe||Lmv19tXhQ z;=mpM@kfC-Nmh#tRF;R7H9}fPG8Dwqc zH8AmBY64gw>9Hf}h4)MUwK z@PU6GeF?NQv!k{o@iH<*b(<%hhz^}%OWgLpqBmtR277bQe2>yJUh+|hsu6hwjDB9M zEElkbqz}j;-(_X4>;5+zKZLo`u)1GEo9dcG z_yxKm!n=Ls56W0Zz2hS}ZxBR0lz5G;NJ`^R`YI_wR^)}!EkHoW zuov!S#NDPakHR0-3^_^Oj+?`3Hm6_0E(escQirEuTwrN}vQdY)LMf6z=SD7t`|&IY zjpWR{JXp6_m{Y|r^9xSUFOvj7Pm52csP$2MKB$i!Bc~P*a~>f4?uYup8{>_3%%@x1 z7(}2jelrr4HX~%j@L4M0UJzFV8zAQQwu(I-ve}CUTz8{y=JG7LqT@}b3ARxGYU6$k z5lMSED6#ga+j)q`362zfI0=VK;}Q2bpftvm+4%ML+C(voil+DSPZnks!$v9mrL=ZZ z35HcrGr$eRkPN#t6t*D!;|{u06Xh+eBK*|ONK2u0{>~-L%gFk&=q$7|4M=;FblfSo z`qdfJKCR0dF9|!h{`$K~QNA@vY(xQ|sxa?G1|ynsgl!Y%=cK?s9GcMP4nOGc+ zSKn6>>@oE=#KRlgU?M9GvtNZ8`~=(3n1(+$j259iS=9TSVilDRy*doVTKSWs@uQ0EInYm5*SnB%TAn2gMfEdwG8Gi7_5rZ?B{ zmRYi=G;r>$dXw;tN4mnfH;e|m!mS4V%S1NDB58BcMBVJ(@OlNWz{;|d`FZGJ%P8sq8_imJ%cG$%97!lE}R3+;qn|2OOkx!xSuqe%E=xAs{ zv_6TeO3L{u{p+1=+6bUI8SFi_f;_gj>KLao`oJ?XP++U1s`QZE{?_46s#HW^+OIOl zV#@euga^iVAF7=_+m)PYt9p&t~I!T#;ME>3; z9m6EoVeI#5wi^@dpqTUK&OvU;%L6s6p+Mr-Dxr~j*V@x**(V${nx}Y$amf*_Rbs&9ETSID)1posChuc+VVaw#oPKK~BEtr?kk)}3>+ds` zB}Lts;!6x1i%8>f^P;dJ-3A@jbDYl8P3hNH`+JFDiGAP|Z^Gw4MT_4)W0@V{Oog~j z1%)nN9hTuZmmJ7R&1^8V$njjp+R*BO#+p$@qi)~958>`GHbdEYbFkeOq}6$=cpre#kAy$FB0odSk%zdqyc zh)qfLL1C;@XeEb&&hK{2vxphX5X&J`NM#CQYo5Blf^CzSa?RXu6M$wpf2uUBRtdtF z@iL{ai0wDLO2?BHrFB<((dKVfvNv�Oz3pqiE@cTqp6b53#k}CfV|L?X?j*Qb^0} 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 - + - + EaglercraftX 1.8 @@ -15,8 +15,8 @@ - + \ No newline at end of file diff --git a/buildtools/production-index.html b/buildtools/production-index.html index 9b56f22c..7eeea4f7 100644 --- a/buildtools/production-index.html +++ b/buildtools/production-index.html @@ -1,8 +1,8 @@ - + - + EaglercraftX 1.8 @@ -15,14 +15,14 @@ - + \ No newline at end of file diff --git a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/CompileLatestClientGUI.java b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/CompileLatestClientGUI.java index 5ddd23bb..499c6d94 100644 --- a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/CompileLatestClientGUI.java +++ b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/CompileLatestClientGUI.java @@ -251,7 +251,11 @@ public class CompileLatestClientGUI { try { compileResultCode = JavaC.runJavaC(new File(minecraftSrcTmp, "minecraft_src_javadoc.jar"), compiledResultClasses, temporaryDirectory, TeaVMBinaries.getTeaVMRuntimeClasspath(), - new File(repositoryFolder, "sources/main/java"), new File(repositoryFolder, "sources/teavm/java")); + new File(repositoryFolder, "sources/main/java"), + new File(repositoryFolder, "sources/protocol-game/java"), + new File(repositoryFolder, "sources/protocol-relay/java"), + new File(repositoryFolder, "sources/teavm/java"), + new File(repositoryFolder, "sources/teavm-boot-menu/java")); }catch(IOException ex) { throw new CompileFailureException("failed to run javac compiler! " + ex.toString(), ex); } @@ -292,10 +296,12 @@ public class CompileLatestClientGUI { teavmClassPath.addAll(Arrays.asList(TeaVMBinaries.getTeaVMRuntimeClasspath())); teavmArgs.put("classPathEntries", teavmClassPath); + teavmArgs.put("compileClassPathEntries", Arrays.asList((new File(repositoryFolder, "sources/teavmc-classpath/resources")).getAbsolutePath())); + teavmArgs.put("entryPointName", "main"); teavmArgs.put("mainClass", "net.lax1dude.eaglercraft.v1_8.internal.teavm.MainClass"); teavmArgs.put("minifying", true); - teavmArgs.put("optimizationLevel", "ADVANCED"); + teavmArgs.put("optimizationLevel", "FULL"); teavmArgs.put("targetDirectory", outputDirectory.getAbsolutePath()); teavmArgs.put("generateSourceMaps", true); teavmArgs.put("targetFileName", "classes.js"); @@ -315,6 +321,15 @@ public class CompileLatestClientGUI { frame.finishCompiling(true, "TeaVM reported problems, check the log"); return; } + + System.out.println(); + System.out.println("Patching classes.js with ES6 shim..."); + + File classesJS = new File(outputDirectory, "classes.js"); + + if(!ES6Compat.patchClassesJS(classesJS, new File(repositoryFolder, "sources/setup/workspace_template/javascript/ES6ShimScript.txt"))) { + System.err.println("Error: could not inject shim, continuing anyway because it is not required"); + } File epkCompiler = new File(repositoryFolder, "sources/setup/workspace_template/desktopRuntime/CompileEPK.jar"); @@ -374,8 +389,7 @@ public class CompileLatestClientGUI { File offlineDownloadGenerator = new File(repositoryFolder, "sources/setup/workspace_template/desktopRuntime/MakeOfflineDownload.jar"); MakeOfflineDownload.compilerMain(offlineDownloadGenerator, new String[] { (new File(repositoryFolder, "sources/setup/workspace_template/javascript/OfflineDownloadTemplate.txt")).getAbsolutePath(), - (new File(outputDirectory, "classes.js")).getAbsolutePath(), - (new File(outputDirectory, "assets.epk")).getAbsolutePath(), + classesJS.getAbsolutePath(), (new File(outputDirectory, "assets.epk")).getAbsolutePath(), (new File(outputDirectory, "EaglercraftX_1.8_Offline_en_US.html")).getAbsolutePath(), (new File(outputDirectory, "EaglercraftX_1.8_Offline_International.html")).getAbsolutePath(), (new File(outputDirectory, "build/languages.epk")).getAbsolutePath() diff --git a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/ES6Compat.java b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/ES6Compat.java new file mode 100644 index 00000000..87847e7b --- /dev/null +++ b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/ES6Compat.java @@ -0,0 +1,43 @@ +package net.lax1dude.eaglercraft.v1_8.buildtools.gui; + +import java.io.File; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.FileUtils; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ES6Compat { + + /** + * TODO: remove this when we update TeaVM to 0.10+ (ES6 is impossible) + */ + public static boolean patchClassesJS(File classesJS, File shimJS) { + try { + String dest = FileUtils.readFileToString(classesJS, StandardCharsets.UTF_8); + int i = dest.substring(0, dest.indexOf("=$rt_globals.Symbol('jsoClass');")).lastIndexOf("let "); + dest = dest.substring(0, i) + "var" + dest.substring(i + 3); + int j = dest.indexOf("function($rt_globals,$rt_exports){"); + dest = dest.substring(0, j + 34) + "\n" + FileUtils.readFileToString(shimJS, StandardCharsets.UTF_8) + "\n" + dest.substring(j + 34); + FileUtils.writeStringToFile(classesJS, dest, StandardCharsets.UTF_8); + return true; + }catch(Throwable t) { + t.printStackTrace(); + return false; + } + } + +} diff --git a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/TeaVMBinaries.java b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/TeaVMBinaries.java index 342e1631..0c64c86e 100644 --- a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/TeaVMBinaries.java +++ b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/TeaVMBinaries.java @@ -214,7 +214,7 @@ public class TeaVMBinaries { teavmClasslib.file.getAbsolutePath(), teavmInterop.file.getAbsolutePath(), teavmJSO.file.getAbsolutePath(), teavmJSOApis.file.getAbsolutePath(), teavmJSOImpl.file.getAbsolutePath(), teavmMetaprogrammingAPI.file.getAbsolutePath(), teavmMetaprogrammingImpl.file.getAbsolutePath(), - teavmPlatform.file.getAbsolutePath() }; + teavmPlatform.file.getAbsolutePath(), teavmCore.file.getAbsolutePath() }; } } diff --git a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/headless/CompileLatestClientHeadless.java b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/headless/CompileLatestClientHeadless.java index 25f243d4..60a53ef6 100644 --- a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/headless/CompileLatestClientHeadless.java +++ b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/gui/headless/CompileLatestClientHeadless.java @@ -25,6 +25,7 @@ import org.json.JSONObject; import net.lax1dude.eaglercraft.v1_8.buildtools.EaglerBuildTools; import net.lax1dude.eaglercraft.v1_8.buildtools.LicensePrompt; import net.lax1dude.eaglercraft.v1_8.buildtools.gui.EPKCompiler; +import net.lax1dude.eaglercraft.v1_8.buildtools.gui.ES6Compat; import net.lax1dude.eaglercraft.v1_8.buildtools.gui.JavaC; import net.lax1dude.eaglercraft.v1_8.buildtools.gui.MakeOfflineDownload; import net.lax1dude.eaglercraft.v1_8.buildtools.gui.TeaVMBinaries; @@ -60,7 +61,7 @@ public class CompileLatestClientHeadless { System.out.println(); System.out.println("Launching client compiler..."); - System.out.println("Copyright (c) 2022-2023 lax1dude"); + System.out.println("Copyright (c) 2022-2024 lax1dude"); System.out.println(); boolean yes = false; @@ -321,7 +322,11 @@ public class CompileLatestClientHeadless { try { compileResultCode = JavaC.runJavaC(new File(minecraftSrcTmp, "minecraft_src_javadoc.jar"), compiledResultClasses, temporaryDirectory, TeaVMBinaries.getTeaVMRuntimeClasspath(), - new File(repositoryFolder, "sources/main/java"), new File(repositoryFolder, "sources/teavm/java")); + new File(repositoryFolder, "sources/main/java"), + new File(repositoryFolder, "sources/protocol-game/java"), + new File(repositoryFolder, "sources/protocol-relay/java"), + new File(repositoryFolder, "sources/teavm/java"), + new File(repositoryFolder, "sources/teavm-boot-menu/java")); }catch(IOException ex) { throw new CompileFailureException("failed to run javac compiler! " + ex.toString(), ex); } @@ -362,10 +367,12 @@ public class CompileLatestClientHeadless { teavmClassPath.addAll(Arrays.asList(TeaVMBinaries.getTeaVMRuntimeClasspath())); teavmArgs.put("classPathEntries", teavmClassPath); + teavmArgs.put("compileClassPathEntries", Arrays.asList((new File(repositoryFolder, "sources/teavmc-classpath/resources")).getAbsolutePath())); + teavmArgs.put("entryPointName", "main"); teavmArgs.put("mainClass", "net.lax1dude.eaglercraft.v1_8.internal.teavm.MainClass"); teavmArgs.put("minifying", minifying); - teavmArgs.put("optimizationLevel", "ADVANCED"); + teavmArgs.put("optimizationLevel", "FULL"); teavmArgs.put("targetDirectory", outputDirectory.getAbsolutePath()); teavmArgs.put("generateSourceMaps", writeSourceMap); teavmArgs.put("targetFileName", "classes.js"); @@ -388,6 +395,15 @@ public class CompileLatestClientHeadless { return; } + System.out.println(); + System.out.println("Patching classes.js with ES6 shim..."); + + File classesJS = new File(outputDirectory, "classes.js"); + + if(!ES6Compat.patchClassesJS(classesJS, new File(repositoryFolder, "sources/setup/workspace_template/javascript/ES6ShimScript.txt"))) { + System.err.println("Error: could not inject shim, continuing anyway because it is not required"); + } + File epkCompiler = new File(repositoryFolder, "sources/setup/workspace_template/desktopRuntime/CompileEPK.jar"); if(!epkCompiler.exists()) { @@ -569,8 +585,7 @@ public class CompileLatestClientHeadless { File offlineDownloadGenerator = new File(repositoryFolder, "sources/setup/workspace_template/desktopRuntime/MakeOfflineDownload.jar"); MakeOfflineDownload.compilerMain(offlineDownloadGenerator, new String[] { offlineTemplateArg.getAbsolutePath(), - (new File(outputDirectory, "classes.js")).getAbsolutePath(), - (new File(outputDirectory, "assets.epk")).getAbsolutePath(), + classesJS.getAbsolutePath(), (new File(outputDirectory, "assets.epk")).getAbsolutePath(), (new File(outputDirectory, "EaglercraftX_1.8_Offline_en_US.html")).getAbsolutePath(), (new File(outputDirectory, "EaglercraftX_1.8_Offline_International.html")).getAbsolutePath(), (new File(temporaryDirectory, "languages.epk")).getAbsolutePath() diff --git a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/diff/PullRequestTask.java b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/diff/PullRequestTask.java index 3d370974..e8833062 100644 --- a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/diff/PullRequestTask.java +++ b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/diff/PullRequestTask.java @@ -10,6 +10,7 @@ import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharsetDecoder; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -65,13 +66,22 @@ public class PullRequestTask { File originalSourceMainJar = new File(EaglerBuildToolsConfig.getTemporaryDirectory(), "MinecraftSrc/minecraft_src_patch.jar"); File minecraftJavadocTmp = new File(EaglerBuildToolsConfig.getTemporaryDirectory(), "MinecraftSrc/minecraft_src_javadoc.jar"); File originalSourceMain = new File(EaglerBuildTools.repositoryRoot, "sources/main/java"); + File originalSourceProtoGame = new File(EaglerBuildTools.repositoryRoot, "sources/protocol-game/java"); + File originalSourceProtoRelay = new File(EaglerBuildTools.repositoryRoot, "sources/protocol-relay/java"); File originalSourceTeaVM = new File(EaglerBuildTools.repositoryRoot, "sources/teavm/java"); + File originalSourceTeaVMC = new File(EaglerBuildTools.repositoryRoot, "sources/teavmc-classpath/resources"); + File originalSourceBootMenu = new File(EaglerBuildTools.repositoryRoot, "sources/teavm-boot-menu/java"); File originalSourceLWJGL = new File(EaglerBuildTools.repositoryRoot, "sources/lwjgl/java"); File originalUnpatchedSourceResourcesJar = new File(EaglerBuildToolsConfig.getTemporaryDirectory(), "MinecraftSrc/minecraft_res.jar"); File originalSourceResourcesJar = new File(EaglerBuildToolsConfig.getTemporaryDirectory(), "MinecraftSrc/minecraft_res_patch.jar"); File originalSourceResources = new File(EaglerBuildTools.repositoryRoot, "sources/resources"); File diffFromMain = new File(EaglerBuildToolsConfig.getWorkspaceDirectory(), "src/main/java"); + File diffFromGame = new File(EaglerBuildToolsConfig.getWorkspaceDirectory(), "src/game/java"); + File diffFromProtoGame = new File(EaglerBuildToolsConfig.getWorkspaceDirectory(), "src/protocol-game/java"); + File diffFromProtoRelay = new File(EaglerBuildToolsConfig.getWorkspaceDirectory(), "src/protocol-relay/java"); File diffFromTeaVM = new File(EaglerBuildToolsConfig.getWorkspaceDirectory(), "src/teavm/java"); + File diffFromBootMenu = new File(EaglerBuildToolsConfig.getWorkspaceDirectory(), "src/teavm-boot-menu/java"); + File diffFromTeaVMC = new File(EaglerBuildToolsConfig.getWorkspaceDirectory(), "src/teavmc-classpath/resources"); File diffFromLWJGL = new File(EaglerBuildToolsConfig.getWorkspaceDirectory(), "src/lwjgl/java"); File diffFromResources = new File(EaglerBuildToolsConfig.getWorkspaceDirectory(), "desktopRuntime/resources"); File pullRequestTo = new File(EaglerBuildTools.repositoryRoot, "pullrequest"); @@ -111,24 +121,54 @@ public class PullRequestTask { File pullRequestToResources = new File(pullRequestTo, "resources"); boolean flag = false; - int i = copyAllModified(diffFromTeaVM, originalSourceTeaVM); + int i = copyAllModified(diffFromMain, originalSourceMain); + if(i > 0) { + flag = true; + } + System.out.println("Found " + i + " changed files in /src/main/java/"); + + i = copyAllModified(diffFromProtoGame, originalSourceProtoGame); + if(i > 0) { + flag = true; + } + System.out.println("Found " + i + " changed files in /src/protocol-game/java/"); + + i = copyAllModified(diffFromProtoRelay, originalSourceProtoRelay); + if(i > 0) { + flag = true; + } + System.out.println("Found " + i + " changed files in /src/protocol-relay/java/"); + + i = copyAllModified(diffFromTeaVM, originalSourceTeaVM); if(i > 0) { flag = true; } System.out.println("Found " + i + " changed files in /src/teavm/java/"); + i = copyAllModified(diffFromBootMenu, originalSourceBootMenu); + if(i > 0) { + flag = true; + } + System.out.println("Found " + i + " changed files in /src/teavm-boot-menu/java/"); + + i = copyAllModified(diffFromTeaVMC, originalSourceTeaVMC); + if(i > 0) { + flag = true; + } + System.out.println("Found " + i + " changed files in /src/teavmc-classpath/resources/"); + i = copyAllModified(diffFromLWJGL, originalSourceLWJGL); if(i > 0) { flag = true; } System.out.println("Found " + i + " changed files in /src/lwjgl/java/"); - i = createDiffFiles(originalSourceMain, minecraftJavadocTmp, originalUnpatchedSourceMainJar, - originalSourceMainJar, diffFromMain, pullRequestToMain, true); + i = createDiffFiles(null, minecraftJavadocTmp, originalUnpatchedSourceMainJar, + originalSourceMainJar, diffFromGame, pullRequestToMain, true); if(i > 0) { flag = true; } - System.out.println("Found " + i + " changed files in /src/main/java/"); + System.out.println("Found " + i + " changed files in /src/game/java/"); i = createDiffFiles(originalSourceResources, originalSourceResourcesJar, originalUnpatchedSourceResourcesJar, null, diffFromResources, pullRequestToResources, false); @@ -197,9 +237,9 @@ public class PullRequestTask { if(newPath.startsWith("/")) { newPath = newPath.substring(1); } - File orig = new File(folderOriginal, newPath); + File orig = folderOriginal != null ? new File(folderOriginal, newPath) : null; byte[] jarData = null; - boolean replacedFileExists = orig.exists(); + boolean replacedFileExists = orig != null && orig.exists(); if(replacedFileExists) { filesReplaced.add(newPath); if(copyFileIfChanged(wf, orig)) { @@ -253,9 +293,13 @@ public class PullRequestTask { ++cnt; } }else { - filesReplaced.add(newPath); - FileUtils.copyFile(wf, orig); - ++cnt; + if(folderOriginal == null) { + System.err.println("Detected a new file in src/game/java, it will be ignored! Do not created new files! (" + newPath + ")"); + }else { + filesReplaced.add(newPath); + FileUtils.copyFile(wf, orig); + ++cnt; + } } } @@ -278,6 +322,10 @@ public class PullRequestTask { } } + if(folderOriginal != null) { + cnt += removeAllDeleted(folderEdited, folderOriginal); + } + return cnt; } @@ -344,6 +392,33 @@ public class PullRequestTask { ++cnt; } } + cnt += removeAllDeleted(inDir, outDir); + return cnt; + } + + private static int removeAllDeleted(File inDir, File outDir) throws IOException { + if(!inDir.isDirectory()) { + return 0; + } + int cnt = 0; + Collection existingFiles = FileUtils.listFiles(outDir, null, true); + String existingPrefix = outDir.getAbsolutePath(); + for(File wf : existingFiles) { + String editedPath = wf.getAbsolutePath().replace(existingPrefix, ""); + if(editedPath.indexOf('\\') != -1) { + editedPath = editedPath.replace('\\', '/'); + } + if(editedPath.startsWith("/")) { + editedPath = editedPath.substring(1); + } + File edited = new File(inDir, editedPath); + if(!edited.isFile()) { + if(!wf.delete()) { + throw new IOException("Could not delete file: " + wf.getAbsolutePath()); + } + ++cnt; + } + } return cnt; } @@ -392,15 +467,6 @@ public class PullRequestTask { return hex32(crc.getValue()); } - private static boolean checkCRC32(File in1, File in2) throws IOException { - CRC32 crc = new CRC32(); - crc.update(FileUtils.readFileToByteArray(in1)); - long v1 = crc.getValue(); - crc.reset(); - crc.update(FileUtils.readFileToByteArray(in2)); - return v1 != crc.getValue(); - } - private static boolean copyFileIfChanged(File in1, File in2) throws IOException { if(!in2.exists()) { FileUtils.copyFile(in1, in2); @@ -409,20 +475,14 @@ public class PullRequestTask { if(in1.lastModified() == in2.lastModified()) { return false; } - CRC32 crc = new CRC32(); byte[] f1 = FileUtils.readFileToByteArray(in1); - crc.update(f1); - long v1 = crc.getValue(); - crc.reset(); byte[] f2 = FileUtils.readFileToByteArray(in2); - crc.update(f2); - if(v1 != crc.getValue()) { - //System.out.println("changed: " + in1.getAbsolutePath()); + if(!Arrays.equals(f1, f2)) { FileUtils.writeByteArrayToFile(in2, f1); return true; }else { return false; } } - + } diff --git a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/init/SetupWorkspace.java b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/init/SetupWorkspace.java index 26492e2f..10fea86f 100644 --- a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/init/SetupWorkspace.java +++ b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/init/SetupWorkspace.java @@ -122,13 +122,22 @@ public class SetupWorkspace { File repoSources = new File("./sources"); File repoSourcesSetup = new File(repoSources, "setup/workspace_template"); - File repoSourcesGame = new File(repoSources, "main/java"); + File repoSourcesMain = new File(repoSources, "main/java"); File repoSourcesTeaVM = new File(repoSources, "teavm/java"); File repoSourcesLWJGL = new File(repoSources, "lwjgl/java"); + File repoSourcesProtoGame = new File(repoSources, "protocol-game/java"); + File repoSourcesProtoRelay = new File(repoSources, "protocol-relay/java"); + File repoSourcesBootMenu = new File(repoSources, "teavm-boot-menu/java"); + File repoSourcesTeavmCRes = new File(repoSources, "teavmc-classpath/resources"); File repoSourcesResources = new File(repoSources, "resources"); File srcMainJava = new File(workspaceDirectory, "src/main/java"); + File srcGameJava = new File(workspaceDirectory, "src/game/java"); File srcLWJGLJava = new File(workspaceDirectory, "src/lwjgl/java"); File srcTeaVMJava = new File(workspaceDirectory, "src/teavm/java"); + File srcProtoGame = new File(workspaceDirectory, "src/protocol-game/java"); + File srcProtoRelay = new File(workspaceDirectory, "src/protocol-relay/java"); + File srcBootMenu = new File(workspaceDirectory, "src/teavm-boot-menu/java"); + File srcTeavmCRes = new File(workspaceDirectory, "src/teavmc-classpath/resources"); File resourcesExtractTo = new File(workspaceDirectory, "desktopRuntime/resources"); File mcLanguagesZip = new File(mcTmpDirectory, "minecraft_languages.zip"); File mcLanguagesExtractTo = new File(workspaceDirectory, "javascript/lang"); @@ -174,11 +183,55 @@ public class SetupWorkspace { System.out.println("Copying files from \"/sources/main/java/\" to workspace..."); try { - FileUtils.copyDirectory(repoSourcesGame, srcMainJava); + FileUtils.copyDirectory(repoSourcesMain, srcMainJava); }catch(IOException ex) { System.err.println("ERROR: could not copy \"/sources/main/java/\" to \"" + srcMainJava.getAbsolutePath() + "\"!"); throw ex; } + + if(repoSourcesProtoGame.isDirectory()) { + System.out.println("Copying files from \"/sources/protocol-game/java/\" to workspace..."); + + try { + FileUtils.copyDirectory(repoSourcesProtoGame, srcProtoGame); + }catch(IOException ex) { + System.err.println("ERROR: could not copy \"/sources/protocol-game/java/\" to \"" + srcProtoGame.getAbsolutePath() + "\"!"); + throw ex; + } + } + + if(repoSourcesProtoRelay.isDirectory()) { + System.out.println("Copying files from \"/sources/protocol-relay/java/\" to workspace..."); + + try { + FileUtils.copyDirectory(repoSourcesProtoRelay, srcProtoRelay); + }catch(IOException ex) { + System.err.println("ERROR: could not copy \"/sources/protocol-relay/java/\" to \"" + srcProtoRelay.getAbsolutePath() + "\"!"); + throw ex; + } + } + + if(repoSourcesTeavmCRes.isDirectory()) { + System.out.println("Copying files from \"/sources/teavmc-classpath/resources/\" to workspace..."); + + try { + FileUtils.copyDirectory(repoSourcesTeavmCRes, srcTeavmCRes); + }catch(IOException ex) { + System.err.println("ERROR: could not copy \"/sources/teavmc-classpath/resources/\" to \"" + srcTeavmCRes.getAbsolutePath() + "\"!"); + throw ex; + } + } + + if(repoSourcesBootMenu.isDirectory()) { + System.out.println("Copying files from \"/sources/teavm-boot-menu/java/\" to workspace..."); + + try { + FileUtils.copyDirectory(repoSourcesBootMenu, srcBootMenu); + }catch(IOException ex) { + System.err.println("ERROR: could not copy \"/sources/teavm-boot-menu/java/\" to \"" + srcBootMenu.getAbsolutePath() + "\"!"); + throw ex; + } + } if(repoSourcesLWJGL.isDirectory()) { System.out.println("Copying files from \"/sources/lwjgl/java/\" to workspace..."); @@ -255,18 +308,18 @@ public class SetupWorkspace { minecraftResJar = tmpPatchedPatchResOut; }else { - System.out.println("Extracting files from \"minecraft_src_javadoc.jar\" to \"/src/main/java/\"..."); + System.out.println("Extracting files from \"minecraft_src_javadoc.jar\" to \"/src/game/java/\"..."); } try { - if(!srcMainJava.isDirectory() && !srcMainJava.mkdirs()) { + if(!srcGameJava.isDirectory() && !srcGameJava.mkdirs()) { System.err.println("ERROR: Could not create destination directory!"); return false; } - extractJarTo(minecraftJavadocTmp, srcMainJava); + extractJarTo(minecraftJavadocTmp, srcGameJava); }catch(IOException ex) { System.err.println("ERROR: could not extract \"" + minecraftJavadocTmp.getName() + ".jar\" to \"" + - srcMainJava.getAbsolutePath() + "\"!"); + srcGameJava.getAbsolutePath() + "\"!"); throw ex; } @@ -364,8 +417,11 @@ public class SetupWorkspace { dotClasspathFile = dotClasspathFile.replace("${LIBRARY_CLASSPATH}", String.join(System.lineSeparator(), classpathEntries)); FileUtils.writeStringToFile(new File(desktopRuntimeProjectDir, ".classpath"), dotClasspathFile, "UTF-8"); - dotProjectFile = dotProjectFile.replace("${LWJGL_SRC_FOLDER}", bsToS((new File(workspaceDirectory, "src/lwjgl/java")).getAbsolutePath())); dotProjectFile = dotProjectFile.replace("${MAIN_SRC_FOLDER}", bsToS((new File(workspaceDirectory, "src/main/java")).getAbsolutePath())); + dotProjectFile = dotProjectFile.replace("${GAME_SRC_FOLDER}", bsToS((new File(workspaceDirectory, "src/game/java")).getAbsolutePath())); + dotProjectFile = dotProjectFile.replace("${PROTO_GAME_SRC_FOLDER}", bsToS((new File(workspaceDirectory, "src/protocol-game/java")).getAbsolutePath())); + dotProjectFile = dotProjectFile.replace("${PROTO_RELAY_SRC_FOLDER}", bsToS((new File(workspaceDirectory, "src/protocol-relay/java")).getAbsolutePath())); + dotProjectFile = dotProjectFile.replace("${LWJGL_SRC_FOLDER}", bsToS((new File(workspaceDirectory, "src/lwjgl/java")).getAbsolutePath())); FileUtils.writeStringToFile(new File(desktopRuntimeProjectDir, ".project"), dotProjectFile, "UTF-8"); debugRuntimeLaunchConfig = debugRuntimeLaunchConfig.replace("${MAIN_CLASS_FILE}", mainClassConfFile); diff --git a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/teavm/TeaVMBridge.java b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/teavm/TeaVMBridge.java index d9a8d4fb..95f7c216 100644 --- a/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/teavm/TeaVMBridge.java +++ b/buildtools/src/main/java/net/lax1dude/eaglercraft/v1_8/buildtools/task/teavm/TeaVMBridge.java @@ -7,6 +7,9 @@ import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Map; import net.lax1dude.eaglercraft.v1_8.buildtools.gui.TeaVMBinaries; @@ -33,6 +36,7 @@ public class TeaVMBridge { /** *

List of required options:

* + * * * * @@ -45,14 +49,20 @@ public class TeaVMBridge { *
*/ public static boolean compileTeaVM(Map options) throws TeaVMClassLoadException, TeaVMRuntimeException { - File[] cp = TeaVMBinaries.getTeaVMCompilerClasspath(); - URL[] urls = new URL[cp.length]; + List philes = new ArrayList<>(); + List things = (List)options.get("compileClassPathEntries"); + for(int i = 0, l = things.size(); i < l; ++i) { + philes.add(new File(things.get(i))); + } + philes.addAll(Arrays.asList(TeaVMBinaries.getTeaVMCompilerClasspath())); - for(int i = 0; i < cp.length; ++i) { + URL[] urls = new URL[philes.size()]; + + for(int i = 0; i < urls.length; ++i) { try { - urls[i] = cp[i].toURI().toURL(); + urls[i] = philes.get(i).toURI().toURL(); } catch (MalformedURLException e) { - throw new TeaVMClassLoadException("Could not resolve URL for: " + cp[i].getAbsolutePath(), e); + throw new TeaVMClassLoadException("Could not resolve URL for: " + philes.get(i).getAbsolutePath(), e); } } diff --git a/client_version b/client_version index b9b626ab..3b26bf81 100644 --- a/client_version +++ b/client_version @@ -1 +1 @@ -u36 \ No newline at end of file +u37 \ No newline at end of file diff --git a/patches/minecraft/delete.txt b/patches/minecraft/delete.txt index aa1178f8..4e6a55a7 100644 --- a/patches/minecraft/delete.txt +++ b/patches/minecraft/delete.txt @@ -1,4 +1,4 @@ -# 144 files to delete: +# 145 files to delete: net/minecraft/client/renderer/VertexBufferUploader.java net/minecraft/realms/DisconnectedRealmsScreen.java net/minecraft/client/stream/Metadata.java @@ -45,6 +45,7 @@ net/minecraft/network/NettyEncryptingDecoder.java net/minecraft/server/integrated/IntegratedPlayerList.java net/minecraft/client/renderer/WorldVertexBufferUploader.java net/minecraft/network/PingResponseHandler.java +net/minecraft/profiler/Profiler.java net/minecraft/client/stream/NullStream.java net/minecraft/network/NetworkSystem.java net/minecraft/client/shader/Shader.java diff --git a/patches/minecraft/net/minecraft/client/LoadingScreenRenderer.edit.java b/patches/minecraft/net/minecraft/client/LoadingScreenRenderer.edit.java index f2245707..3d8a1639 100644 --- a/patches/minecraft/net/minecraft/client/LoadingScreenRenderer.edit.java +++ b/patches/minecraft/net/minecraft/client/LoadingScreenRenderer.edit.java @@ -18,15 +18,14 @@ ~ import net.minecraft.client.resources.I18n; -> DELETE 10 @ 10 : 11 +> DELETE 9 @ 9 : 11 -> DELETE 4 @ 4 : 6 +> DELETE 3 @ 3 : 6 -> CHANGE 22 : 25 @ 22 : 32 +> CHANGE 22 : 24 @ 22 : 32 -~ ScaledResolution scaledresolution = new ScaledResolution(this.mc); -~ GlStateManager.ortho(0.0D, scaledresolution.getScaledWidth_double(), -~ scaledresolution.getScaledHeight_double(), 0.0D, 100.0D, 300.0D); +~ GlStateManager.ortho(0.0D, mc.scaledResolution.getScaledWidth_double(), +~ mc.scaledResolution.getScaledHeight_double(), 0.0D, 100.0D, 300.0D); > INSERT 19 : 37 @ 19 @@ -49,7 +48,11 @@ + } + -> CHANGE 13 : 14 @ 13 : 20 +> CHANGE 9 : 10 @ 9 : 10 + +~ ScaledResolution scaledresolution = mc.scaledResolution; + +> CHANGE 3 : 4 @ 3 : 10 ~ GlStateManager.clear(256); @@ -58,6 +61,13 @@ ~ GlStateManager.clear(16640); ~ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); -> DELETE 44 @ 44 : 49 +> CHANGE 41 : 45 @ 41 : 47 + +~ if (this.message != null) { +~ this.mc.fontRendererObj.drawStringWithShadow(this.message, +~ (float) ((k - this.mc.fontRendererObj.getStringWidth(this.message)) / 2), +~ (float) (l / 2 - 4 + 8), 16777215); + +> DELETE 1 @ 1 : 2 > EOF diff --git a/patches/minecraft/net/minecraft/client/Minecraft.edit.java b/patches/minecraft/net/minecraft/client/Minecraft.edit.java index b6723cde..1d14e426 100644 --- a/patches/minecraft/net/minecraft/client/Minecraft.edit.java +++ b/patches/minecraft/net/minecraft/client/Minecraft.edit.java @@ -10,9 +10,7 @@ ~ import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL._wglBindFramebuffer; ~ -> DELETE 2 @ 2 : 6 - -> DELETE 1 @ 1 : 2 +> DELETE 2 @ 2 : 8 > CHANGE 2 : 3 @ 2 : 6 @@ -20,15 +18,10 @@ > DELETE 1 @ 1 : 4 -> CHANGE 1 : 56 @ 1 : 4 +> CHANGE 1 : 74 @ 1 : 4 ~ -~ import net.lax1dude.eaglercraft.v1_8.internal.PlatformInput; -~ -~ import org.apache.commons.lang3.Validate; -~ -~ import com.google.common.collect.Lists; -~ +~ import net.lax1dude.eaglercraft.v1_8.ClientUUIDLoadingCache; ~ import net.lax1dude.eaglercraft.v1_8.Display; ~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ~ import net.lax1dude.eaglercraft.v1_8.EagUtils; @@ -37,16 +30,30 @@ ~ import net.lax1dude.eaglercraft.v1_8.IOUtils; ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; ~ import net.lax1dude.eaglercraft.v1_8.Mouse; +~ import net.lax1dude.eaglercraft.v1_8.PauseMenuCustomizeState; +~ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; +~ import net.lax1dude.eaglercraft.v1_8.Touch; +~ import net.lax1dude.eaglercraft.v1_8.cookie.ServerCookieDataStore; +~ import net.lax1dude.eaglercraft.v1_8.internal.PlatformInput; +~ +~ import org.apache.commons.lang3.Validate; +~ +~ import com.google.common.collect.Lists; +~ ~ import net.lax1dude.eaglercraft.v1_8.futures.Executors; ~ import net.lax1dude.eaglercraft.v1_8.futures.FutureTask; ~ import net.lax1dude.eaglercraft.v1_8.futures.ListenableFuture; ~ import net.lax1dude.eaglercraft.v1_8.futures.ListenableFutureTask; ~ import net.lax1dude.eaglercraft.v1_8.internal.EnumPlatformType; ~ import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; +~ import net.lax1dude.eaglercraft.v1_8.internal.PlatformWebRTC; ~ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; ~ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; ~ import net.lax1dude.eaglercraft.v1_8.minecraft.EaglerFolderResourcePack; ~ import net.lax1dude.eaglercraft.v1_8.minecraft.EaglerFontRenderer; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.GuiScreenGenericErrorMessage; +~ import net.lax1dude.eaglercraft.v1_8.notifications.ServerNotificationRenderer; ~ import net.lax1dude.eaglercraft.v1_8.opengl.EaglerMeshLoader; ~ import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; @@ -61,6 +68,7 @@ ~ import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.texture.MetalsLUT; ~ import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.texture.PBRTextureMapUtils; ~ import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.texture.TemperaturesLUT; +~ import net.lax1dude.eaglercraft.v1_8.profanity_filter.GuiScreenContentWarning; ~ import net.lax1dude.eaglercraft.v1_8.profile.EaglerProfile; ~ import net.lax1dude.eaglercraft.v1_8.profile.GuiScreenEditProfile; ~ import net.lax1dude.eaglercraft.v1_8.profile.SkinPreviewRenderer; @@ -74,9 +82,17 @@ ~ import net.lax1dude.eaglercraft.v1_8.sp.gui.GuiScreenIntegratedServerBusy; ~ import net.lax1dude.eaglercraft.v1_8.sp.gui.GuiScreenSingleplayerConnecting; ~ import net.lax1dude.eaglercraft.v1_8.sp.lan.LANServerController; +~ import net.lax1dude.eaglercraft.v1_8.touch_gui.TouchControls; +~ import net.lax1dude.eaglercraft.v1_8.touch_gui.TouchOverlayRenderer; +~ import net.lax1dude.eaglercraft.v1_8.update.GuiUpdateDownloadSuccess; +~ import net.lax1dude.eaglercraft.v1_8.update.GuiUpdateInstallOptions; ~ import net.lax1dude.eaglercraft.v1_8.update.RelayUpdateChecker; +~ import net.lax1dude.eaglercraft.v1_8.update.UpdateDataObj; +~ import net.lax1dude.eaglercraft.v1_8.update.UpdateResultObj; +~ import net.lax1dude.eaglercraft.v1_8.update.UpdateService; ~ import net.lax1dude.eaglercraft.v1_8.voice.GuiVoiceOverlay; ~ import net.lax1dude.eaglercraft.v1_8.voice.VoiceClientController; +~ import net.lax1dude.eaglercraft.v1_8.webview.WebViewOverlayController; > DELETE 2 @ 2 : 4 @@ -114,9 +130,7 @@ > DELETE 8 @ 8 : 12 -> DELETE 1 @ 1 : 3 - -> DELETE 1 @ 1 : 3 +> DELETE 1 @ 1 : 6 > INSERT 6 : 9 @ 6 @@ -124,7 +138,11 @@ + import net.minecraft.util.ChatStyle; + import net.minecraft.util.EnumChatFormatting; -> INSERT 11 : 12 @ 11 +> INSERT 7 : 8 @ 7 + ++ import net.minecraft.util.MovingObjectPosition.MovingObjectType; + +> INSERT 4 : 5 @ 4 + import net.minecraft.util.StringTranslate; @@ -140,7 +158,11 @@ ~ public static final boolean isRunningOnMac = false; -> DELETE 12 @ 12 : 14 +> INSERT 10 : 11 @ 10 + ++ public float displayDPI; + +> DELETE 2 @ 2 : 4 > INSERT 11 : 12 @ 11 @@ -156,11 +178,13 @@ ~ private EaglercraftNetworkManager myNetworkManager; -> DELETE 9 @ 9 : 11 +> DELETE 1 @ 1 : 2 + +> DELETE 7 @ 7 : 9 > CHANGE 4 : 5 @ 4 : 7 -~ private final List> scheduledTasks = new LinkedList(); +~ private final List> scheduledTasks = new LinkedList<>(); > INSERT 14 : 18 @ 14 @@ -169,15 +193,20 @@ + public int bungeeOutdatedMsgTimer = 0; + private boolean isLANOpen = false; -> INSERT 1 : 9 @ 1 +> INSERT 1 : 14 @ 1 + public SkullCommand eagskullCommand; + + public GuiVoiceOverlay voiceOverlay; ++ public ServerNotificationRenderer notifRenderer; ++ public TouchOverlayRenderer touchOverlayRenderer; + + public float startZoomValue = 18.0f; + public float adjustedZoomValue = 18.0f; + public boolean isZoomKey = false; ++ private String reconnectURI = null; ++ public boolean mouseGrabSupported = false; ++ public ScaledResolution scaledResolution = null; + > CHANGE 2 : 3 @ 2 : 5 @@ -192,7 +221,11 @@ ~ logger.info("Setting user: " + this.session.getProfile().getName()); -> CHANGE 6 : 11 @ 6 : 10 +> INSERT 2 : 3 @ 2 + ++ this.displayDPI = 1.0f; + +> CHANGE 4 : 9 @ 4 : 8 ~ String serverToJoin = EagRuntime.getConfiguration().getServerToJoin(); ~ if (serverToJoin != null) { @@ -247,16 +280,20 @@ ~ EaglerFolderResourcePack.deleteOldResourcePacks(EaglerFolderResourcePack.SERVER_RESOURCE_PACKS, 604800000L); ~ this.mcResourcePackRepository = new ResourcePackRepository(this.mcDefaultResourcePack, this.metadataSerializer_, -> DELETE 8 @ 8 : 11 +> INSERT 4 : 5 @ 4 + ++ this.scaledResolution = new ScaledResolution(this); + +> DELETE 4 @ 4 : 7 > CHANGE 3 : 5 @ 3 : 5 -~ this.fontRendererObj = new EaglerFontRenderer(this.gameSettings, +~ this.fontRendererObj = EaglerFontRenderer.createSupportedFontRenderer(this.gameSettings, ~ new ResourceLocation("textures/font/ascii.png"), this.renderEngine, false); > CHANGE 5 : 6 @ 5 : 6 -~ this.standardGalacticFontRenderer = new EaglerFontRenderer(this.gameSettings, +~ this.standardGalacticFontRenderer = EaglerFontRenderer.createSupportedFontRenderer(this.gameSettings, > INSERT 5 : 12 @ 5 @@ -284,15 +321,25 @@ + SkinPreviewRenderer.initialize(); -> INSERT 2 : 14 @ 2 +> INSERT 2 : 24 @ 2 ++ ++ this.mouseGrabSupported = Mouse.isMouseGrabSupported(); ++ PointerInputAbstraction.init(this); ++ + this.eagskullCommand = new SkullCommand(this); + this.voiceOverlay = new GuiVoiceOverlay(this); -+ ScaledResolution voiceRes = new ScaledResolution(this); -+ this.voiceOverlay.setResolution(voiceRes.getScaledWidth(), voiceRes.getScaledHeight()); ++ this.voiceOverlay.setResolution(scaledResolution.getScaledWidth(), scaledResolution.getScaledHeight()); ++ ++ this.notifRenderer = new ServerNotificationRenderer(); ++ this.notifRenderer.init(); ++ this.notifRenderer.setResolution(this, scaledResolution.getScaledWidth(), scaledResolution.getScaledHeight(), ++ scaledResolution.getScaleFactor()); ++ this.touchOverlayRenderer = new TouchOverlayRenderer(this); + + ServerList.initServerList(this); + EaglerProfile.read(); ++ ServerCookieDataStore.load(); + + GuiScreen mainMenu = new GuiMainMenu(); + if (isDemo()) { @@ -303,24 +350,35 @@ ~ mainMenu = new GuiConnecting(mainMenu, this, this.serverName, this.serverPort); -> INSERT 2 : 4 @ 2 +> INSERT 2 : 10 @ 2 -+ this.displayGuiScreen(new GuiScreenEditProfile(mainMenu)); ++ mainMenu = new GuiScreenEditProfile(mainMenu); ++ ++ if (!EagRuntime.getConfiguration().isForceProfanityFilter() && !gameSettings.hasShownProfanityFilter) { ++ mainMenu = new GuiScreenContentWarning(mainMenu); ++ } ++ ++ this.displayGuiScreen(mainMenu); + -> DELETE 3 @ 3 : 15 +> DELETE 3 @ 3 : 6 -> CHANGE 16 : 17 @ 16 : 24 +> CHANGE 1 : 7 @ 1 : 9 -~ throw new UnsupportedOperationException("wtf u trying to twitch stream in a browser game?"); +~ while (Mouse.next()) +~ ; +~ while (Keyboard.next()) +~ ; +~ while (Touch.next()) +~ ; -> CHANGE 2 : 5 @ 2 : 24 +> CHANGE 15 : 18 @ 15 : 24 ~ private void createDisplay() { ~ Display.create(); ~ Display.setTitle("Eaglercraft 1.8.8"); -> DELETE 2 @ 2 : 39 +> DELETE 2 @ 2 : 63 > CHANGE 1 : 2 @ 1 : 11 @@ -363,41 +421,68 @@ + GuiMainMenu.doResourceReloadHack(); + -> CHANGE 7 : 10 @ 7 : 19 +> CHANGE 7 : 12 @ 7 : 19 ~ private void updateDisplayMode() { ~ this.displayWidth = Display.getWidth(); ~ this.displayHeight = Display.getHeight(); +~ this.displayDPI = Display.getDPI(); +~ this.scaledResolution = new ScaledResolution(this); -> CHANGE 2 : 6 @ 2 : 46 +> CHANGE 2 : 6 @ 2 : 51 ~ private void drawSplashScreen(TextureManager textureManagerInstance) { ~ Display.update(); ~ updateDisplayMode(); ~ GlStateManager.viewport(0, 0, displayWidth, displayHeight); -> DELETE 2 @ 2 : 5 +> CHANGE 2 : 4 @ 2 : 4 -> CHANGE 16 : 17 @ 16 : 17 +~ GlStateManager.ortho(0.0D, (double) scaledResolution.getScaledWidth(), +~ (double) scaledResolution.getScaledHeight(), 0.0D, 1000.0D, 3000.0D); + +> CHANGE 12 : 13 @ 12 : 13 ~ new DynamicTexture(ImageData.loadImageFile(inputstream))); -> DELETE 24 @ 24 : 26 +> CHANGE 20 : 22 @ 20 : 22 + +~ this.func_181536_a((scaledResolution.getScaledWidth() - short1) / 2, +~ (scaledResolution.getScaledHeight() - short2) / 2, 0, 0, short1, short2, 255, 255, 255, 255); + +> DELETE 2 @ 2 : 4 > DELETE 26 @ 26 : 30 -> CHANGE 31 : 42 @ 31 : 32 +> INSERT 17 : 18 @ 17 + ++ this.scaledResolution = new ScaledResolution(this); + +> CHANGE 2 : 4 @ 2 : 6 + +~ ((GuiScreen) guiScreenIn).setWorldAndResolution(this, scaledResolution.getScaledWidth(), +~ scaledResolution.getScaledHeight()); + +> INSERT 5 : 9 @ 5 + ++ EagRuntime.getConfiguration().getHooks().callScreenChangedHook( ++ currentScreen != null ? currentScreen.getClass().getName() : null, scaledResolution.getScaledWidth(), ++ scaledResolution.getScaledHeight(), displayWidth, displayHeight, scaledResolution.getScaleFactor()); ++ } + +> INSERT 1 : 9 @ 1 + ++ public void shutdownIntegratedServer(GuiScreen cont) { ++ if (SingleplayerServerController.shutdownEaglercraftServer() ++ || SingleplayerServerController.getStatusState() == IntegratedServerState.WORLD_UNLOADING) { ++ displayGuiScreen(new GuiScreenIntegratedServerBusy(cont, "singleplayer.busy.stoppingIntegratedServer", ++ "singleplayer.failed.stoppingIntegratedServer", SingleplayerServerController::isReady)); ++ } else { ++ displayGuiScreen(cont); ++ } + +> CHANGE 2 : 3 @ 2 : 3 -~ public void shutdownIntegratedServer(GuiScreen cont) { -~ if (SingleplayerServerController.shutdownEaglercraftServer() -~ || SingleplayerServerController.getStatusState() == IntegratedServerState.WORLD_UNLOADING) { -~ displayGuiScreen(new GuiScreenIntegratedServerBusy(cont, "singleplayer.busy.stoppingIntegratedServer", -~ "singleplayer.failed.stoppingIntegratedServer", SingleplayerServerController::isReady)); -~ } else { -~ displayGuiScreen(cont); -~ } -~ } -~ ~ public void checkGLError(String message) { > CHANGE 1 : 2 @ 1 : 2 @@ -435,79 +520,77 @@ > DELETE 3 @ 3 : 5 -> CHANGE 5 : 6 @ 5 : 6 +> CHANGE 4 : 5 @ 4 : 6 ~ if (Display.isCloseRequested()) { -> CHANGE 14 : 15 @ 14 : 15 +> INSERT 3 : 6 @ 3 + ++ PointerInputAbstraction.runGameLoop(); ++ this.gameSettings.touchscreen = PointerInputAbstraction.isTouchMode(); ++ + +> DELETE 8 @ 8 : 9 + +> CHANGE 2 : 3 @ 2 : 3 ~ Util.func_181617_a((FutureTask) this.scheduledTasks.remove(0), logger); -> CHANGE 7 : 18 @ 7 : 8 +> DELETE 3 @ 3 : 4 + +> DELETE 1 @ 1 : 2 + +> CHANGE 1 : 15 @ 1 : 2 ~ if (this.timer.elapsedTicks > 1) { -~ long watchdog = System.currentTimeMillis(); +~ long watchdog = EagRuntime.steadyTimeMillis(); ~ for (int j = 0; j < this.timer.elapsedTicks; ++j) { ~ this.runTick(); -~ long millis = System.currentTimeMillis(); +~ if (j < this.timer.elapsedTicks - 1) { +~ PointerInputAbstraction.runGameLoop(); +~ } +~ long millis = EagRuntime.steadyTimeMillis(); ~ if (millis - watchdog > 50l) { ~ watchdog = millis; -~ EagUtils.sleep(0l); +~ EagRuntime.immediateContinue(); ~ } ~ } ~ } else if (this.timer.elapsedTicks == 1) { -> DELETE 10 @ 10 : 18 +> DELETE 3 @ 3 : 4 -> CHANGE 1 : 4 @ 1 : 5 +> DELETE 2 @ 2 : 3 + +> DELETE 1 @ 1 : 11 + +> CHANGE 1 : 12 @ 1 : 7 ~ if (!Display.contextLost()) { -~ this.mcProfiler.startSection("EaglercraftGPU_optimize"); ~ EaglercraftGPU.optimize(); - -> CHANGE 1 : 11 @ 1 : 2 - ~ _wglBindFramebuffer(0x8D40, null); ~ GlStateManager.viewport(0, 0, this.displayWidth, this.displayHeight); ~ GlStateManager.clearColor(0.0f, 0.0f, 0.0f, 1.0f); ~ GlStateManager.pushMatrix(); ~ GlStateManager.clear(16640); -~ this.mcProfiler.startSection("display"); ~ GlStateManager.enableTexture2D(); ~ if (this.thePlayer != null && this.thePlayer.isEntityInsideOpaqueBlock()) { ~ this.gameSettings.thirdPersonView = 0; ~ } -> CHANGE 1 : 6 @ 1 : 5 +> CHANGE 1 : 3 @ 1 : 5 -~ this.mcProfiler.endSection(); ~ if (!this.skipRenderWorld) { -~ this.mcProfiler.endStartSection("gameRenderer"); ~ this.entityRenderer.func_181560_a(this.timer.renderPartialTicks, i); -~ this.mcProfiler.endSection(); -> CHANGE 2 : 18 @ 2 : 7 +> CHANGE 2 : 5 @ 2 : 7 -~ this.mcProfiler.endSection(); -~ if (this.gameSettings.showDebugInfo && this.gameSettings.showDebugProfilerChart -~ && !this.gameSettings.hideGUI) { -~ if (!this.mcProfiler.profilingEnabled) { -~ this.mcProfiler.clearProfiling(); -~ } -~ -~ this.mcProfiler.profilingEnabled = true; -~ this.displayDebugInfo(i1); -~ } else { -~ this.mcProfiler.profilingEnabled = false; -~ this.prevFrameTime = System.nanoTime(); -~ } -~ ~ this.guiAchievement.updateAchievementWindow(); +~ this.touchOverlayRenderer.render(displayWidth, displayHeight, scaledResolution); ~ GlStateManager.popMatrix(); -> DELETE 2 @ 2 : 11 +> DELETE 2 @ 2 : 12 -> DELETE 2 @ 2 : 10 +> DELETE 1 @ 1 : 9 > INSERT 1 : 2 @ 1 @@ -526,93 +609,160 @@ > DELETE 3 @ 3 : 7 -> INSERT 8 : 9 @ 8 +> DELETE 3 @ 3 : 4 -+ Mouse.tickCursorShape(); +> DELETE 1 @ 1 : 2 -> INSERT 5 : 10 @ 5 +> CHANGE 2 : 3 @ 2 : 3 -+ if (Display.isVSyncSupported()) { -+ Display.setVSync(this.gameSettings.enableVsync); -+ } else { -+ this.gameSettings.enableVsync = false; -+ } +~ Mouse.tickCursorShape(); -> DELETE 34 @ 34 : 52 +> CHANGE 3 : 8 @ 3 : 4 -> CHANGE 39 : 40 @ 39 : 40 +~ if (Display.isVSyncSupported()) { +~ Display.setVSync(this.gameSettings.enableVsync); +~ } else { +~ this.gameSettings.enableVsync = false; +~ } -~ EaglercraftGPU.glLineWidth(1.0F); +> DELETE 1 @ 1 : 2 -> CHANGE 9 : 10 @ 9 : 10 +> CHANGE 4 : 7 @ 4 : 5 -~ (double) ((float) j - (float) short1 * 0.6F - 16.0F), 0.0D).color(0, 0, 0, 100).endVertex(); +~ float dpiFetch = -1.0f; +~ if (!this.fullscreen +~ && (Display.wasResized() || (dpiFetch = Math.max(Display.getDPI(), 1.0f)) != this.displayDPI)) { -> CHANGE 1 : 2 @ 1 : 2 +> INSERT 2 : 3 @ 2 -~ .color(0, 0, 0, 100).endVertex(); ++ float f = this.displayDPI; -> CHANGE 1 : 2 @ 1 : 2 +> CHANGE 2 : 4 @ 2 : 3 -~ .color(0, 0, 0, 100).endVertex(); +~ this.displayDPI = dpiFetch == -1.0f ? Math.max(Display.getDPI(), 1.0f) : dpiFetch; +~ if (this.displayWidth != i || this.displayHeight != j || this.displayDPI != f) { -> CHANGE 1 : 2 @ 1 : 2 +> DELETE 22 @ 22 : 180 -~ (double) ((float) j - (float) short1 * 0.6F - 16.0F), 0.0D).color(0, 0, 0, 100).endVertex(); +> CHANGE 5 : 7 @ 5 : 6 -> DELETE 110 @ 110 : 114 +~ boolean touch = PointerInputAbstraction.isTouchMode(); +~ if (touch || Display.isActive()) { -> CHANGE 108 : 109 @ 108 : 148 +> CHANGE 2 : 5 @ 2 : 3 + +~ if (!touch && mouseGrabSupported) { +~ this.mouseHelper.grabMouseCursor(); +~ } + +> CHANGE 10 : 13 @ 10 : 11 + +~ if (!PointerInputAbstraction.isTouchMode() && mouseGrabSupported) { +~ this.mouseHelper.ungrabMouseCursor(); +~ } + +> DELETE 6 @ 6 : 10 + +> CHANGE 55 : 56 @ 55 : 56 + +~ public void rightClickMouse() { + +> CHANGE 52 : 53 @ 52 : 92 ~ Display.toggleFullscreen(); > INSERT 5 : 6 @ 5 -+ ScaledResolution scaledresolution = new ScaledResolution(this); ++ this.scaledResolution = new ScaledResolution(this); + +> CHANGE 1 : 2 @ 1 : 3 + +~ this.currentScreen.onResize(this, scaledResolution.getScaledWidth(), scaledResolution.getScaledHeight()); + +> DELETE 3 @ 3 : 5 + +> CHANGE 1 : 3 @ 1 : 5 + +~ if (voiceOverlay != null) { +~ voiceOverlay.setResolution(scaledResolution.getScaledWidth(), scaledResolution.getScaledHeight()); + +> INSERT 1 : 5 @ 1 + ++ if (notifRenderer != null) { ++ notifRenderer.setResolution(this, scaledResolution.getScaledWidth(), scaledResolution.getScaledHeight(), ++ scaledResolution.getScaleFactor()); ++ } + +> INSERT 1 : 4 @ 1 + ++ EagRuntime.getConfiguration().getHooks().callScreenChangedHook( ++ currentScreen != null ? currentScreen.getClass().getName() : null, scaledResolution.getScaledWidth(), ++ scaledResolution.getScaledHeight(), displayWidth, displayHeight, scaledResolution.getScaleFactor()); + +> CHANGE 11 : 50 @ 11 : 12 + +~ RateLimitTracker.tick(); +~ +~ boolean isHostingLAN = LANServerController.isHostingLAN(); +~ this.isGamePaused = !isHostingLAN && this.isSingleplayer() && this.theWorld != null && this.thePlayer != null +~ && this.currentScreen != null && this.currentScreen.doesGuiPauseGame(); +~ +~ if (isLANOpen && !isHostingLAN) { +~ ingameGUI.getChatGUI().printChatMessage(new ChatComponentTranslation("lanServer.relayDisconnected")); +~ } +~ isLANOpen = isHostingLAN; +~ +~ if (wasPaused != isGamePaused) { +~ SingleplayerServerController.setPaused(this.isGamePaused); +~ wasPaused = isGamePaused; +~ } +~ +~ PlatformWebRTC.runScheduledTasks(); +~ WebViewOverlayController.runTick(); +~ SingleplayerServerController.runTick(); +~ RelayUpdateChecker.runTick(); +~ +~ UpdateResultObj update = UpdateService.getUpdateResult(); +~ if (update != null) { +~ if (update.isSuccess()) { +~ UpdateDataObj updateSuccess = update.getSuccess(); +~ if (EagRuntime.getConfiguration().isAllowBootMenu()) { +~ if (currentScreen == null || (!(currentScreen instanceof GuiUpdateDownloadSuccess) +~ && !(currentScreen instanceof GuiUpdateInstallOptions))) { +~ displayGuiScreen(new GuiUpdateDownloadSuccess(currentScreen, updateSuccess)); +~ } +~ } else { +~ UpdateService.quine(updateSuccess.clientSignature, updateSuccess.clientBundle); +~ } +~ } else { +~ displayGuiScreen( +~ new GuiScreenGenericErrorMessage("updateFailed.title", update.getFailure(), currentScreen)); +~ } +~ } +~ + +> CHANGE 4 : 6 @ 4 : 5 + +~ VoiceClientController.tickVoiceClient(this); +~ > DELETE 1 @ 1 : 2 -> DELETE 4 @ 4 : 6 +> CHANGE 4 : 8 @ 4 : 5 -> CHANGE 1 : 2 @ 1 : 7 +~ if (this.thePlayer != null && this.thePlayer.sendQueue != null) { +~ this.thePlayer.sendQueue.getEaglerMessageController().flush(); +~ } +~ -~ this.voiceOverlay.setResolution(scaledresolution.getScaledWidth(), scaledresolution.getScaledHeight()); - -> INSERT 11 : 30 @ 11 - -+ RateLimitTracker.tick(); -+ -+ boolean isHostingLAN = LANServerController.isHostingLAN(); -+ this.isGamePaused = !isHostingLAN && this.isSingleplayer() && this.theWorld != null && this.thePlayer != null -+ && this.currentScreen != null && this.currentScreen.doesGuiPauseGame(); -+ -+ if (isLANOpen && !isHostingLAN) { -+ ingameGUI.getChatGUI().printChatMessage(new ChatComponentTranslation("lanServer.relayDisconnected")); -+ } -+ isLANOpen = isHostingLAN; -+ -+ if (wasPaused != isGamePaused) { -+ SingleplayerServerController.setPaused(this.isGamePaused); -+ wasPaused = isGamePaused; -+ } -+ -+ SingleplayerServerController.runTick(); -+ RelayUpdateChecker.runTick(); -+ - -> INSERT 5 : 8 @ 5 - -+ this.mcProfiler.endStartSection("eaglerVoice"); -+ VoiceClientController.tickVoiceClient(this); -+ - -> INSERT 10 : 11 @ 10 +> INSERT 2 : 4 @ 2 + GlStateManager.viewport(0, 0, displayWidth, displayHeight); // to be safe ++ GlStateManager.enableAlpha(); > INSERT 8 : 14 @ 8 -+ if (this.currentScreen == null && this.dontPauseTimer <= 0) { ++ if (this.currentScreen == null && this.dontPauseTimer <= 0 && !PointerInputAbstraction.isTouchMode()) { + if (!Mouse.isMouseGrabbed()) { + this.setIngameNotInFocus(); + this.displayInGameMenu(); @@ -627,7 +777,17 @@ + --this.dontPauseTimer; + } -> CHANGE 10 : 11 @ 10 : 11 +> INSERT 2 : 9 @ 2 + ++ String pastedStr; ++ while ((pastedStr = Touch.getPastedString()) != null) { ++ if (this.currentScreen != null) { ++ this.currentScreen.fireInputEvent(EnumInputEvent.CLIPBOARD_PASTE, pastedStr); ++ } ++ } ++ + +> CHANGE 8 : 9 @ 8 : 9 ~ return Minecraft.this.currentScreen.getClass().getName(); @@ -635,18 +795,114 @@ ~ return Minecraft.this.currentScreen.getClass().getName(); -> CHANGE 25 : 28 @ 25 : 26 +> DELETE 8 @ 8 : 9 -~ if (this.isZoomKey) { -~ this.adjustedZoomValue = MathHelper.clamp_float(adjustedZoomValue - j * 4.0f, 5.0f, 32.0f); -~ } else if (this.thePlayer.isSpectator()) { +> CHANGE 1 : 39 @ 1 : 7 -> CHANGE 14 : 16 @ 14 : 15 +~ boolean touched; +~ boolean moused = false; +~ +~ while ((touched = Touch.next()) || (moused = Mouse.next())) { +~ boolean touch = false; +~ if (touched) { +~ PointerInputAbstraction.enterTouchModeHook(); +~ boolean mouse = moused; +~ moused = false; +~ int tc = Touch.getEventTouchPointCount(); +~ if (tc > 0) { +~ for (int i = 0; i < tc; ++i) { +~ final int uid = Touch.getEventTouchPointUID(i); +~ int x = Touch.getEventTouchX(i); +~ int y = Touch.getEventTouchY(i); +~ switch (Touch.getEventType()) { +~ case TOUCHSTART: +~ if (TouchControls.handleTouchBegin(uid, x, y)) { +~ break; +~ } +~ touch = true; +~ handlePlaceTouchStart(); +~ break; +~ case TOUCHEND: +~ if (TouchControls.handleTouchEnd(uid, x, y)) { +~ touch = true; +~ break; +~ } +~ handlePlaceTouchEnd(); +~ break; +~ default: +~ break; +~ } +~ } +~ TouchControls.handleInput(); +~ if (!touch) { +~ continue; +~ } -~ if ((!this.inGameHasFocus || !Mouse.isActuallyGrabbed()) && Mouse.getEventButtonState()) { +> CHANGE 1 : 4 @ 1 : 2 + +~ if (!mouse) { +~ continue; +~ } + +> INSERT 3 : 16 @ 3 + ++ if (!touch) { ++ int i = Mouse.getEventButton(); ++ KeyBinding.setKeyBindState(i - 100, Mouse.getEventButtonState()); ++ if (Mouse.getEventButtonState()) { ++ PointerInputAbstraction.enterMouseModeHook(); ++ if (this.thePlayer.isSpectator() && i == 2) { ++ this.ingameGUI.getSpectatorGui().func_175261_b(); ++ } else { ++ KeyBinding.onTick(i - 100); ++ } ++ } ++ } ++ + +> CHANGE 2 : 17 @ 2 : 8 + +~ if (!touch) { +~ int j = Mouse.getEventDWheel(); +~ if (j != 0) { +~ if (this.isZoomKey) { +~ this.adjustedZoomValue = MathHelper.clamp_float(adjustedZoomValue - j * 4.0f, 5.0f, +~ 32.0f); +~ } else if (this.thePlayer.isSpectator()) { +~ j = j < 0 ? -1 : 1; +~ if (this.ingameGUI.getSpectatorGui().func_175262_a()) { +~ this.ingameGUI.getSpectatorGui().func_175259_b(-j); +~ } else { +~ float f = MathHelper.clamp_float( +~ this.thePlayer.capabilities.getFlySpeed() + (float) j * 0.005F, 0.0F, 0.2F); +~ this.thePlayer.capabilities.setFlySpeed(f); +~ } + +> CHANGE 1 : 2 @ 1 : 4 + +~ this.thePlayer.inventory.changeCurrentItem(j); + +> DELETE 1 @ 1 : 3 + +> CHANGE 4 : 7 @ 4 : 5 + +~ if ((!this.inGameHasFocus || !(touch || Mouse.isActuallyGrabbed())) +~ && (touch || Mouse.getEventButtonState())) { ~ this.inGameHasFocus = false; -> INSERT 16 : 19 @ 16 +> CHANGE 3 : 8 @ 3 : 4 + +~ if (touch) { +~ this.currentScreen.handleTouchInput(); +~ } else { +~ this.currentScreen.handleMouseInput(); +~ } + +> CHANGE 8 : 9 @ 8 : 9 + +~ processTouchMine(); + +> INSERT 3 : 6 @ 3 + if (k == 0x1D && (areKeysLocked() || isFullScreen())) { + KeyBinding.setKeyBindState(gameSettings.keyBindSprint.getKeyCode(), Keyboard.getEventKeyState()); @@ -681,7 +937,13 @@ + GlStateManager.recompileShaders(); -> INSERT 71 : 77 @ 71 +> CHANGE 28 : 29 @ 28 : 40 + +~ togglePerspective(); + +> DELETE 6 @ 6 : 18 + +> INSERT 13 : 19 @ 13 + boolean zoomKey = this.gameSettings.keyBindZoomCamera.isKeyDown(); + if (zoomKey != isZoomKey) { @@ -690,75 +952,251 @@ + } + -> INSERT 92 : 93 @ 92 +> INSERT 26 : 28 @ 26 + ++ boolean miningTouch = isMiningTouch(); ++ boolean useTouch = thePlayer.getItemShouldUseOnTouchEagler(); + +> CHANGE 1 : 2 @ 1 : 2 + +~ if (!this.gameSettings.keyBindUseItem.isKeyDown() && !miningTouch) { + +> INSERT 19 : 28 @ 19 + ++ if (miningTouch && !wasMiningTouch) { ++ if ((objectMouseOver != null && objectMouseOver.typeOfHit == MovingObjectType.ENTITY) || useTouch) { ++ this.rightClickMouse(); ++ } else { ++ this.clickMouse(); ++ } ++ wasMiningTouch = true; ++ } ++ + +> INSERT 8 : 9 @ 8 + ++ wasMiningTouch = miningTouch; + +> INSERT 6 : 10 @ 6 + ++ if (miningTouch && useTouch && this.rightClickDelayTimer == 0 && !this.thePlayer.isUsingItem()) { ++ this.rightClickMouse(); ++ } ++ + +> CHANGE 1 : 3 @ 1 : 2 + +~ this.currentScreen == null && (this.gameSettings.keyBindAttack.isKeyDown() || miningTouch) +~ && this.inGameHasFocus && !useTouch); + +> DELETE 11 @ 11 : 12 + +> DELETE 4 @ 4 : 5 + +> DELETE 4 @ 4 : 5 + +> INSERT 7 : 8 @ 7 + this.eagskullCommand.tick(); -> INSERT 43 : 87 @ 43 +> DELETE 28 @ 28 : 29 + +> DELETE 5 @ 5 : 6 + +> DELETE 4 @ 4 : 5 + +> CHANGE 3 : 46 @ 3 : 15 + +~ if (this.theWorld != null) { +~ ++joinWorldTickCounter; +~ if (bungeeOutdatedMsgTimer > 0) { +~ if (--bungeeOutdatedMsgTimer == 0 && this.thePlayer.sendQueue != null) { +~ String pluginBrand = this.thePlayer.sendQueue.getNetworkManager().getPluginBrand(); +~ String pluginVersion = this.thePlayer.sendQueue.getNetworkManager().getPluginVersion(); +~ if (pluginBrand != null && pluginVersion != null +~ && EaglerXBungeeVersion.isUpdateToPluginAvailable(pluginBrand, pluginVersion)) { +~ String pfx = EnumChatFormatting.GOLD + "[EagX]" + EnumChatFormatting.AQUA; +~ ingameGUI.getChatGUI().printChatMessage( +~ new ChatComponentText(pfx + " ---------------------------------------")); +~ ingameGUI.getChatGUI().printChatMessage( +~ new ChatComponentText(pfx + " This server appears to be using version " +~ + EnumChatFormatting.YELLOW + pluginVersion)); +~ ingameGUI.getChatGUI().printChatMessage( +~ new ChatComponentText(pfx + " of the EaglerXBungee plugin which is outdated")); +~ ingameGUI.getChatGUI().printChatMessage(new ChatComponentText(pfx)); +~ ingameGUI.getChatGUI() +~ .printChatMessage(new ChatComponentText(pfx + " If you are the admin update to " +~ + EnumChatFormatting.YELLOW + EaglerXBungeeVersion.getPluginVersion() +~ + EnumChatFormatting.AQUA + " or newer")); +~ ingameGUI.getChatGUI().printChatMessage(new ChatComponentText(pfx)); +~ ingameGUI.getChatGUI().printChatMessage((new ChatComponentText(pfx + " Click: ")) +~ .appendSibling((new ChatComponentText("" + EnumChatFormatting.GREEN +~ + EnumChatFormatting.UNDERLINE + EaglerXBungeeVersion.getPluginButton())) +~ .setChatStyle((new ChatStyle()).setChatClickEvent( +~ new ClickEvent(ClickEvent.Action.EAGLER_PLUGIN_DOWNLOAD, +~ "plugin_download.zip"))))); +~ ingameGUI.getChatGUI().printChatMessage( +~ new ChatComponentText(pfx + " ---------------------------------------")); +~ } +~ } +~ } +~ } else { +~ joinWorldTickCounter = 0; +~ if (currentScreen != null && currentScreen.shouldHangupIntegratedServer()) { +~ if (SingleplayerServerController.hangupEaglercraftServer()) { +~ this.displayGuiScreen(new GuiScreenIntegratedServerBusy(currentScreen, +~ "singleplayer.busy.stoppingIntegratedServer", +~ "singleplayer.failed.stoppingIntegratedServer", SingleplayerServerController::isReady)); +~ } +~ } +~ TouchControls.resetSneak(); + +> CHANGE 2 : 30 @ 2 : 4 + +~ if (reconnectURI != null) { +~ String reconURI = reconnectURI; +~ reconnectURI = null; +~ if (EagRuntime.getConfiguration().isAllowServerRedirects()) { +~ boolean enableCookies; +~ boolean msg; +~ if (this.currentServerData != null) { +~ enableCookies = this.currentServerData.enableCookies; +~ msg = false; +~ } else { +~ enableCookies = EagRuntime.getConfiguration().isEnableServerCookies(); +~ msg = true; +~ } +~ if (theWorld != null) { +~ theWorld.sendQuittingDisconnectingPacket(); +~ loadWorld(null); +~ } +~ logger.info("Recieved SPacketRedirectClientV4EAG, reconnecting to: {}", reconURI); +~ if (msg) { +~ logger.warn("No existing server connection, cookies will default to {}!", +~ enableCookies ? "enabled" : "disabled"); +~ } +~ ServerAddress addr = AddressResolver.resolveAddressFromURI(reconURI); +~ this.displayGuiScreen( +~ new GuiConnecting(new GuiMainMenu(), this, addr.getIP(), addr.getPort(), enableCookies)); +~ } else { +~ logger.warn("Server redirect blocked: {}", reconURI); +~ } + +> CHANGE 2 : 4 @ 2 : 13 + +~ this.systemTime = getSystemTime(); +~ } + +> CHANGE 1 : 4 @ 1 : 2 + +~ private long placeTouchStartTime = -1l; +~ private long mineTouchStartTime = -1l; +~ private boolean wasMiningTouch = false; + +> CHANGE 1 : 12 @ 1 : 5 + +~ private void processTouchMine() { +~ if ((currentScreen == null || currentScreen.allowUserInput) +~ && PointerInputAbstraction.isTouchingScreenNotButton()) { +~ if (PointerInputAbstraction.isDraggingNotTouching()) { +~ if (mineTouchStartTime != -1l) { +~ long l = EagRuntime.steadyTimeMillis(); +~ if ((placeTouchStartTime == -1l || (l - placeTouchStartTime) < 350l) +~ || (l - mineTouchStartTime) < 350l) { +~ mineTouchStartTime = -1l; +~ } +~ } + +> CHANGE 1 : 4 @ 1 : 2 + +~ if (mineTouchStartTime == -1l) { +~ mineTouchStartTime = EagRuntime.steadyTimeMillis(); +~ } + +> INSERT 1 : 5 @ 1 -+ if (this.theWorld != null) { -+ ++joinWorldTickCounter; -+ if (bungeeOutdatedMsgTimer > 0) { -+ if (--bungeeOutdatedMsgTimer == 0 && this.thePlayer.sendQueue != null) { -+ String pluginBrand = this.thePlayer.sendQueue.getNetworkManager().getPluginBrand(); -+ String pluginVersion = this.thePlayer.sendQueue.getNetworkManager().getPluginVersion(); -+ if (pluginBrand != null && pluginVersion != null -+ && EaglerXBungeeVersion.isUpdateToPluginAvailable(pluginBrand, pluginVersion)) { -+ String pfx = EnumChatFormatting.GOLD + "[EagX]" + EnumChatFormatting.AQUA; -+ ingameGUI.getChatGUI().printChatMessage( -+ new ChatComponentText(pfx + " ---------------------------------------")); -+ ingameGUI.getChatGUI().printChatMessage( -+ new ChatComponentText(pfx + " This server appears to be using version " -+ + EnumChatFormatting.YELLOW + pluginVersion)); -+ ingameGUI.getChatGUI().printChatMessage( -+ new ChatComponentText(pfx + " of the EaglerXBungee plugin which is outdated")); -+ ingameGUI.getChatGUI().printChatMessage(new ChatComponentText(pfx)); -+ ingameGUI.getChatGUI() -+ .printChatMessage(new ChatComponentText(pfx + " If you are the admin update to " -+ + EnumChatFormatting.YELLOW + EaglerXBungeeVersion.getPluginVersion() -+ + EnumChatFormatting.AQUA + " or newer")); -+ ingameGUI.getChatGUI().printChatMessage(new ChatComponentText(pfx)); -+ ingameGUI.getChatGUI().printChatMessage((new ChatComponentText(pfx + " Click: ")) -+ .appendSibling((new ChatComponentText("" + EnumChatFormatting.GREEN -+ + EnumChatFormatting.UNDERLINE + EaglerXBungeeVersion.getPluginButton())) -+ .setChatStyle((new ChatStyle()).setChatClickEvent( -+ new ClickEvent(ClickEvent.Action.EAGLER_PLUGIN_DOWNLOAD, -+ "plugin_download.zip"))))); -+ ingameGUI.getChatGUI().printChatMessage( -+ new ChatComponentText(pfx + " ---------------------------------------")); -+ } -+ } -+ } + } else { -+ joinWorldTickCounter = 0; -+ if (currentScreen != null && currentScreen.shouldHangupIntegratedServer()) { -+ if (SingleplayerServerController.hangupEaglercraftServer()) { -+ this.displayGuiScreen(new GuiScreenIntegratedServerBusy(currentScreen, -+ "singleplayer.busy.stoppingIntegratedServer", -+ "singleplayer.failed.stoppingIntegratedServer", SingleplayerServerController::isReady)); -+ } -+ } ++ mineTouchStartTime = -1l; + } ++ } + +> CHANGE 1 : 23 @ 1 : 5 + +~ private boolean isMiningTouch() { +~ if (mineTouchStartTime == -1l) +~ return false; +~ long l = EagRuntime.steadyTimeMillis(); +~ return (placeTouchStartTime == -1l || (l - placeTouchStartTime) >= 350l) && (l - mineTouchStartTime) >= 350l; +~ } +~ +~ private void handlePlaceTouchStart() { +~ if (placeTouchStartTime == -1l) { +~ placeTouchStartTime = EagRuntime.steadyTimeMillis(); +~ } +~ } +~ +~ private void handlePlaceTouchEnd() { +~ if (placeTouchStartTime != -1l) { +~ int len = (int) (EagRuntime.steadyTimeMillis() - placeTouchStartTime); +~ if (len < 350l && !PointerInputAbstraction.isDraggingNotTouching()) { +~ if (objectMouseOver != null && objectMouseOver.typeOfHit == MovingObjectType.ENTITY) { +~ clickMouse(); +~ } else { +~ rightClickMouse(); +~ } + +> INSERT 1 : 2 @ 1 + ++ placeTouchStartTime = -1l; + +> INSERT 1 : 2 @ 1 + ++ } + +> CHANGE 1 : 14 @ 1 : 8 + +~ public void togglePerspective() { +~ ++this.gameSettings.thirdPersonView; +~ if (this.gameSettings.thirdPersonView > 2) { +~ this.gameSettings.thirdPersonView = 0; +~ } +~ +~ if (this.gameSettings.thirdPersonView == 0) { +~ this.entityRenderer.loadEntityShader(this.getRenderViewEntity()); +~ } else if (this.gameSettings.thirdPersonView == 1) { +~ this.entityRenderer.loadEntityShader((Entity) null); +~ } +~ +~ this.renderGlobal.setDisplayListEntitiesDirty(); + +> INSERT 2 : 20 @ 2 + ++ public void launchIntegratedServer(String folderName, String worldName, WorldSettings worldSettingsIn) { ++ this.loadWorld((WorldClient) null); ++ renderManager.setEnableFNAWSkins(this.gameSettings.enableFNAWSkins); ++ session.reset(); ++ EaglerProfile.clearServerSkinOverride(); ++ PauseMenuCustomizeState.reset(); ++ SingleplayerServerController.launchEaglercraftServer(folderName, gameSettings.difficulty.getDifficultyId(), ++ Math.max(gameSettings.renderDistanceChunks, 2), worldSettingsIn); ++ EagRuntime.setMCServerWindowGlobal("singleplayer"); ++ this.displayGuiScreen(new GuiScreenIntegratedServerBusy( ++ new GuiScreenSingleplayerConnecting(new GuiMainMenu(), "Connecting to " + folderName), ++ "singleplayer.busy.startingIntegratedServer", "singleplayer.failed.startingIntegratedServer", ++ () -> SingleplayerServerController.isWorldReady(), (t, u) -> { ++ Minecraft.this.displayGuiScreen(GuiScreenIntegratedServerBusy.createException(new GuiMainMenu(), ++ ((GuiScreenIntegratedServerBusy) t).failMessage, u)); ++ })); ++ } + -> CHANGE 6 : 18 @ 6 : 54 - -~ Minecraft.getMinecraft().getRenderManager().setEnableFNAWSkins(this.gameSettings.enableFNAWSkins); -~ session.reset(); -~ SingleplayerServerController.launchEaglercraftServer(folderName, gameSettings.difficulty.getDifficultyId(), -~ Math.max(gameSettings.renderDistanceChunks, 2), worldSettingsIn); -~ EagRuntime.setMCServerWindowGlobal("singleplayer"); -~ this.displayGuiScreen(new GuiScreenIntegratedServerBusy( -~ new GuiScreenSingleplayerConnecting(new GuiMainMenu(), "Connecting to " + folderName), -~ "singleplayer.busy.startingIntegratedServer", "singleplayer.failed.startingIntegratedServer", -~ () -> SingleplayerServerController.isWorldReady(), (t, u) -> { -~ Minecraft.this.displayGuiScreen(GuiScreenIntegratedServerBusy.createException(new GuiMainMenu(), -~ ((GuiScreenIntegratedServerBusy) t).failMessage, u)); -~ })); - -> INSERT 12 : 13 @ 12 +> INSERT 10 : 15 @ 10 + session.reset(); ++ EaglerProfile.clearServerSkinOverride(); ++ PauseMenuCustomizeState.reset(); ++ ClientUUIDLoadingCache.flushRequestCache(); ++ WebViewOverlayController.setPacketSendCallback(null); > DELETE 1 @ 1 : 7 @@ -779,7 +1217,11 @@ ~ GameSettings g = theMinecraft.gameSettings; ~ return g.ambientOcclusion != 0 && !g.shadersAODisable; -> CHANGE 130 : 131 @ 130 : 131 +> CHANGE 2 : 3 @ 2 : 3 + +~ public void middleClickMouse() { + +> CHANGE 127 : 128 @ 127 : 128 ~ return EagRuntime.getVersion(); @@ -801,19 +1243,63 @@ ~ return !Minecraft.this.gameSettings.shaders && Minecraft.this.gameSettings.enableDynamicLights ? "Yes" ~ : "No"; -> INSERT 2 : 7 @ 2 +> INSERT 2 : 47 @ 2 + theCrash.getCategory().addCrashSectionCallable("In Ext. Pipeline", new Callable() { + public String call() throws Exception { + return GlStateManager.isExtensionPipeline() ? "Yes" : "No"; + } + }); ++ theCrash.getCategory().addCrashSectionCallable("GPU Shader5 Capable", new Callable() { ++ public String call() throws Exception { ++ return EaglercraftGPU.checkShader5Capable() ? "Yes" : "No"; ++ } ++ }); ++ theCrash.getCategory().addCrashSectionCallable("GPU TexStorage Capable", new Callable() { ++ public String call() throws Exception { ++ return EaglercraftGPU.checkTexStorageCapable() ? "Yes" : "No"; ++ } ++ }); ++ theCrash.getCategory().addCrashSectionCallable("GPU TextureLOD Capable", new Callable() { ++ public String call() throws Exception { ++ return EaglercraftGPU.checkTextureLODCapable() ? "Yes" : "No"; ++ } ++ }); ++ theCrash.getCategory().addCrashSectionCallable("GPU Instancing Capable", new Callable() { ++ public String call() throws Exception { ++ return EaglercraftGPU.checkInstancingCapable() ? "Yes" : "No"; ++ } ++ }); ++ theCrash.getCategory().addCrashSectionCallable("GPU VAO Capable", new Callable() { ++ public String call() throws Exception { ++ return EaglercraftGPU.checkVAOCapable() ? "Yes" : "No"; ++ } ++ }); ++ theCrash.getCategory().addCrashSectionCallable("Is Software VAOs", new Callable() { ++ public String call() throws Exception { ++ return EaglercraftGPU.areVAOsEmulated() ? "Yes" : "No"; ++ } ++ }); ++ theCrash.getCategory().addCrashSectionCallable("GPU Render-to-MipMap", new Callable() { ++ public String call() throws Exception { ++ return EaglercraftGPU.checkFBORenderMipmapCapable() ? "Yes" : "No"; ++ } ++ }); ++ theCrash.getCategory().addCrashSectionCallable("Touch Mode", new Callable() { ++ public String call() throws Exception { ++ return PointerInputAbstraction.isTouchMode() ? "Yes" : "No"; ++ } ++ }); > CHANGE 2 : 3 @ 2 : 6 ~ return "Definitely Not; You're an eagler"; -> DELETE 36 @ 36 : 41 +> CHANGE 32 : 33 @ 32 : 34 + +~ return "N/A (disabled)"; + +> DELETE 2 @ 2 : 7 > CHANGE 12 : 13 @ 12 : 13 @@ -854,7 +1340,7 @@ > CHANGE 1 : 2 @ 1 : 2 -~ return System.currentTimeMillis(); +~ return EagRuntime.steadyTimeMillis(); > CHANGE 3 : 4 @ 3 : 4 @@ -901,7 +1387,7 @@ > DELETE 26 @ 26 : 34 -> INSERT 7 : 35 @ 7 +> INSERT 7 : 48 @ 7 + + public static int getGLMaximumTextureSize() { @@ -927,9 +1413,22 @@ + public boolean getEnableFNAWSkins() { + boolean ret = this.gameSettings.enableFNAWSkins; + if (this.thePlayer != null) { -+ ret &= this.thePlayer.sendQueue.currentFNAWSkinAllowedState; ++ if (this.thePlayer.sendQueue.currentFNAWSkinForcedState) { ++ ret = true; ++ } else { ++ ret &= this.thePlayer.sendQueue.currentFNAWSkinAllowedState; ++ } + } + return ret; + } ++ ++ public void handleReconnectPacket(String redirectURI) { ++ this.reconnectURI = redirectURI; ++ } ++ ++ public boolean isEnableProfanityFilter() { ++ return EagRuntime.getConfiguration().isForceProfanityFilter() || gameSettings.enableProfanityFilter; ++ } ++ > EOF diff --git a/patches/minecraft/net/minecraft/client/audio/SoundCategory.edit.java b/patches/minecraft/net/minecraft/client/audio/SoundCategory.edit.java index 93eaafe8..241cae8b 100644 --- a/patches/minecraft/net/minecraft/client/audio/SoundCategory.edit.java +++ b/patches/minecraft/net/minecraft/client/audio/SoundCategory.edit.java @@ -12,11 +12,7 @@ + import com.google.common.collect.Maps; + -> CHANGE 2 : 3 @ 2 : 3 - -~ MOBS("hostile", 5), ANIMALS("neutral", 6), PLAYERS("player", 7), AMBIENT("ambient", 8), VOICE("voice", 9); - -> INSERT 1 : 3 @ 1 +> INSERT 4 : 6 @ 4 + public static final SoundCategory[] _VALUES = values(); + diff --git a/patches/minecraft/net/minecraft/client/audio/SoundHandler.edit.java b/patches/minecraft/net/minecraft/client/audio/SoundHandler.edit.java index eca9fb69..c09c014b 100644 --- a/patches/minecraft/net/minecraft/client/audio/SoundHandler.edit.java +++ b/patches/minecraft/net/minecraft/client/audio/SoundHandler.edit.java @@ -13,12 +13,10 @@ > DELETE 2 @ 2 : 3 -> CHANGE 1 : 13 @ 1 : 11 +> CHANGE 1 : 11 @ 1 : 11 ~ import java.util.Set; ~ -~ import net.lax1dude.eaglercraft.v1_8.internal.PlatformAudio; -~ ~ import com.google.common.collect.Lists; ~ ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftSoundManager; @@ -77,14 +75,7 @@ ~ } catch (IOException e) { ~ throw new RuntimeException("Exception caught reading JSON", e); -> INSERT 122 : 126 @ 122 - -+ if (category == SoundCategory.VOICE) { -+ PlatformAudio.setMicVol(volume); -+ } -+ - -> CHANGE 13 : 19 @ 13 : 15 +> CHANGE 135 : 141 @ 135 : 137 ~ SoundCategory cat = soundeventaccessorcomposite.getSoundCategory(); ~ for (int i = 0; i < categories.length; ++i) { diff --git a/patches/minecraft/net/minecraft/client/entity/AbstractClientPlayer.edit.java b/patches/minecraft/net/minecraft/client/entity/AbstractClientPlayer.edit.java index 7a5051d1..15fc7f55 100644 --- a/patches/minecraft/net/minecraft/client/entity/AbstractClientPlayer.edit.java +++ b/patches/minecraft/net/minecraft/client/entity/AbstractClientPlayer.edit.java @@ -5,24 +5,39 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 4 @ 2 : 4 +> CHANGE 2 : 7 @ 2 : 4 +~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; +~ import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; ~ import net.lax1dude.eaglercraft.v1_8.mojang.authlib.GameProfile; +~ import net.lax1dude.eaglercraft.v1_8.profanity_filter.ProfanityFilter; ~ import net.lax1dude.eaglercraft.v1_8.profile.SkinModel; > DELETE 2 @ 2 : 6 -> DELETE 6 @ 6 : 7 +> INSERT 4 : 5 @ 4 -> INSERT 6 : 14 @ 6 ++ import net.minecraft.event.ClickEvent; -+ public long eaglerHighPolyAnimationTick = System.currentTimeMillis(); +> INSERT 1 : 4 @ 1 + ++ import net.minecraft.scoreboard.ScorePlayerTeam; ++ import net.minecraft.util.ChatComponentText; ++ import net.minecraft.util.IChatComponent; + +> DELETE 1 @ 1 : 2 + +> INSERT 6 : 16 @ 6 + ++ public long eaglerHighPolyAnimationTick = EagRuntime.steadyTimeMillis(); + public float eaglerHighPolyAnimationFloat1 = 0.0f; + public float eaglerHighPolyAnimationFloat2 = 0.0f; + public float eaglerHighPolyAnimationFloat3 = 0.0f; + public float eaglerHighPolyAnimationFloat4 = 0.0f; + public float eaglerHighPolyAnimationFloat5 = 0.0f; + public float eaglerHighPolyAnimationFloat6 = 0.0f; ++ public EaglercraftUUID clientBrandUUIDCache = null; ++ private String nameProfanityFilter = null; + > DELETE 38 @ 38 : 56 @@ -35,4 +50,29 @@ + } + +> INSERT 27 : 49 @ 27 + ++ ++ public String getNameProfanityFilter() { ++ if (Minecraft.getMinecraft().isEnableProfanityFilter()) { ++ if (nameProfanityFilter == null) { ++ nameProfanityFilter = ProfanityFilter.getInstance() ++ .profanityFilterString(this.getGameProfile().getName()); ++ } ++ return nameProfanityFilter; ++ } else { ++ return this.getGameProfile().getName(); ++ } ++ } ++ ++ public IChatComponent getDisplayNameProfanityFilter() { ++ ChatComponentText chatcomponenttext = new ChatComponentText( ++ ScorePlayerTeam.formatPlayerName(this.getTeam(), this.getNameProfanityFilter())); ++ chatcomponenttext.getChatStyle() ++ .setChatClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/msg " + this.getName() + " ")); ++ chatcomponenttext.getChatStyle().setChatHoverEvent(this.getHoverEvent()); ++ chatcomponenttext.getChatStyle().setInsertion(this.getName()); ++ return chatcomponenttext; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/client/entity/EntityPlayerSP.edit.java b/patches/minecraft/net/minecraft/client/entity/EntityPlayerSP.edit.java index f853691b..b7cf5678 100644 --- a/patches/minecraft/net/minecraft/client/entity/EntityPlayerSP.edit.java +++ b/patches/minecraft/net/minecraft/client/entity/EntityPlayerSP.edit.java @@ -5,8 +5,9 @@ # Version: 1.0 # Author: lax1dude -> INSERT 2 : 4 @ 2 +> INSERT 2 : 5 @ 2 ++ import net.lax1dude.eaglercraft.v1_8.EaglercraftVersion; + import net.lax1dude.eaglercraft.v1_8.sp.lan.LANClientNetworkManager; + import net.lax1dude.eaglercraft.v1_8.sp.socket.ClientIntegratedServerNetworkManager; @@ -22,7 +23,11 @@ ~ public EntityPlayerSP(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandler, StatFileWriter statWriter) { -> DELETE 2 @ 2 : 3 +> INSERT 1 : 2 @ 1 + ++ this.clientBrandUUIDCache = EaglercraftVersion.clientBrandUUID; + +> DELETE 1 @ 1 : 2 > INSERT 2 : 3 @ 2 diff --git a/patches/minecraft/net/minecraft/client/gui/ChatLine.edit.java b/patches/minecraft/net/minecraft/client/gui/ChatLine.edit.java new file mode 100644 index 00000000..2a9236b0 --- /dev/null +++ b/patches/minecraft/net/minecraft/client/gui/ChatLine.edit.java @@ -0,0 +1,28 @@ + +# Eagler Context Redacted Diff +# Copyright (c) 2024 lax1dude. All rights reserved. + +# Version: 1.0 +# Author: lax1dude + +> INSERT 2 : 4 @ 2 + ++ import net.lax1dude.eaglercraft.v1_8.profanity_filter.ProfanityFilter; ++ import net.minecraft.client.Minecraft; + +> INSERT 5 : 6 @ 5 + ++ private IChatComponent lineStringProfanityFilter; + +> CHANGE 9 : 17 @ 9 : 10 + +~ if (Minecraft.getMinecraft().isEnableProfanityFilter()) { +~ if (lineStringProfanityFilter == null) { +~ lineStringProfanityFilter = ProfanityFilter.getInstance().profanityFilterChatComponent(lineString); +~ } +~ return lineStringProfanityFilter; +~ } else { +~ return lineString; +~ } + +> EOF diff --git a/patches/minecraft/net/minecraft/client/gui/FontRenderer.edit.java b/patches/minecraft/net/minecraft/client/gui/FontRenderer.edit.java index 512b09b0..5569b47f 100644 --- a/patches/minecraft/net/minecraft/client/gui/FontRenderer.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/FontRenderer.edit.java @@ -7,12 +7,13 @@ > DELETE 2 @ 2 : 6 -> CHANGE 4 : 11 @ 4 : 6 +> CHANGE 4 : 12 @ 4 : 6 ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; ~ ~ import net.lax1dude.eaglercraft.v1_8.HString; ~ import net.lax1dude.eaglercraft.v1_8.IOUtils; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.FontMappingHelper; ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; ~ import net.lax1dude.eaglercraft.v1_8.opengl.ImageData; ~ import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; @@ -50,7 +51,24 @@ ~ protected boolean underlineStyle; ~ protected boolean strikethroughStyle; -> CHANGE 43 : 44 @ 43 : 44 +> INSERT 1 : 15 @ 1 + ++ protected static char[] codepointLookup = new char[] { 192, 193, 194, 200, 202, 203, 205, 211, 212, 213, 218, 223, ++ 227, 245, 287, 304, 305, 338, 339, 350, 351, 372, 373, 382, 519, 0, 0, 0, 0, 0, 0, 0, 32, 33, 34, 35, 36, ++ 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, ++ 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, ++ 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, ++ 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 0, 199, 252, 233, 226, 228, 224, 229, 231, ++ 234, 235, 232, 239, 238, 236, 196, 197, 201, 230, 198, 244, 246, 242, 251, 249, 255, 214, 220, 248, 163, ++ 216, 215, 402, 225, 237, 243, 250, 241, 209, 170, 186, 191, 174, 172, 189, 188, 161, 171, 187, 9617, 9618, ++ 9619, 9474, 9508, 9569, 9570, 9558, 9557, 9571, 9553, 9559, 9565, 9564, 9563, 9488, 9492, 9524, 9516, 9500, ++ 9472, 9532, 9566, 9567, 9562, 9556, 9577, 9574, 9568, 9552, 9580, 9575, 9576, 9572, 9573, 9561, 9560, 9554, ++ 9555, 9579, 9578, 9496, 9484, 9608, 9604, 9612, 9616, 9600, 945, 946, 915, 960, 931, 963, 956, 964, 934, ++ 920, 937, 948, 8734, 8709, 8712, 8745, 8801, 177, 8805, 8804, 8992, 8993, 247, 8776, 176, 8729, 183, 8730, ++ 8319, 178, 9632, 0 }; ++ + +> CHANGE 42 : 43 @ 42 : 43 ~ ImageData bufferedimage; @@ -60,7 +78,11 @@ ~ int j = bufferedimage.height; ~ int[] aint = bufferedimage.pixels; -> CHANGE 68 : 87 @ 68 : 78 +> CHANGE 54 : 55 @ 54 : 56 + +~ int i = FontMappingHelper.lookupChar(parChar1, false); + +> CHANGE 12 : 31 @ 12 : 22 ~ Tessellator tessellator = Tessellator.getInstance(); ~ WorldRenderer worldrenderer = tessellator.getWorldRenderer(); @@ -130,7 +152,20 @@ ~ int i1 = "0123456789abcdefklmnor".indexOf(Character.toLowerCase(parString1.charAt(i + 1))); -> CHANGE 133 : 134 @ 133 : 134 +> CHANGE 39 : 40 @ 39 : 41 + +~ int j = FontMappingHelper.lookupChar(c0, false); + +> INSERT 2 : 3 @ 2 + ++ char[] chars = FontRenderer.codepointLookup; + +> CHANGE 3 : 5 @ 3 : 8 + +~ j = this.fontRandom.nextInt(chars.length); +~ c1 = chars[j]; + +> CHANGE 82 : 83 @ 82 : 83 ~ private int renderStringAligned(String text, int x, int y, int wrapWidth, int color, boolean parFlag) { @@ -153,7 +188,11 @@ + return (int) this.posX; -> INSERT 119 : 122 @ 119 +> CHANGE 42 : 43 @ 42 : 44 + +~ int i = FontMappingHelper.lookupChar(character, false); + +> INSERT 75 : 78 @ 75 + if ((textColor & -67108864) == 0) { + textColor |= -16777216; diff --git a/patches/minecraft/net/minecraft/client/gui/GuiButton.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiButton.edit.java index 5f7489b5..36d87be3 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiButton.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiButton.edit.java @@ -46,4 +46,12 @@ ~ GlStateManager.popMatrix(); ~ } +> INSERT 32 : 37 @ 32 + ++ ++ public boolean isSliderTouchEvents() { ++ return false; ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiChat.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiChat.edit.java index 6b77b96f..2338cc80 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiChat.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiChat.edit.java @@ -7,7 +7,7 @@ > DELETE 2 @ 2 : 3 -> CHANGE 2 : 13 @ 2 : 4 +> CHANGE 2 : 18 @ 2 : 4 ~ ~ import org.apache.commons.lang3.StringUtils; @@ -16,25 +16,50 @@ ~ ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; ~ import net.lax1dude.eaglercraft.v1_8.Mouse; +~ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; ~ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; ~ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.GuiScreenVisualViewport; +~ import net.lax1dude.eaglercraft.v1_8.notifications.GuiButtonNotifBell; +~ import net.lax1dude.eaglercraft.v1_8.notifications.GuiScreenNotifications; ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; ~ import net.minecraft.client.resources.I18n; > DELETE 6 @ 6 : 11 -> INSERT 12 : 14 @ 12 +> CHANGE 1 : 2 @ 1 : 2 + +~ public class GuiChat extends GuiScreenVisualViewport { + +> INSERT 10 : 13 @ 10 + private GuiButton exitButton; ++ private GuiButtonNotifBell notifBellButton; + -> INSERT 9 : 12 @ 9 +> INSERT 9 : 17 @ 9 + if (!(this instanceof GuiSleepMP)) { + this.buttonList.add(exitButton = new GuiButton(69, this.width - 100, 3, 97, 20, I18n.format("chat.exit"))); ++ if (!this.mc.isIntegratedServerRunning() && this.mc.thePlayer != null ++ && this.mc.thePlayer.sendQueue.getEaglerMessageProtocol().ver >= 4) { ++ this.buttonList.add(notifBellButton = new GuiButtonNotifBell(70, this.width - 122, 3)); ++ notifBellButton.setUnread(mc.thePlayer.sendQueue.getNotifManager().getUnread()); ++ } + } -> CHANGE 18 : 20 @ 18 : 27 +> CHANGE 14 : 15 @ 14 : 15 + +~ public void updateScreen0() { + +> INSERT 1 : 4 @ 1 + ++ if (notifBellButton != null && mc.thePlayer != null) { ++ notifBellButton.setUnread(mc.thePlayer.sendQueue.getNotifManager().getUnread()); ++ } + +> CHANGE 2 : 4 @ 2 : 11 ~ protected void keyTyped(char parChar1, int parInt1) { ~ if (parInt1 == 1 && (this.mc.gameSettings.keyBindClose.getKeyCode() == 0 || this.mc.areKeysLocked())) { @@ -77,13 +102,30 @@ > CHANGE 25 : 26 @ 25 : 26 -~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { +~ protected void mouseClicked0(int parInt1, int parInt2, int parInt3) { -> INSERT 11 : 17 @ 11 +> CHANGE 1 : 3 @ 1 : 2 + +~ IChatComponent ichatcomponent = this.mc.ingameGUI.getChatGUI() +~ .getChatComponent(PointerInputAbstraction.getVCursorX(), PointerInputAbstraction.getVCursorY()); + +> INSERT 3 : 6 @ 3 + ++ if (mc.notifRenderer.handleClicked(this, parInt1, parInt2)) { ++ return; ++ } + +> CHANGE 3 : 4 @ 3 : 4 + +~ super.mouseClicked0(parInt1, parInt2, parInt3); + +> INSERT 2 : 10 @ 2 + protected void actionPerformed(GuiButton par1GuiButton) { + if (par1GuiButton.id == 69) { + this.mc.displayGuiScreen(null); ++ } else if (par1GuiButton.id == 70) { ++ this.mc.displayGuiScreen(new GuiScreenNotifications(this)); + } + } + @@ -101,27 +143,43 @@ ~ stringbuilder.append(this.foundPlayerNames.get(i)); -> INSERT 44 : 45 @ 44 +> CHANGE 41 : 42 @ 41 : 42 -+ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); +~ public void drawScreen0(int i, int j, float f) { -> INSERT 5 : 9 @ 5 +> CHANGE 2 : 5 @ 2 : 3 -+ if (exitButton != null) { -+ exitButton.yPosition = 3 + mc.guiAchievement.getHeight(); -+ } -+ +~ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); +~ IChatComponent ichatcomponent = this.mc.ingameGUI.getChatGUI() +~ .getChatComponent(PointerInputAbstraction.getVCursorX(), PointerInputAbstraction.getVCursorY()); -> CHANGE 8 : 10 @ 8 : 9 +> CHANGE 4 : 9 @ 4 : 5 + +~ if (exitButton != null) { +~ exitButton.yPosition = 3 + mc.guiAchievement.getHeight(); +~ } +~ +~ super.drawScreen0(i, j, f); + +> CHANGE 7 : 9 @ 7 : 8 ~ for (int i = 0; i < parArrayOfString.length; ++i) { ~ String s = parArrayOfString[i]; -> INSERT 24 : 28 @ 24 +> INSERT 24 : 37 @ 24 + + public boolean blockPTTKey() { + return true; + } ++ ++ public boolean showCopyPasteButtons() { ++ return true; ++ } ++ ++ public void fireInputEvent(EnumInputEvent event, String str) { ++ inputField.fireInputEvent(event, str); ++ } ++ > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiCommandBlock.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiCommandBlock.edit.java index b90ff796..98576b03 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiCommandBlock.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiCommandBlock.edit.java @@ -5,12 +5,13 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 6 @ 2 : 7 +> CHANGE 2 : 7 @ 2 : 7 ~ import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; ~ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; ~ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; > DELETE 5 @ 5 : 8 @@ -26,11 +27,23 @@ ~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { -> INSERT 46 : 50 @ 46 +> INSERT 46 : 62 @ 46 + + public boolean blockPTTKey() { -+ return commandTextField.isFocused(); ++ return commandTextField.isFocused() || previousOutputTextField.isFocused(); + } ++ ++ @Override ++ public boolean showCopyPasteButtons() { ++ return commandTextField.isFocused() || previousOutputTextField.isFocused(); ++ } ++ ++ @Override ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ commandTextField.fireInputEvent(event, param); ++ previousOutputTextField.fireInputEvent(event, param); ++ } ++ > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiControls.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiControls.edit.java index 6bc61680..50dea76f 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiControls.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiControls.edit.java @@ -11,8 +11,18 @@ > DELETE 1 @ 1 : 6 -> CHANGE 49 : 50 @ 49 : 50 +> CHANGE 6 : 8 @ 6 : 7 +~ GameSettings.Options.INVERT_MOUSE, GameSettings.Options.SENSITIVITY, +~ GameSettings.Options.EAGLER_TOUCH_CONTROL_OPACITY }; + +> CHANGE 42 : 48 @ 42 : 43 + +~ public void handleTouchInput() throws IOException { +~ super.handleTouchInput(); +~ this.keyBindingList.handleTouchInput(); +~ } +~ ~ protected void actionPerformed(GuiButton parGuiButton) { > CHANGE 3 : 6 @ 3 : 5 diff --git a/patches/minecraft/net/minecraft/client/gui/GuiCreateFlatWorld.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiCreateFlatWorld.edit.java index 4d9df200..5cca10b2 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiCreateFlatWorld.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiCreateFlatWorld.edit.java @@ -15,8 +15,13 @@ > DELETE 2 @ 2 : 3 -> CHANGE 61 : 62 @ 61 : 62 +> CHANGE 61 : 67 @ 61 : 62 +~ public void handleTouchInput() throws IOException { +~ super.handleTouchInput(); +~ this.createFlatWorldListSlotGui.handleTouchInput(); +~ } +~ ~ protected void actionPerformed(GuiButton parGuiButton) { > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiCreateWorld.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiCreateWorld.edit.java index fb893414..a343ba99 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiCreateWorld.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiCreateWorld.edit.java @@ -7,10 +7,11 @@ > DELETE 2 @ 2 : 3 -> CHANGE 1 : 3 @ 1 : 6 +> CHANGE 1 : 4 @ 1 : 6 ~ ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; > DELETE 7 @ 7 : 8 @@ -42,4 +43,19 @@ ~ StringUtils.isNotEmpty(field_146335_h.text) ? "createWorld.seedNote" : "selectWorld.seedInfo", ~ new Object[0]), this.width / 2 - 100, 85, -6250336); +> INSERT 47 : 59 @ 47 + ++ ++ @Override ++ public boolean showCopyPasteButtons() { ++ return field_146333_g.isFocused() || field_146335_h.isFocused(); ++ } ++ ++ @Override ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ field_146333_g.fireInputEvent(event, param); ++ field_146335_h.fireInputEvent(event, param); ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiCustomizeWorldScreen.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiCustomizeWorldScreen.edit.java index d88b2ff5..f49b59c3 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiCustomizeWorldScreen.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiCustomizeWorldScreen.edit.java @@ -5,16 +5,25 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 6 : 10 @ 6 : 16 +> CHANGE 6 : 11 @ 6 : 16 ~ ~ import net.lax1dude.eaglercraft.v1_8.HString; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; ~ import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; > DELETE 1 @ 1 : 2 -> CHANGE 347 : 348 @ 347 : 348 +> INSERT 89 : 94 @ 89 + ++ public void handleTouchInput() throws IOException { ++ super.handleTouchInput(); ++ this.field_175349_r.handleTouchInput(); ++ } ++ + +> CHANGE 258 : 259 @ 258 : 259 ~ HString.format("%5.3f", new Object[] { Float.valueOf(this.field_175336_F.mainNoiseScaleX) }), @@ -106,4 +115,18 @@ ~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { +> INSERT 59 : 70 @ 59 + ++ ++ @Override ++ public boolean showCopyPasteButtons() { ++ return field_175349_r.isTextFieldFocused(); ++ } ++ ++ @Override ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ field_175349_r.fireInputEvent(event, param); ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiDownloadTerrain.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiDownloadTerrain.edit.java index 07925d58..8ae7b8d0 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiDownloadTerrain.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiDownloadTerrain.edit.java @@ -11,11 +11,16 @@ ~ protected void keyTyped(char parChar1, int parInt1) { -> INSERT 24 : 28 @ 24 +> INSERT 24 : 33 @ 24 + + public boolean shouldHangupIntegratedServer() { + return false; + } ++ ++ public boolean canCloseGui() { ++ return false; ++ } ++ > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiEnchantment.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiEnchantment.edit.java index b2d9feda..600bbc61 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiEnchantment.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiEnchantment.edit.java @@ -29,8 +29,9 @@ ~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { -> CHANGE 25 : 28 @ 25 : 28 +> CHANGE 24 : 28 @ 24 : 28 +~ ScaledResolution scaledresolution = mc.scaledResolution; ~ GlStateManager.viewport((scaledresolution.getScaledWidth() - 290 - 12) / 2 * scaledresolution.getScaleFactor(), ~ (scaledresolution.getScaledHeight() - 220 + 10) / 2 * scaledresolution.getScaleFactor(), ~ 290 * scaledresolution.getScaleFactor(), 220 * scaledresolution.getScaleFactor()); diff --git a/patches/minecraft/net/minecraft/client/gui/GuiFlatPresets.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiFlatPresets.edit.java index 5dc8fbd2..c0dfa359 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiFlatPresets.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiFlatPresets.edit.java @@ -5,10 +5,11 @@ # Version: 1.0 # Author: lax1dude -> INSERT 7 : 11 @ 7 +> INSERT 7 : 12 @ 7 + + import net.lax1dude.eaglercraft.v1_8.Keyboard; ++ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; + import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; @@ -18,7 +19,15 @@ > DELETE 9 @ 9 : 10 -> CHANGE 41 : 42 @ 41 : 42 +> INSERT 37 : 42 @ 37 + ++ public void handleTouchInput() throws IOException { ++ super.handleTouchInput(); ++ this.field_146435_s.handleTouchInput(); ++ } ++ + +> CHANGE 4 : 5 @ 4 : 5 ~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { @@ -35,4 +44,18 @@ ~ for (int i = 0, l = parList.size(); i < l; ++i) { ~ flatgeneratorinfo.getWorldFeatures().put(parList.get(i), Maps.newHashMap()); +> INSERT 132 : 143 @ 132 + ++ ++ @Override ++ public boolean showCopyPasteButtons() { ++ return field_146433_u.isFocused(); ++ } ++ ++ @Override ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ field_146433_u.fireInputEvent(event, param); ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiIngame.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiIngame.edit.java index 15edf0f6..efd5bccb 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiIngame.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiIngame.edit.java @@ -5,22 +5,31 @@ # Version: 1.0 # Author: lax1dude -> INSERT 2 : 7 @ 2 +> INSERT 2 : 12 @ 2 + import java.util.ArrayList; + import java.util.Collection; ++ ++ import net.lax1dude.eaglercraft.v1_8.Display; ++ import net.lax1dude.eaglercraft.v1_8.EagRuntime; + import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; ++ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; ++ import net.lax1dude.eaglercraft.v1_8.Touch; + import net.lax1dude.eaglercraft.v1_8.minecraft.EaglerTextureAtlasSprite; + -> CHANGE 3 : 7 @ 3 : 6 +> CHANGE 3 : 9 @ 3 : 6 ~ ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; ~ import net.lax1dude.eaglercraft.v1_8.opengl.OpenGlHelper; ~ import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; +~ import net.lax1dude.eaglercraft.v1_8.touch_gui.TouchControls; +~ import net.lax1dude.eaglercraft.v1_8.touch_gui.TouchOverlayRenderer; -> DELETE 2 @ 2 : 11 +> CHANGE 2 : 3 @ 2 : 11 + +~ import net.minecraft.client.gui.inventory.GuiInventory; > DELETE 2 @ 2 : 3 @@ -28,7 +37,15 @@ ~ import net.minecraft.client.renderer.entity.RenderManager; -> CHANGE 32 : 33 @ 32 : 33 +> INSERT 13 : 14 @ 13 + ++ import net.minecraft.network.play.client.C16PacketClientStatus; + +> INSERT 11 : 12 @ 11 + ++ import net.minecraft.util.MovingObjectPosition.MovingObjectType; + +> CHANGE 8 : 9 @ 8 : 9 ~ private final EaglercraftRandom rand = new EaglercraftRandom(); @@ -40,34 +57,110 @@ > DELETE 19 @ 19 : 20 -> CHANGE 16 : 19 @ 16 : 21 +> CHANGE 11 : 12 @ 11 : 12 + +~ ScaledResolution scaledresolution = mc.scaledResolution; + +> CHANGE 4 : 7 @ 4 : 9 ~ GlStateManager.tryBlendFuncSeparate(770, 771, 1, 0); ~ GlStateManager.enableDepth(); ~ GlStateManager.disableLighting(); -> DELETE 21 @ 21 : 22 +> INSERT 15 : 17 @ 15 + ++ onBeginHotbarDraw(); ++ + +> DELETE 6 @ 6 : 7 > DELETE 1 @ 1 : 8 -> CHANGE 44 : 45 @ 44 : 47 +> DELETE 1 @ 1 : 2 -~ this.overlayDebug.renderDebugInfo(scaledresolution); +> DELETE 1 @ 1 : 2 -> INSERT 83 : 87 @ 83 +> DELETE 4 @ 4 : 22 + +> INSERT 1 : 2 @ 1 + ++ GlStateManager.disableBlend(); + +> INSERT 7 : 9 @ 7 + ++ GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); ++ GlStateManager.disableBlend(); + +> CHANGE 6 : 8 @ 6 : 14 + +~ GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); +~ GlStateManager.disableBlend(); + +> DELETE 1 @ 1 : 2 + +> INSERT 21 : 22 @ 21 + ++ } + +> CHANGE 1 : 20 @ 1 : 2 + +~ GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); +~ drawEaglerInteractButton(scaledresolution); +~ +~ onEndHotbarDraw(); +~ +~ GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); +~ if (this.mc.thePlayer.getSleepTimer() > 0) { +~ GlStateManager.disableDepth(); +~ GlStateManager.disableAlpha(); +~ int j1 = this.mc.thePlayer.getSleepTimer(); +~ float f1 = (float) j1 / 100.0F; +~ if (f1 > 1.0F) { +~ f1 = 1.0F - (float) (j1 - 100) / 10.0F; +~ } +~ +~ int k = (int) (220.0F * f1) << 24 | 1052704; +~ drawRect(0, 0, i, j, k); +~ GlStateManager.enableAlpha(); +~ GlStateManager.enableDepth(); + +> INSERT 2 : 8 @ 2 + ++ if (this.mc.isDemo()) { ++ this.renderDemo(scaledresolution); ++ } ++ ++ this.overlayDebug.renderDebugInfo(scaledresolution); ++ + +> DELETE 1 @ 1 : 2 + +> DELETE 33 @ 33 : 35 + +> INSERT 18 : 22 @ 18 + if (this.mc.currentScreen == null) { + this.mc.voiceOverlay.drawOverlay(); + } + -> INSERT 4 : 7 @ 4 +> INSERT 4 : 9 @ 4 + if (this.mc.gameSettings.hudWorld && (mc.currentScreen == null || !(mc.currentScreen instanceof GuiChat))) { + j -= 10; + } ++ j -= (this.mc.displayHeight - (Display.getVisualViewportX() + Display.getVisualViewportH())) * j ++ / this.mc.displayHeight; -> INSERT 19 : 30 @ 19 +> DELETE 1 @ 1 : 2 + +> DELETE 1 @ 1 : 2 + +> INSERT 11 : 12 @ 11 + ++ + +> INSERT 4 : 15 @ 4 + public void renderGameOverlayCrosshairs(int scaledResWidth, int scaledResHeight) { + if (this.showCrosshair()) { @@ -81,7 +174,55 @@ + } + -> DELETE 147 @ 147 : 151 +> INSERT 9 : 26 @ 9 + ++ ++ if (PointerInputAbstraction.isTouchMode()) { ++ this.mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); ++ this.drawTexturedModalRect(i + 89, sr.getScaledHeight() - 22, 234, 0, 22, 22); ++ int areaHAdd = 12; ++ hotbarAreaX = (i - 91) * mc.displayWidth / sr.getScaledWidth(); ++ hotbarAreaY = (sr.getScaledHeight() - 22 - areaHAdd) * mc.displayHeight / sr.getScaledHeight(); ++ hotbarAreaW = 203 * mc.displayWidth / sr.getScaledWidth(); ++ hotbarAreaH = (22 + areaHAdd) * mc.displayHeight / sr.getScaledHeight(); ++ } else { ++ hotbarAreaX = -1; ++ hotbarAreaY = -1; ++ hotbarAreaW = -1; ++ hotbarAreaH = -1; ++ } ++ ++ this.mc.getTextureManager().bindTexture(widgetsTexPath); + +> INSERT 16 : 17 @ 16 + ++ + +> DELETE 5 @ 5 : 6 + +> DELETE 9 @ 9 : 11 + +> DELETE 3 @ 3 : 4 + +> DELETE 12 @ 12 : 13 + +> DELETE 1 @ 1 : 2 + +> DELETE 10 @ 10 : 11 + +> DELETE 5 @ 5 : 6 + +> CHANGE 1 : 2 @ 1 : 2 + +~ String s = this.highlightingItemStack.getDisplayNameProfanityFilter(); + +> DELETE 25 @ 25 : 26 + +> DELETE 3 @ 3 : 4 + +> DELETE 11 @ 11 : 12 + +> DELETE 25 @ 25 : 29 > CHANGE 17 : 19 @ 17 : 18 @@ -107,7 +248,23 @@ + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(770, 771, 1, 0); -> CHANGE 248 : 249 @ 248 : 249 +> DELETE 23 @ 23 : 25 + +> DELETE 17 @ 17 : 19 + +> DELETE 61 @ 61 : 63 + +> DELETE 39 @ 39 : 40 + +> DELETE 36 @ 36 : 37 + +> DELETE 14 @ 14 : 15 + +> CHANGE 6 : 7 @ 6 : 9 + +~ int i = mc.scaledResolution.getScaledWidth(); + +> CHANGE 40 : 41 @ 40 : 41 ~ public void renderVignette(float parFloat1, int scaledWidth, int scaledHeight) { @@ -179,4 +336,223 @@ + } + +> INSERT 27 : 243 @ 27 + ++ ++ private int hotbarAreaX = -1; ++ private int hotbarAreaY = -1; ++ private int hotbarAreaW = -1; ++ private int hotbarAreaH = -1; ++ private int currentHotbarSlotTouch = -1; ++ private long hotbarSlotTouchStart = -1l; ++ private boolean hotbarSlotTouchAlreadySelected = false; ++ private int interactButtonX = -1; ++ private int interactButtonY = -1; ++ private int interactButtonW = -1; ++ private int interactButtonH = -1; ++ private int touchVPosX = -1; ++ private int touchVPosY = -1; ++ private int touchEventUID = -1; ++ ++ private void drawEaglerInteractButton(ScaledResolution parScaledResolution) { ++ if (PointerInputAbstraction.isTouchMode() && mc.objectMouseOver != null ++ && mc.objectMouseOver.typeOfHit == MovingObjectType.ENTITY) { ++ int scale = parScaledResolution.getScaleFactor(); ++ interactButtonW = 118 * scale; ++ interactButtonH = 20 * scale; ++ int xx = (parScaledResolution.getScaledWidth() - 118) / 2; ++ int yy = parScaledResolution.getScaledHeight() - 70; ++ interactButtonX = xx * scale; ++ interactButtonY = yy * scale; ++ mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); ++ boolean hover = touchVPosX >= interactButtonX && touchVPosY >= interactButtonY ++ && touchVPosX < interactButtonX + interactButtonW && touchVPosY < interactButtonY + interactButtonH; ++ float f = MathHelper.clamp_float(mc.gameSettings.touchControlOpacity, 0.0f, 1.0f); ++ if (f > 0.0f) { ++ GlStateManager.color(1.0f, 1.0f, 1.0f, f); ++ drawTexturedModalRect(xx, yy, 0, hover ? 216 : 236, 118, 20); ++ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); ++ drawCenteredString(mc.fontRendererObj, I18n.format("touch.interact.entity"), ++ parScaledResolution.getScaledWidth() / 2, yy + 6, ++ (hover ? 16777120 : 14737632) | ((int) (f * 255.0f) << 24)); ++ } ++ } else { ++ interactButtonX = -1; ++ interactButtonY = -1; ++ interactButtonW = -1; ++ interactButtonH = -1; ++ } ++ } ++ ++ private int applyTouchHotbarTransformX(int posX, boolean scaled) { ++ if (scaled) { ++ return (posX + mc.scaledResolution.getScaledWidth() / 4) * 2 / 3; ++ } else { ++ return (posX + mc.displayWidth / 4) * 2 / 3; ++ } ++ } ++ ++ private int applyTouchHotbarTransformY(int posY, boolean scaled) { ++ if (scaled) { ++ return (posY + mc.scaledResolution.getScaledHeight() / 2) * 2 / 3; ++ } else { ++ return (posY + mc.displayHeight / 2) * 2 / 3; ++ } ++ } ++ ++ private void onBeginHotbarDraw() { ++ if (PointerInputAbstraction.isTouchMode()) { ++ GlStateManager.pushMatrix(); ++ ScaledResolution res = mc.scaledResolution; ++ GlStateManager.translate(res.getScaledWidth() / -4, res.getScaledHeight() / -2, field_175199_z); ++ GlStateManager.scale(1.5f, 1.5f, 1.5f); ++ } ++ } ++ ++ private void onEndHotbarDraw() { ++ if (PointerInputAbstraction.isTouchMode()) { ++ GlStateManager.popMatrix(); ++ } ++ } ++ ++ private int getHotbarSlotTouched(int pointX) { ++ int xx = pointX - hotbarAreaX - 2; ++ xx /= 20 * mc.scaledResolution.getScaleFactor(); ++ if (xx < 0) ++ xx = 0; ++ if (xx > 9) ++ xx = 9; ++ return xx; ++ } ++ ++ public boolean handleTouchBeginEagler(int uid, int pointX, int pointY) { ++ if (mc.thePlayer == null) { ++ return false; ++ } ++ if (touchEventUID == -1) { ++ pointX = applyTouchHotbarTransformX(pointX, false); ++ pointY = applyTouchHotbarTransformY(pointY, false); ++ if (pointX >= hotbarAreaX && pointY >= hotbarAreaY && pointX < hotbarAreaX + hotbarAreaW ++ && pointY < hotbarAreaY + hotbarAreaH) { ++ touchEventUID = uid; ++ currentHotbarSlotTouch = getHotbarSlotTouched(pointX); ++ hotbarSlotTouchStart = EagRuntime.steadyTimeMillis(); ++ if (currentHotbarSlotTouch >= 0 && currentHotbarSlotTouch < 9) { ++ if (mc.thePlayer.isSpectator()) { ++ hotbarSlotTouchAlreadySelected = false; ++ mc.ingameGUI.getSpectatorGui().func_175260_a(currentHotbarSlotTouch); ++ } else { ++ hotbarSlotTouchAlreadySelected = (mc.thePlayer.inventory.currentItem == currentHotbarSlotTouch); ++ mc.thePlayer.inventory.currentItem = currentHotbarSlotTouch; ++ } ++ } else if (currentHotbarSlotTouch == 9) { ++ hotbarSlotTouchAlreadySelected = false; ++ currentHotbarSlotTouch = 69; ++ if (mc.playerController.isRidingHorse()) { ++ mc.thePlayer.sendHorseInventory(); ++ } else { ++ mc.getNetHandler().addToSendQueue( ++ new C16PacketClientStatus(C16PacketClientStatus.EnumState.OPEN_INVENTORY_ACHIEVEMENT)); ++ mc.displayGuiScreen(new GuiInventory(mc.thePlayer)); ++ } ++ } ++ return true; ++ } ++ if (pointX >= interactButtonX && pointY >= interactButtonY && pointX < interactButtonX + interactButtonW ++ && pointY < interactButtonY + interactButtonH) { ++ touchEventUID = uid; ++ mc.rightClickMouse(); ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ public boolean handleTouchEndEagler(int uid, int pointX, int pointY) { ++ if (uid == touchEventUID) { ++ if (hotbarSlotTouchStart != -1l && currentHotbarSlotTouch != 69) { ++ if (EagRuntime.steadyTimeMillis() - hotbarSlotTouchStart < 350l) { ++ if (hotbarSlotTouchAlreadySelected) { ++ if (mc.thePlayer != null) { ++ mc.thePlayer.dropOneItem(false); ++ } ++ } ++ } ++ } ++ touchVPosX = -1; ++ touchVPosY = -1; ++ touchEventUID = -1; ++ currentHotbarSlotTouch = -1; ++ hotbarSlotTouchStart = -1l; ++ hotbarSlotTouchAlreadySelected = false; ++ return true; ++ } ++ return false; ++ } ++ ++ public void updateTouchEagler(boolean screenTouched) { ++ if (screenTouched) { ++ int pointCount = Touch.touchPointCount(); ++ for (int i = 0; i < pointCount; ++i) { ++ int uid = Touch.touchPointUID(i); ++ if (TouchControls.touchControls.containsKey(uid)) { ++ continue; ++ } ++ if (touchEventUID == -1 || touchEventUID == uid) { ++ touchVPosX = applyTouchHotbarTransformX(Touch.touchPointX(i), false); ++ touchVPosY = applyTouchHotbarTransformY(mc.displayHeight - Touch.touchPointY(i) - 1, false); ++ long millis = EagRuntime.steadyTimeMillis(); ++ if (touchEventUID != -1 && hotbarSlotTouchStart != -1l) { ++ if (currentHotbarSlotTouch != 69) { ++ int slot = getHotbarSlotTouched(touchVPosX); ++ if (slot != currentHotbarSlotTouch) { ++ hotbarSlotTouchAlreadySelected = false; ++ currentHotbarSlotTouch = slot; ++ hotbarSlotTouchStart = millis; ++ if (slot >= 0 && slot < 9) { ++ if (mc.thePlayer.isSpectator()) { ++ mc.ingameGUI.getSpectatorGui().func_175260_a(slot); ++ } else { ++ mc.thePlayer.inventory.currentItem = slot; ++ } ++ } ++ } else { ++ if (millis - hotbarSlotTouchStart > 1200l) { ++ if (!mc.thePlayer.isSpectator()) { ++ hotbarSlotTouchStart = millis; ++ this.mc.thePlayer.dropOneItem(true); ++ } ++ } ++ } ++ } ++ } ++ return; ++ } ++ } ++ } ++ if (touchEventUID != -1) { ++ handleTouchEndEagler(touchEventUID, touchVPosX, touchVPosY); ++ } ++ touchVPosX = -1; ++ touchVPosY = -1; ++ touchEventUID = -1; ++ currentHotbarSlotTouch = -1; ++ hotbarSlotTouchStart = -1l; ++ hotbarSlotTouchAlreadySelected = false; ++ } ++ ++ public boolean isTouchOverlapEagler(int uid, int tx, int ty) { ++ if (touchEventUID == uid) { ++ return true; ++ } ++ ty = mc.displayHeight - ty - 1; ++ tx = applyTouchHotbarTransformX(tx, false); ++ ty = applyTouchHotbarTransformY(ty, false); ++ return (tx >= hotbarAreaX && ty >= hotbarAreaY && tx < hotbarAreaX + hotbarAreaW ++ && ty < hotbarAreaY + hotbarAreaH) ++ || (tx >= interactButtonX && ty >= interactButtonY && tx < interactButtonX + interactButtonW ++ && ty < interactButtonY + interactButtonH); ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiIngameMenu.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiIngameMenu.edit.java index dcf9da2d..83439317 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiIngameMenu.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiIngameMenu.edit.java @@ -5,10 +5,14 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 14 @ 2 : 9 +> CHANGE 2 : 21 @ 2 : 9 ~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ~ import net.lax1dude.eaglercraft.v1_8.Mouse; +~ import net.lax1dude.eaglercraft.v1_8.PauseMenuCustomizeState; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.GuiButtonWithStupidIcons; +~ import net.lax1dude.eaglercraft.v1_8.notifications.GuiButtonNotifBell; +~ import net.lax1dude.eaglercraft.v1_8.notifications.GuiScreenNotifications; ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; ~ import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; ~ import net.lax1dude.eaglercraft.v1_8.sp.gui.GuiScreenLANInfo; @@ -17,6 +21,9 @@ ~ import net.lax1dude.eaglercraft.v1_8.sp.lan.LANServerController; ~ import net.lax1dude.eaglercraft.v1_8.update.GuiUpdateCheckerOverlay; ~ import net.lax1dude.eaglercraft.v1_8.voice.GuiVoiceMenu; +~ import net.lax1dude.eaglercraft.v1_8.webview.GuiScreenPhishingWaring; +~ import net.lax1dude.eaglercraft.v1_8.webview.GuiScreenRecieveServerInfo; +~ import net.lax1dude.eaglercraft.v1_8.webview.GuiScreenServerInfo; ~ import net.minecraft.client.Minecraft; ~ import net.minecraft.client.audio.PositionedSoundRecord; @@ -28,7 +35,7 @@ > DELETE 2 @ 2 : 4 -> INSERT 1 : 15 @ 1 +> INSERT 1 : 16 @ 1 + private GuiButton lanButton; + @@ -36,6 +43,7 @@ + + private GuiUpdateCheckerOverlay updateCheckerOverlay; + private GuiVoiceMenu voiceMenu; ++ private GuiButtonNotifBell notifBellButton; + + public GuiIngameMenu() { + updateCheckerOverlay = new GuiUpdateCheckerOverlay(true, this); @@ -51,14 +59,56 @@ + this.updateCheckerOverlay.setResolution(mc, width, height); -> CHANGE 12 : 14 @ 12 : 15 +> CHANGE 2 : 6 @ 2 : 4 -~ this.buttonList.add(lanButton = new GuiButton(7, this.width / 2 + 2, this.height / 4 + 96 + b0, 98, 20, -~ I18n.format(LANServerController.isLANOpen() ? "menu.closeLan" : "menu.openToLan", new Object[0]))); +~ this.buttonList.add(new GuiButtonWithStupidIcons(1, this.width / 2 - 100, this.height / 4 + 120 + b0, +~ I18n.format("menu.returnToMenu", new Object[0]), PauseMenuCustomizeState.icon_disconnect_L, +~ PauseMenuCustomizeState.icon_disconnect_L_aspect, PauseMenuCustomizeState.icon_disconnect_R, +~ PauseMenuCustomizeState.icon_disconnect_R_aspect)); -> CHANGE 4 : 9 @ 4 : 5 +> INSERT 2 : 6 @ 2 ++ if (this.mc.thePlayer != null && this.mc.thePlayer.sendQueue.getEaglerMessageProtocol().ver >= 4) { ++ this.buttonList.add(notifBellButton = new GuiButtonNotifBell(11, width - 22, height - 22)); ++ notifBellButton.setUnread(mc.thePlayer.sendQueue.getNotifManager().getUnread()); ++ } + +> CHANGE 2 : 40 @ 2 : 14 + +~ this.buttonList.add(new GuiButtonWithStupidIcons(4, this.width / 2 - 100, this.height / 4 + 24 + b0, +~ I18n.format("menu.returnToGame", new Object[0]), PauseMenuCustomizeState.icon_backToGame_L, +~ PauseMenuCustomizeState.icon_backToGame_L_aspect, PauseMenuCustomizeState.icon_backToGame_R, +~ PauseMenuCustomizeState.icon_backToGame_R_aspect)); +~ this.buttonList.add(new GuiButtonWithStupidIcons(0, this.width / 2 - 100, this.height / 4 + 96 + b0, 98, 20, +~ I18n.format("menu.options", new Object[0]), PauseMenuCustomizeState.icon_options_L, +~ PauseMenuCustomizeState.icon_options_L_aspect, PauseMenuCustomizeState.icon_options_R, +~ PauseMenuCustomizeState.icon_options_R_aspect)); +~ this.buttonList +~ .add(lanButton = new GuiButtonWithStupidIcons(7, this.width / 2 + 2, this.height / 4 + 96 + b0, 98, 20, +~ I18n.format(LANServerController.isLANOpen() ? "menu.closeLan" : "menu.openToLan", +~ new Object[0]), +~ PauseMenuCustomizeState.icon_discord_L, PauseMenuCustomizeState.icon_discord_L_aspect, +~ PauseMenuCustomizeState.icon_discord_R, PauseMenuCustomizeState.icon_discord_R_aspect)); +~ this.buttonList.add(new GuiButtonWithStupidIcons(5, this.width / 2 - 100, this.height / 4 + 48 + b0, 98, 20, +~ I18n.format("gui.achievements", new Object[0]), PauseMenuCustomizeState.icon_achievements_L, +~ PauseMenuCustomizeState.icon_achievements_L_aspect, PauseMenuCustomizeState.icon_achievements_R, +~ PauseMenuCustomizeState.icon_achievements_R_aspect)); +~ this.buttonList.add(new GuiButtonWithStupidIcons(6, this.width / 2 + 2, this.height / 4 + 48 + b0, 98, 20, +~ I18n.format("gui.stats", new Object[0]), PauseMenuCustomizeState.icon_statistics_L, +~ PauseMenuCustomizeState.icon_statistics_L_aspect, PauseMenuCustomizeState.icon_statistics_R, +~ PauseMenuCustomizeState.icon_statistics_R_aspect)); ~ lanButton.enabled = SingleplayerServerController.isWorldRunning(); +~ if (PauseMenuCustomizeState.discordButtonMode != PauseMenuCustomizeState.DISCORD_MODE_NONE) { +~ lanButton.enabled = true; +~ lanButton.id = 8; +~ lanButton.displayString = "" + PauseMenuCustomizeState.discordButtonText; +~ } +~ if (PauseMenuCustomizeState.serverInfoMode != PauseMenuCustomizeState.DISCORD_MODE_NONE) { +~ this.buttonList.add(new GuiButtonWithStupidIcons(9, this.width / 2 - 100, this.height / 4 + 72 + b0, +~ PauseMenuCustomizeState.serverInfoButtonText, PauseMenuCustomizeState.icon_serverInfo_L, +~ PauseMenuCustomizeState.icon_serverInfo_L_aspect, PauseMenuCustomizeState.icon_serverInfo_R, +~ PauseMenuCustomizeState.icon_serverInfo_R_aspect)); +~ } ~ if (!hasSentAutoSave) { ~ hasSentAutoSave = true; ~ SingleplayerServerController.autoSave(); @@ -80,7 +130,7 @@ ~ this.mc.shutdownIntegratedServer(new GuiMultiplayer(new GuiMainMenu())); -> CHANGE 16 : 30 @ 16 : 17 +> CHANGE 16 : 69 @ 16 : 17 ~ if (!LANServerController.supported()) { ~ mc.displayGuiScreen(new GuiScreenLANNotSupported(this)); @@ -96,8 +146,47 @@ ~ new GuiShareToLan(this, this.mc.playerController.getCurrentGameType().getName()))); ~ } ~ break; +~ case 8: +~ if (PauseMenuCustomizeState.discordButtonMode == PauseMenuCustomizeState.DISCORD_MODE_INVITE_URL +~ && PauseMenuCustomizeState.discordInviteURL != null) { +~ EagRuntime.openLink(PauseMenuCustomizeState.discordInviteURL); +~ } +~ break; +~ case 9: +~ switch (PauseMenuCustomizeState.serverInfoMode) { +~ case PauseMenuCustomizeState.SERVER_INFO_MODE_EXTERNAL_URL: +~ if (PauseMenuCustomizeState.serverInfoURL != null) { +~ EagRuntime.openLink(PauseMenuCustomizeState.serverInfoURL); +~ } +~ break; +~ case PauseMenuCustomizeState.SERVER_INFO_MODE_SHOW_EMBED_OVER_HTTP: +~ if (PauseMenuCustomizeState.serverInfoURL != null) { +~ GuiScreen screen = GuiScreenServerInfo.createForCurrentState(this, +~ PauseMenuCustomizeState.serverInfoURL); +~ if (!this.mc.gameSettings.hasHiddenPhishWarning && !GuiScreenPhishingWaring.hasShownMessage) { +~ screen = new GuiScreenPhishingWaring(screen); +~ } +~ this.mc.displayGuiScreen(screen); +~ } +~ break; +~ case PauseMenuCustomizeState.SERVER_INFO_MODE_SHOW_EMBED_OVER_WS: +~ if (PauseMenuCustomizeState.serverInfoHash != null) { +~ GuiScreen screen = new GuiScreenRecieveServerInfo(this, PauseMenuCustomizeState.serverInfoHash); +~ if (!this.mc.gameSettings.hasHiddenPhishWarning && !GuiScreenPhishingWaring.hasShownMessage) { +~ screen = new GuiScreenPhishingWaring(screen); +~ } +~ this.mc.displayGuiScreen(screen); +~ } +~ break; +~ default: +~ break; +~ } +~ break; +~ case 11: +~ this.mc.displayGuiScreen(new GuiScreenNotifications(this)); +~ break; -> CHANGE 6 : 13 @ 6 : 7 +> CHANGE 6 : 16 @ 6 : 7 ~ if (EagRuntime.getConfiguration().isAllowVoiceClient() ~ && (!mc.isSingleplayer() || LANServerController.isHostingLAN())) { @@ -106,13 +195,34 @@ ~ if (Mouse.isActuallyGrabbed()) { ~ Mouse.setGrabbed(false); ~ } +~ if (notifBellButton != null && mc.thePlayer != null) { +~ notifBellButton.setUnread(mc.thePlayer.sendQueue.getNotifManager().getUnread()); +~ } -> CHANGE 4 : 5 @ 4 : 5 - -~ this.drawCenteredString(this.fontRendererObj, I18n.format("menu.game", new Object[0]), this.width / 2, 20, - -> CHANGE 1 : 55 @ 1 : 2 +> CHANGE 4 : 80 @ 4 : 7 +~ String titleStr = I18n.format("menu.game", new Object[0]); +~ int titleStrWidth = fontRendererObj.getStringWidth(titleStr); +~ this.drawString(this.fontRendererObj, titleStr, (this.width - titleStrWidth) / 2, 20, 16777215); +~ if (PauseMenuCustomizeState.icon_title_L != null) { +~ mc.getTextureManager().bindTexture(PauseMenuCustomizeState.icon_title_L); +~ GlStateManager.pushMatrix(); +~ GlStateManager.translate( +~ (this.width - titleStrWidth) / 2 - 6 - 16 * PauseMenuCustomizeState.icon_title_L_aspect, 16, 0.0f); +~ float f2 = 16.0f / 256.0f; +~ GlStateManager.scale(f2 * PauseMenuCustomizeState.icon_title_L_aspect, f2, f2); +~ this.drawTexturedModalRect(0, 0, 0, 0, 256, 256); +~ GlStateManager.popMatrix(); +~ } +~ if (PauseMenuCustomizeState.icon_title_R != null) { +~ mc.getTextureManager().bindTexture(PauseMenuCustomizeState.icon_title_L); +~ GlStateManager.pushMatrix(); +~ GlStateManager.translate((this.width - titleStrWidth) / 2 + titleStrWidth + 6, 16, 0.0f); +~ float f2 = 16.0f / 256.0f; +~ GlStateManager.scale(f2 * PauseMenuCustomizeState.icon_title_R_aspect, f2, f2); +~ this.drawTexturedModalRect(0, 0, 0, 0, 256, 256); +~ GlStateManager.popMatrix(); +~ } ~ ~ this.updateCheckerOverlay.drawScreen(i, j, f); ~ @@ -168,7 +278,7 @@ ~ } catch (GuiVoiceMenu.AbortedException ex) { ~ } -> INSERT 1 : 80 @ 1 +> INSERT 1 : 84 @ 1 + + protected void keyTyped(char par1, int par2) { @@ -249,5 +359,9 @@ + } catch (GuiVoiceMenu.AbortedException ex) { + } + } ++ ++ protected boolean isPartOfPauseMenu() { ++ return true; ++ } > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiKeyBindingList.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiKeyBindingList.edit.java index ba289f0e..3ed0d132 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiKeyBindingList.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiKeyBindingList.edit.java @@ -5,16 +5,21 @@ # Version: 1.0 # Author: lax1dude -> INSERT 3 : 5 @ 3 +> INSERT 3 : 6 @ 3 + + import net.lax1dude.eaglercraft.v1_8.ArrayUtils; ++ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; > DELETE 1 @ 1 : 4 > DELETE 4 @ 4 : 5 -> CHANGE 17 : 19 @ 17 : 18 +> CHANGE 8 : 9 @ 8 : 9 + +~ super(mcIn, controls.width, controls.height, 66, controls.height - 32, 20); + +> CHANGE 8 : 10 @ 8 : 9 ~ for (int l = 0; l < akeybinding.length; ++l) { ~ KeyBinding keybinding = akeybinding[l]; @@ -25,4 +30,27 @@ ~ for (int m = 0; m < kb.length; ++m) { ~ KeyBinding keybindingx = kb[m]; +> CHANGE 19 : 24 @ 19 : 20 + +~ if (var4 != 0 && var4 != 12345) +~ return false; +~ boolean touchMode = PointerInputAbstraction.isTouchMode(); +~ if ((!touchMode || (this.btnChangeKeyBinding.isSliderTouchEvents() == (var4 == 12345))) +~ && this.btnChangeKeyBinding.mousePressed(GuiKeyBindingList.this.mc, i, j)) { + +> CHANGE 2 : 4 @ 2 : 3 + +~ } else if ((!touchMode || (this.btnReset.isSliderTouchEvents() == (var4 == 12345))) +~ && this.btnReset.mousePressed(GuiKeyBindingList.this.mc, i, j)) { + +> CHANGE 10 : 17 @ 10 : 12 + +~ if (var4 != 0 && var4 != 12345) +~ return; +~ boolean touchMode = PointerInputAbstraction.isTouchMode(); +~ if (!touchMode || (this.btnChangeKeyBinding.isSliderTouchEvents() == (var4 == 12345))) +~ this.btnChangeKeyBinding.mouseReleased(i, j); +~ if (!touchMode || (this.btnReset.isSliderTouchEvents() == (var4 == 12345))) +~ this.btnReset.mouseReleased(i, j); + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiLanguage.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiLanguage.edit.java index 41dd1b8d..3ed88ce3 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiLanguage.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiLanguage.edit.java @@ -16,11 +16,20 @@ > DELETE 1 @ 1 : 6 -> CHANGE 34 : 35 @ 34 : 35 +> CHANGE 34 : 40 @ 34 : 35 +~ public void handleTouchInput() throws IOException { +~ super.handleTouchInput(); +~ this.list.handleTouchInput(); +~ } +~ ~ protected void actionPerformed(GuiButton parGuiButton) { -> INSERT 56 : 57 @ 56 +> CHANGE 12 : 13 @ 12 : 13 + +~ ScaledResolution scaledresolution = this.mc.scaledResolution; + +> INSERT 43 : 44 @ 43 + this.mc.loadingScreen.eaglerShowRefreshResources(); diff --git a/patches/minecraft/net/minecraft/client/gui/GuiMainMenu.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiMainMenu.edit.java index 9c5354c8..713bced6 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiMainMenu.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiMainMenu.edit.java @@ -182,16 +182,15 @@ ~ protected void keyTyped(char parChar1, int parInt1) { -> CHANGE 3 : 9 @ 3 : 7 +> CHANGE 3 : 8 @ 3 : 6 ~ if (viewportTexture == null) { ~ viewportTexture = new DynamicTexture(256, 256); ~ backgroundTexture = this.mc.getTextureManager().getDynamicTextureLocation("background", viewportTexture); ~ } ~ this.updateCheckerOverlay.setResolution(mc, width, height); -~ Calendar calendar = EagRuntime.getLocaleCalendar(); -> DELETE 9 @ 9 : 10 +> DELETE 10 @ 10 : 11 > INSERT 1 : 8 @ 1 diff --git a/patches/minecraft/net/minecraft/client/gui/GuiMultiplayer.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiMultiplayer.edit.java index ea4be681..215cb262 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiMultiplayer.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiMultiplayer.edit.java @@ -10,12 +10,14 @@ + import java.io.IOException; + -> CHANGE 2 : 16 @ 2 : 15 +> CHANGE 2 : 18 @ 2 : 15 ~ +~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ~ import net.lax1dude.eaglercraft.v1_8.EaglerXBungeeVersion; ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; ~ import net.lax1dude.eaglercraft.v1_8.Mouse; +~ import net.lax1dude.eaglercraft.v1_8.cookie.ServerCookieDataStore; ~ import net.lax1dude.eaglercraft.v1_8.internal.EnumCursorType; ~ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; ~ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; @@ -80,7 +82,15 @@ + lanServerList.forceRefresh(); + } -> CHANGE 32 : 35 @ 32 : 36 +> INSERT 12 : 17 @ 12 + ++ public void handleTouchInput() throws IOException { ++ super.handleTouchInput(); ++ this.serverListSelector.handleTouchInput(); ++ } ++ + +> CHANGE 20 : 23 @ 20 : 24 ~ this.savedServerList.updateServerPing(); ~ if (lanServerList.update()) { @@ -111,7 +121,7 @@ > CHANGE 3 : 8 @ 3 : 4 -~ long millis = System.currentTimeMillis(); +~ long millis = EagRuntime.steadyTimeMillis(); ~ if (millis - lastRefreshCommit > 700l) { ~ lastRefreshCommit = millis; ~ this.refreshServerList(); @@ -123,23 +133,35 @@ > CHANGE 14 : 19 @ 14 : 16 -~ long millis = System.currentTimeMillis(); +~ long millis = EagRuntime.steadyTimeMillis(); ~ if (millis - lastRefreshCommit > 700l) { ~ lastRefreshCommit = millis; ~ this.refreshServerList(); ~ } -> CHANGE 15 : 20 @ 15 : 17 +> INSERT 10 : 13 @ 10 -~ long millis = System.currentTimeMillis(); ++ if (!this.selectedServer.enableCookies) { ++ ServerCookieDataStore.clearCookie(this.selectedServer.serverIP); ++ } + +> CHANGE 5 : 10 @ 5 : 7 + +~ long millis = EagRuntime.steadyTimeMillis(); ~ if (millis - lastRefreshCommit > 700l) { ~ lastRefreshCommit = millis; ~ this.refreshServerList(); ~ } -> CHANGE 10 : 15 @ 10 : 12 +> INSERT 6 : 9 @ 6 -~ long millis = System.currentTimeMillis(); ++ if (serverdata.enableCookies && !this.selectedServer.enableCookies) { ++ ServerCookieDataStore.clearCookie(this.selectedServer.serverIP); ++ } + +> CHANGE 4 : 9 @ 4 : 6 + +~ long millis = EagRuntime.steadyTimeMillis(); ~ if (millis - lastRefreshCommit > 700l) { ~ lastRefreshCommit = millis; ~ this.refreshServerList(); @@ -171,7 +193,11 @@ + relaysButton.drawScreen(i, j); + drawPluginDownloadLink(i, j); -> INSERT 3 : 4 @ 3 +> INSERT 2 : 3 @ 2 + ++ GlStateManager.disableLighting(); + +> INSERT 1 : 2 @ 1 + } diff --git a/patches/minecraft/net/minecraft/client/gui/GuiNewChat.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiNewChat.edit.java index fb3bda12..f0c9e36d 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiNewChat.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiNewChat.edit.java @@ -28,4 +28,8 @@ ~ this.field_146253_i.add(0, new ChatLine(parInt2, (IChatComponent) list.get(j), parInt1)); +> CHANGE 62 : 63 @ 62 : 63 + +~ ScaledResolution scaledresolution = mc.scaledResolution; + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiOptionSlider.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiOptionSlider.edit.java index 20d8a9ce..7e726ab7 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiOptionSlider.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiOptionSlider.edit.java @@ -11,4 +11,22 @@ > DELETE 1 @ 1 : 3 +> CHANGE 4 : 5 @ 4 : 5 + +~ public float sliderValue; + +> INSERT 21 : 25 @ 21 + ++ public GameSettings.Options getEnumOptions() { ++ return options; ++ } ++ + +> INSERT 40 : 44 @ 40 + ++ ++ public boolean isSliderTouchEvents() { ++ return true; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiOptions.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiOptions.edit.java index 726b582c..d81dd46b 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiOptions.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiOptions.edit.java @@ -5,18 +5,26 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 13 @ 2 : 3 +> CHANGE 2 : 21 @ 2 : 3 ~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ~ import net.lax1dude.eaglercraft.v1_8.Mouse; +~ import net.lax1dude.eaglercraft.v1_8.boot_menu.GuiScreenEnterBootMenu; +~ import net.lax1dude.eaglercraft.v1_8.cookie.GuiScreenRevokeSessionToken; +~ import net.lax1dude.eaglercraft.v1_8.cookie.ServerCookieDataStore; ~ import net.lax1dude.eaglercraft.v1_8.internal.EnumCursorType; ~ import net.lax1dude.eaglercraft.v1_8.internal.EnumPlatformType; +~ import net.lax1dude.eaglercraft.v1_8.internal.KeyboardConstants; ~ import net.lax1dude.eaglercraft.v1_8.minecraft.EaglerFolderResourcePack; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.GuiScreenGenericErrorMessage; ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; ~ import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.EaglerDeferredPipeline; ~ import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.gui.GuiShaderConfig; ~ import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.gui.GuiShadersNotSupported; ~ import net.lax1dude.eaglercraft.v1_8.profile.GuiScreenImportExportProfile; +~ import net.lax1dude.eaglercraft.v1_8.recording.GuiScreenRecordingNote; +~ import net.lax1dude.eaglercraft.v1_8.recording.GuiScreenRecordingSettings; +~ import net.lax1dude.eaglercraft.v1_8.recording.ScreenRecordingController; ~ import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; > DELETE 1 @ 1 : 21 @@ -41,11 +49,12 @@ ~ I18n.format("shaders.gui.optionsButton"))); -> CHANGE 2 : 5 @ 2 : 4 +> CHANGE 2 : 6 @ 2 : 4 +~ boolean support = ScreenRecordingController.isSupported(); ~ this.buttonList.add(broadcastSettings = new GuiButton(107, this.width / 2 + 5, this.height / 6 + 72 - 6, 150, -~ 20, I18n.format(EagRuntime.getRecText(), new Object[0]))); -~ broadcastSettings.enabled = EagRuntime.recSupported(); +~ 20, I18n.format(support ? "options.screenRecording.button" : "options.screenRecording.unsupported"))); +~ broadcastSettings.enabled = support; > CHANGE 8 : 10 @ 8 : 9 @@ -87,24 +96,29 @@ > DELETE 22 @ 22 : 27 -> CHANGE 16 : 18 @ 16 : 23 +> CHANGE 16 : 22 @ 16 : 22 -~ EagRuntime.toggleRec(); -~ broadcastSettings.displayString = I18n.format(EagRuntime.getRecText(), new Object[0]); +~ if (ScreenRecordingController.isSupported()) { +~ GuiScreen screen = new GuiScreenRecordingSettings(this); +~ if (!GuiScreenRecordingNote.hasShown) { +~ screen = new GuiScreenRecordingNote(screen); +~ } +~ this.mc.displayGuiScreen(screen); -> INSERT 2 : 5 @ 2 +> INSERT 3 : 6 @ 3 + if (parGuiButton.id == 104) { + EagRuntime.showDebugConsole(); + } -> INSERT 6 : 24 @ 6 +> INSERT 6 : 49 @ 6 + + if (mc.theWorld == null && !EagRuntime.getConfiguration().isDemo()) { + GlStateManager.pushMatrix(); + GlStateManager.scale(0.75f, 0.75f, 0.75f); + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); ++ + String text = I18n.format("editProfile.importExport"); + + int w = mc.fontRendererObj.getStringWidth(text); @@ -118,9 +132,43 @@ + GlStateManager.popMatrix(); + } + ++ if (mc.theWorld == null && EagRuntime.getConfiguration().isAllowBootMenu()) { ++ drawCenteredString(mc.fontRendererObj, I18n.format("options.pressDeleteText"), width / 2, height / 6 + 22, ++ 11184810); ++ } ++ ++ if (EagRuntime.getConfiguration().isEnableServerCookies() && mc.thePlayer == null) { ++ GlStateManager.pushMatrix(); ++ GlStateManager.scale(0.75f, 0.75f, 0.75f); ++ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); ++ ++ String text = I18n.format("revokeSessionToken.button"); ++ ++ int w = mc.fontRendererObj.getStringWidth(text); ++ boolean hover = i > width - 5 - (w + 5) * 3 / 4 && j > 1 && i < width - 2 && j < 12; ++ if (hover) { ++ Mouse.showCursor(EnumCursorType.HAND); ++ } ++ ++ drawString(mc.fontRendererObj, EnumChatFormatting.UNDERLINE + text, (width - 1) * 4 / 3 - w - 5, 5, ++ hover ? 0xFFEEEE22 : 0xFFCCCCCC); ++ ++ GlStateManager.popMatrix(); ++ } ++ -> INSERT 2 : 14 @ 2 +> INSERT 2 : 35 @ 2 ++ ++ @Override ++ protected void keyTyped(char parChar1, int parInt1) { ++ super.keyTyped(parChar1, parInt1); ++ if (parInt1 == KeyboardConstants.KEY_DELETE || parInt1 == KeyboardConstants.KEY_BACK) { ++ if (mc.theWorld == null && EagRuntime.getConfiguration().isAllowBootMenu()) { ++ mc.displayGuiScreen(new GuiScreenEnterBootMenu(this)); ++ } ++ } ++ } + + protected void mouseClicked(int mx, int my, int button) { + super.mouseClicked(mx, my, button); @@ -132,6 +180,17 @@ + .playSound(PositionedSoundRecord.create(new ResourceLocation("gui.button.press"), 1.0F)); + } + } ++ if (EagRuntime.getConfiguration().isEnableServerCookies() && mc.thePlayer == null) { ++ int w = mc.fontRendererObj.getStringWidth(I18n.format("revokeSessionToken.button")); ++ if (mx > width - 5 - (w + 5) * 3 / 4 && my > 1 && mx < width - 2 && my < 12) { ++ ServerCookieDataStore.flush(); ++ mc.displayGuiScreen(ServerCookieDataStore.numRevokable() == 0 ++ ? new GuiScreenGenericErrorMessage("errorNoSessions.title", "errorNoSessions.desc", this) ++ : new GuiScreenRevokeSessionToken(this)); ++ mc.getSoundHandler() ++ .playSound(PositionedSoundRecord.create(new ResourceLocation("gui.button.press"), 1.0F)); ++ } ++ } + } > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiOptionsRowList.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiOptionsRowList.edit.java index ad47b72c..a5f47961 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiOptionsRowList.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiOptionsRowList.edit.java @@ -7,12 +7,88 @@ > DELETE 2 @ 2 : 3 -> INSERT 1 : 4 @ 1 +> INSERT 1 : 5 @ 1 + + import com.google.common.collect.Lists; + ++ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; > DELETE 1 @ 1 : 5 +> CHANGE 72 : 77 @ 72 : 73 + +~ if (var4 != 0 && var4 != 12345) +~ return false; +~ boolean touchMode = PointerInputAbstraction.isTouchMode(); +~ if ((!touchMode || (this.field_148323_b.isSliderTouchEvents() == (var4 == 12345))) +~ && this.field_148323_b.mousePressed(this.field_148325_a, i, j)) { + +> CHANGE 8 : 11 @ 8 : 9 + +~ } else if (this.field_148324_c != null +~ && (!touchMode || (this.field_148324_c.isSliderTouchEvents() == (var4 == 12345))) +~ && this.field_148324_c.mousePressed(this.field_148325_a, i, j)) { + +> CHANGE 14 : 19 @ 14 : 15 + +~ if (var4 != 0 && var4 != 12345) +~ return; +~ boolean touchMode = PointerInputAbstraction.isTouchMode(); +~ if (this.field_148323_b != null +~ && (!touchMode || (this.field_148323_b.isSliderTouchEvents() == (var4 == 12345)))) { + +> CHANGE 3 : 5 @ 3 : 4 + +~ if (this.field_148324_c != null +~ && (!touchMode || (this.field_148324_c.isSliderTouchEvents() == (var4 == 12345)))) { + +> INSERT 8 : 53 @ 8 + ++ ++ public GuiOptionButton getButtonFor(GameSettings.Options enumOption) { ++ for (Row r : field_148184_k) { ++ if (r.field_148323_b != null) { ++ if (r.field_148323_b instanceof GuiOptionButton) { ++ GuiOptionButton btn = (GuiOptionButton) r.field_148323_b; ++ if (btn.returnEnumOptions() == enumOption) { ++ return btn; ++ } ++ } ++ } ++ if (r.field_148324_c != null) { ++ if (r.field_148324_c instanceof GuiOptionButton) { ++ GuiOptionButton btn = (GuiOptionButton) r.field_148324_c; ++ if (btn.returnEnumOptions() == enumOption) { ++ return btn; ++ } ++ } ++ } ++ } ++ return null; ++ } ++ ++ public GuiOptionSlider getSliderFor(GameSettings.Options enumOption) { ++ for (Row r : field_148184_k) { ++ if (r.field_148323_b != null) { ++ if (r.field_148323_b instanceof GuiOptionSlider) { ++ GuiOptionSlider btn = (GuiOptionSlider) r.field_148323_b; ++ if (btn.getEnumOptions() == enumOption) { ++ return btn; ++ } ++ } ++ } ++ if (r.field_148324_c != null) { ++ if (r.field_148324_c instanceof GuiOptionSlider) { ++ GuiOptionSlider btn = (GuiOptionSlider) r.field_148324_c; ++ if (btn.getEnumOptions() == enumOption) { ++ return btn; ++ } ++ } ++ } ++ } ++ return null; ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiOverlayDebug.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiOverlayDebug.edit.java index 5e45371f..b30c05d1 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiOverlayDebug.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiOverlayDebug.edit.java @@ -18,8 +18,11 @@ + import java.util.Locale; -> INSERT 1 : 14 @ 1 +> INSERT 1 : 17 @ 1 ++ ++ import org.apache.commons.lang3.StringUtils; ++ + import java.util.TimeZone; + + import com.google.common.base.Strings; @@ -52,14 +55,11 @@ + public int playerOffset = 0; -> INSERT 7 : 10 @ 7 - -+ playerOffset = 0; -+ int ww = scaledResolutionIn.getScaledWidth(); -+ int hh = scaledResolutionIn.getScaledHeight(); - -> CHANGE 1 : 22 @ 1 : 7 +> CHANGE 7 : 31 @ 7 : 14 +~ playerOffset = 0; +~ int ww = scaledResolutionIn.getScaledWidth(); +~ int hh = scaledResolutionIn.getScaledHeight(); ~ if (this.mc.gameSettings.showDebugInfo) { ~ GlStateManager.pushMatrix(); ~ this.renderDebugInfoLeft(); @@ -82,34 +82,33 @@ ~ } ~ -> INSERT 2 : 26 @ 2 +> CHANGE 2 : 25 @ 2 : 3 -+ if (this.mc.currentScreen == null || !(this.mc.currentScreen instanceof GuiChat)) { -+ if (this.mc.gameSettings.hudStats) { -+ drawStatsHUD(ww - 2, hh - 2); -+ } -+ -+ if (this.mc.gameSettings.hudWorld) { -+ drawWorldHUD(2, hh - 2); -+ } -+ } -+ -+ if (this.mc.gameSettings.hudCoords && this.mc.joinWorldTickCounter < 80) { -+ if (this.mc.joinWorldTickCounter > 70) { -+ GlStateManager.enableBlend(); -+ GlStateManager.blendFunc(770, 771); -+ } -+ int i = this.mc.joinWorldTickCounter - 70; -+ if (i < 0) -+ i = 0; -+ drawHideHUD(ww / 2, hh - 70, (10 - i) * 0xFF / 10); -+ if (this.mc.joinWorldTickCounter > 70) { -+ GlStateManager.disableBlend(); -+ } -+ } -+ +~ if (this.mc.currentScreen == null || !(this.mc.currentScreen instanceof GuiChat)) { +~ if (this.mc.gameSettings.hudStats) { +~ drawStatsHUD(ww - 2, hh - 2); +~ } +~ +~ if (this.mc.gameSettings.hudWorld) { +~ drawWorldHUD(2, hh - 2); +~ } +~ } +~ +~ if (this.mc.gameSettings.hudCoords && this.mc.joinWorldTickCounter < 80) { +~ if (this.mc.joinWorldTickCounter > 70) { +~ GlStateManager.enableBlend(); +~ GlStateManager.blendFunc(770, 771); +~ } +~ int i = this.mc.joinWorldTickCounter - 70; +~ if (i < 0) +~ i = 0; +~ drawHideHUD(ww / 2, hh - 70, (10 - i) * 0xFF / 10); +~ if (this.mc.joinWorldTickCounter > 70) { +~ GlStateManager.disableBlend(); +~ } +~ } -> INSERT 3 : 142 @ 3 +> INSERT 2 : 140 @ 2 + private void drawFPS(int x, int y) { + this.fontRenderer.drawStringWithShadow(this.mc.renderGlobal.getDebugInfoShort(), x, y, 0xFFFFFF); @@ -205,7 +204,6 @@ + final double dticks = ticks - minutes * ticksPerMinute; + final long seconds = (long) Math.floor(dticks / ticksPerSecond); + -+ // TODO: why does desktop JRE not apply "GMT" correctly? + final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"), Locale.ENGLISH); + + cal.setLenient(true); @@ -215,10 +213,10 @@ + cal.add(Calendar.MINUTE, (int) minutes); + cal.add(Calendar.SECOND, (int) seconds + 1); + ++ SimpleDateFormat fmt = this.mc.gameSettings.hud24h ? SDFTwentyFour : SDFTwelve; ++ fmt.setCalendar(cal); + String timeString = EnumChatFormatting.WHITE + "Day " + ((totalTicks + 30000l) / 24000l) + " (" -+ + EnumChatFormatting.YELLOW -+ + (this.mc.gameSettings.hud24h ? SDFTwentyFour : SDFTwelve).format(cal.getTime()) -+ + EnumChatFormatting.WHITE + ")"; ++ + EnumChatFormatting.YELLOW + fmt.format(cal.getTime()) + EnumChatFormatting.WHITE + ")"; + + Entity e = mc.getRenderViewEntity(); + BlockPos blockpos = new BlockPos(e.posX, MathHelper.clamp_double(e.getEntityBoundingBox().minY, 0.0D, 254.0D), @@ -251,7 +249,7 @@ + } + -> INSERT 4 : 37 @ 4 +> INSERT 4 : 44 @ 4 + private int drawSingleplayerStats(ScaledResolution parScaledResolution) { + if (mc.isDemo()) { @@ -263,23 +261,30 @@ + if (tpsAge < 20000l) { + int color = tpsAge > 2000l ? 0x777777 : 0xFFFFFF; + List strs = SingleplayerServerController.getTPS(); ++ if (SingleplayerServerController.isRunningSingleThreadMode()) { ++ strs = Lists.newArrayList(strs); ++ strs.add(""); ++ strs.add(I18n.format("singleplayer.tpscounter.singleThreadMode")); ++ } + int l; + boolean first = true; + for (int j = 0, m = strs.size(); j < m; ++j) { + String str = strs.get(j); -+ l = (int) (this.fontRenderer.getStringWidth(str) * (!first ? 0.5f : 1.0f)); -+ GlStateManager.pushMatrix(); -+ GlStateManager.translate(parScaledResolution.getScaledWidth() - 2 - l, i + 2, 0.0f); -+ if (!first) { -+ GlStateManager.scale(0.5f, 0.5f, 0.5f); ++ if (!StringUtils.isAllEmpty(str)) { ++ l = (int) (this.fontRenderer.getStringWidth(str) * (!first ? 0.5f : 1.0f)); ++ GlStateManager.pushMatrix(); ++ GlStateManager.translate(parScaledResolution.getScaledWidth() - 2 - l, i + 2, 0.0f); ++ if (!first) { ++ GlStateManager.scale(0.5f, 0.5f, 0.5f); ++ } ++ this.fontRenderer.drawStringWithShadow(str, 0, 0, color); ++ GlStateManager.popMatrix(); ++ if (color == 0xFFFFFF) { ++ color = 14737632; ++ } + } -+ this.fontRenderer.drawStringWithShadow(str, 0, 0, color); -+ GlStateManager.popMatrix(); + i += (int) (this.fontRenderer.FONT_HEIGHT * (!first ? 0.5f : 1.0f)); + first = false; -+ if (color == 0xFFFFFF) { -+ color = 14737632; -+ } + } + } + } @@ -370,4 +375,8 @@ > DELETE 8 @ 8 : 12 +> CHANGE 25 : 26 @ 25 : 26 + +~ ScaledResolution scaledresolution = this.mc.scaledResolution; + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiPageButtonList.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiPageButtonList.edit.java index 3fe499ad..d5c1e1cc 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiPageButtonList.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiPageButtonList.edit.java @@ -10,9 +10,11 @@ + import java.util.List; + -> CHANGE 4 : 5 @ 4 : 5 +> CHANGE 4 : 7 @ 4 : 5 ~ +~ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; > DELETE 1 @ 1 : 9 @@ -46,4 +48,53 @@ ~ for (int k = 0; k < astring.length; ++k) { ~ ((GuiTextField) this.field_178072_w.get(j)).setText(astring[k]); +> INSERT 31 : 46 @ 31 + ++ public boolean isTextFieldFocused() { ++ for (GuiTextField txt : field_178072_w) { ++ if (txt.isFocused()) { ++ return true; ++ } ++ } ++ return false; ++ } ++ ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ for (GuiTextField txt : field_178072_w) { ++ txt.fireInputEvent(event, param); ++ } ++ } ++ + +> CHANGE 93 : 100 @ 93 : 95 + +~ if (k != 0 && k != 12345) +~ return false; +~ boolean touchMode = PointerInputAbstraction.isTouchMode(); +~ boolean flag = this.field_178029_b != null && (!touchMode || stupidCheck(this.field_178029_b, k)) +~ && this.func_178026_a(this.field_178029_b, i, j, k); +~ boolean flag1 = this.field_178030_c != null && (!touchMode || stupidCheck(this.field_178030_c, k)) +~ && this.func_178026_a(this.field_178030_c, i, j, k); + +> INSERT 3 : 11 @ 3 + ++ private static boolean stupidCheck(Gui gui, int k) { ++ if (gui instanceof GuiButton) { ++ return ((GuiButton) gui).isSliderTouchEvents() == (k == 12345); ++ } else { ++ return k != 12345; ++ } ++ } ++ + +> CHANGE 32 : 39 @ 32 : 34 + +~ if (k != 0 && k != 12345) +~ return; +~ boolean touchMode = PointerInputAbstraction.isTouchMode(); +~ if (!touchMode || stupidCheck(field_178029_b, k)) +~ this.func_178016_b(this.field_178029_b, i, j, k); +~ if (!touchMode || stupidCheck(field_178030_c, k)) +~ this.func_178016_b(this.field_178030_c, i, j, k); + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiPlayerTabOverlay.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiPlayerTabOverlay.edit.java index 6d261807..6b048d50 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiPlayerTabOverlay.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiPlayerTabOverlay.edit.java @@ -20,7 +20,16 @@ > DELETE 2 @ 2 : 3 -> CHANGE 46 : 48 @ 46 : 47 +> CHANGE 27 : 29 @ 27 : 28 + +~ IChatComponent dname = networkPlayerInfoIn.getDisplayNameProfanityFilter(); +~ return dname != null ? dname.getFormattedText() + +> CHANGE 1 : 2 @ 1 : 2 + +~ networkPlayerInfoIn.getGameProfileNameProfanityFilter()); + +> CHANGE 16 : 18 @ 16 : 17 ~ for (int m = 0, n = list.size(); m < n; ++m) { ~ NetworkPlayerInfo networkplayerinfo = (NetworkPlayerInfo) list.get(m); diff --git a/patches/minecraft/net/minecraft/client/gui/GuiRenameWorld.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiRenameWorld.edit.java index b0c9f4ff..3c333a72 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiRenameWorld.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiRenameWorld.edit.java @@ -5,9 +5,10 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 5 @ 2 : 6 +> CHANGE 2 : 6 @ 2 : 6 ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; ~ import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; ~ import net.lax1dude.eaglercraft.v1_8.sp.gui.GuiScreenIntegratedServerBusy; @@ -74,4 +75,18 @@ ~ this.drawCenteredString(this.fontRendererObj, ~ I18n.format(duplicate ? "selectWorld.duplicate" : "selectWorld.renameTitle", new Object[0]), +> INSERT 6 : 17 @ 6 + ++ ++ @Override ++ public boolean showCopyPasteButtons() { ++ return field_146583_f.isFocused(); ++ } ++ ++ @Override ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ field_146583_f.fireInputEvent(event, param); ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiRepair.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiRepair.edit.java index e57581c0..6550f086 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiRepair.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiRepair.edit.java @@ -7,11 +7,12 @@ > DELETE 2 @ 2 : 4 -> INSERT 1 : 5 @ 1 +> INSERT 1 : 6 @ 1 + + import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; + import net.lax1dude.eaglercraft.v1_8.Keyboard; ++ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; > DELETE 1 @ 1 : 2 @@ -28,11 +29,22 @@ ~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { -> INSERT 46 : 50 @ 46 +> INSERT 46 : 61 @ 46 + + public boolean blockPTTKey() { + return nameField.isFocused(); + } ++ ++ @Override ++ public boolean showCopyPasteButtons() { ++ return nameField.isFocused(); ++ } ++ ++ @Override ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ nameField.fireInputEvent(event, param); ++ } ++ > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiScreen.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiScreen.edit.java index 2b51878c..511cc426 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiScreen.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiScreen.edit.java @@ -9,9 +9,18 @@ > DELETE 1 @ 1 : 3 -> INSERT 4 : 20 @ 4 +> INSERT 2 : 3 @ 2 + ++ import java.util.HashMap; + +> INSERT 1 : 2 @ 1 + ++ import java.util.Map; + +> INSERT 1 : 24 @ 1 + ++ import net.lax1dude.eaglercraft.v1_8.internal.EnumTouchEvent; + import org.apache.commons.lang3.StringUtils; + + import com.google.common.base.Splitter; @@ -22,11 +31,17 @@ + import net.lax1dude.eaglercraft.v1_8.EaglerXBungeeVersion; + import net.lax1dude.eaglercraft.v1_8.Keyboard; + import net.lax1dude.eaglercraft.v1_8.Mouse; ++ import net.lax1dude.eaglercraft.v1_8.PauseMenuCustomizeState; ++ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; ++ import net.lax1dude.eaglercraft.v1_8.Touch; + import net.lax1dude.eaglercraft.v1_8.internal.KeyboardConstants; + import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; + import net.lax1dude.eaglercraft.v1_8.log4j.Logger; ++ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; + import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; ++ import net.lax1dude.eaglercraft.v1_8.touch_gui.TouchControls; ++ import net.lax1dude.eaglercraft.v1_8.webview.GuiScreenServerInfo; > CHANGE 1 : 2 @ 1 : 9 @@ -38,14 +53,27 @@ + import net.minecraft.client.resources.I18n; -> DELETE 13 @ 13 : 19 +> CHANGE 13 : 14 @ 13 : 19 -> CHANGE 17 : 19 @ 17 : 18 +~ import net.minecraft.util.ResourceLocation; + +> CHANGE 13 : 14 @ 13 : 14 + +~ protected GuiButton selectedButton; + +> CHANGE 3 : 5 @ 3 : 4 ~ private String clickedLinkURI; ~ protected long showingCloseKey = 0; -> CHANGE 2 : 3 @ 2 : 3 +> INSERT 1 : 5 @ 1 + ++ protected int touchModeCursorPosX = -1; ++ protected int touchModeCursorPosY = -1; ++ private long lastTouchEvent; ++ + +> CHANGE 1 : 2 @ 1 : 2 ~ for (int k = 0, l = this.buttonList.size(); k < l; ++k) { @@ -55,7 +83,7 @@ > INSERT 3 : 33 @ 3 -+ long millis = System.currentTimeMillis(); ++ long millis = EagRuntime.steadyTimeMillis(); + long closeKeyTimeout = millis - showingCloseKey; + if (closeKeyTimeout < 3000l) { + int alpha1 = 0xC0000000; @@ -86,7 +114,7 @@ + } + -> CHANGE 2 : 14 @ 2 : 4 +> CHANGE 2 : 16 @ 2 : 4 ~ protected int getCloseKey() { ~ if (this instanceof GuiContainer) { @@ -97,6 +125,8 @@ ~ } ~ ~ protected void keyTyped(char parChar1, int parInt1) { +~ if (!canCloseGui()) +~ return; ~ if (((this.mc.theWorld == null || this.mc.thePlayer.getHealth() <= 0.0F) && parInt1 == 1) ~ || parInt1 == this.mc.gameSettings.keyBindClose.getKeyCode() ~ || (parInt1 == 1 && (this.mc.gameSettings.keyBindClose.getKeyCode() == 0 || this.mc.areKeysLocked()))) { @@ -104,7 +134,7 @@ > INSERT 4 : 6 @ 4 + } else if (parInt1 == 1) { -+ showingCloseKey = System.currentTimeMillis(); ++ showingCloseKey = EagRuntime.steadyTimeMillis(); > DELETE 1 @ 1 : 2 @@ -116,22 +146,72 @@ ~ EagRuntime.setClipboard(copyText); -> CHANGE 6 : 7 @ 6 : 7 +> CHANGE 4 : 6 @ 4 : 5 +~ renderToolTip0(itemstack, i, j, false); +~ } + +> CHANGE 1 : 5 @ 1 : 2 + +~ protected void renderToolTip0(ItemStack itemstack, int i, int j, boolean eagler) { +~ List list = itemstack.getTooltipProfanityFilter(this.mc.thePlayer, this.mc.gameSettings.advancedItemTooltips); +~ ~ for (int k = 0, l = list.size(); k < l; ++k) { -> CHANGE 22 : 24 @ 22 : 24 +> CHANGE 7 : 8 @ 7 : 8 + +~ this.drawHoveringText0(list, i, j, eagler); + +> INSERT 7 : 11 @ 7 + ++ drawHoveringText0(list, i, j, false); ++ } ++ ++ protected void drawHoveringText0(List list, int i, int j, boolean eagler) { + +> CHANGE 7 : 9 @ 7 : 9 ~ for (int m = 0, n = list.size(); m < n; ++m) { ~ int l = this.fontRendererObj.getStringWidth(list.get(m)); -> CHANGE 37 : 40 @ 37 : 38 +> CHANGE 5 : 7 @ 5 : 7 + +~ int j2 = i; +~ int k2 = j; + +> CHANGE 5 : 8 @ 5 : 8 + +~ if (!eagler) { +~ j2 += 12; +~ k2 -= 12; + +> CHANGE 1 : 10 @ 1 : 3 + +~ if (j2 + k > this.width) { +~ j2 -= 28 + k; +~ } +~ +~ if (k2 + i1 + 6 > this.height) { +~ k2 = this.height - i1 - 6; +~ } +~ } else { +~ j2 -= (k + 3) >> 1; + +> CHANGE 19 : 22 @ 19 : 20 ~ if (s1.length() > 0) { ~ this.fontRendererObj.drawStringWithShadow(s1, (float) j2, (float) k2, -1); ~ } -> INSERT 107 : 108 @ 107 +> CHANGE 16 : 17 @ 16 : 17 + +~ public void handleComponentHover(IChatComponent parIChatComponent, int parInt1, int parInt2) { + +> CHANGE 76 : 77 @ 76 : 77 + +~ public boolean handleComponentClick(IChatComponent parIChatComponent) { + +> INSERT 13 : 14 @ 13 + String uri = clickevent.getValue(); @@ -165,20 +245,229 @@ ~ LOGGER.error("Invalid plugin download from EPK was blocked: {}", ~ EaglerXBungeeVersion.pluginFileEPK); -> CHANGE 24 : 25 @ 24 : 25 +> CHANGE 24 : 49 @ 24 : 26 +~ protected void touchStarted(int parInt1, int parInt2, int parInt3) { +~ if (shouldTouchGenerateMouseEvents()) { +~ this.mouseClicked(parInt1, parInt2, 12345); +~ } +~ } +~ +~ protected void touchTapped(int parInt1, int parInt2, int parInt3) { +~ if (shouldTouchGenerateMouseEvents()) { +~ this.mouseClicked(parInt1, parInt2, 0); +~ this.mouseReleased(parInt1, parInt2, 0); +~ } +~ } +~ +~ protected void touchMoved(int parInt1, int parInt2, int parInt3) { +~ } +~ +~ protected void touchEndMove(int parInt1, int parInt2, int parInt3) { +~ if (shouldTouchGenerateMouseEvents()) { +~ this.mouseReleased(parInt1, parInt2, 12345); +~ } +~ } +~ ~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { +~ boolean touchMode = PointerInputAbstraction.isTouchMode(); +~ if (parInt3 == 0 || parInt3 == 12345) { -> CHANGE 24 : 25 @ 24 : 25 +> INSERT 2 : 4 @ 2 + ++ if (touchMode && (parInt3 == 12345) != guibutton.isSliderTouchEvents()) ++ continue; + +> CHANGE 11 : 13 @ 11 : 12 + +~ if (this.selectedButton != null && (k == 0 || k == 12345) +~ && (!PointerInputAbstraction.isTouchMode() || (k == 12345) == selectedButton.isSliderTouchEvents())) { + +> CHANGE 9 : 10 @ 9 : 10 ~ protected void actionPerformed(GuiButton parGuiButton) { -> CHANGE 119 : 121 @ 119 : 128 +> INSERT 16 : 23 @ 16 + ++ boolean noTouch = true; ++ while (Touch.next()) { ++ noTouch = false; ++ this.handleTouchInput(); ++ TouchControls.handleInput(); ++ } ++ + +> CHANGE 2 : 5 @ 2 : 3 + +~ if (noTouch) { +~ this.handleMouseInput(); +~ } + +> INSERT 11 : 95 @ 11 + ++ public final Map touchStarts = new HashMap<>(); ++ ++ /** ++ * Handles touch input. ++ */ ++ public void handleTouchInput() throws IOException { ++ EnumTouchEvent et = Touch.getEventType(); ++ if (et == EnumTouchEvent.TOUCHSTART) { ++ PointerInputAbstraction.enterTouchModeHook(); ++ } ++ float scaleFac = getEaglerScale(); ++ for (int t = 0, c = Touch.getEventTouchPointCount(); t < c; ++t) { ++ int u = Touch.getEventTouchPointUID(t); ++ int i = Touch.getEventTouchX(t); ++ int j = Touch.getEventTouchY(t); ++ if (et == EnumTouchEvent.TOUCHSTART) { ++ if (TouchControls.handleTouchBegin(u, i, j)) { ++ continue; ++ } ++ } else if (et == EnumTouchEvent.TOUCHEND) { ++ if (TouchControls.handleTouchEnd(u, i, j)) { ++ continue; ++ } ++ } ++ i = applyEaglerScale(scaleFac, i * this.width / this.mc.displayWidth, this.width); ++ j = applyEaglerScale(scaleFac, this.height - j * this.height / this.mc.displayHeight - 1, this.height); ++ float si = Touch.getEventTouchRadiusX(t) * this.width / this.mc.displayWidth / scaleFac; ++ if (si < 1.0f) ++ si = 1.0f; ++ float sj = Touch.getEventTouchRadiusY(t) * this.height / this.mc.displayHeight / scaleFac; ++ if (sj < 1.0f) ++ sj = 1.0f; ++ int[] ck = touchStarts.remove(u); ++ switch (et) { ++ case TOUCHSTART: ++ if (t == 0) { ++ touchModeCursorPosX = i; ++ touchModeCursorPosY = j; ++ } ++ lastTouchEvent = EagRuntime.steadyTimeMillis(); ++ touchStarts.put(u, new int[] { i, j, 0 }); ++ this.touchStarted(i, j, u); ++ break; ++ case TOUCHMOVE: ++ if (t == 0) { ++ touchModeCursorPosX = i; ++ touchModeCursorPosY = j; ++ } ++ if (ck != null && Math.abs(ck[0] - i) < si && Math.abs(ck[1] - j) < sj) { ++ touchStarts.put(u, ck); ++ break; ++ } ++ touchStarts.put(u, new int[] { i, j, (ck != null && isTouchDraggingStateLocked(u)) ? ck[2] : 1 }); ++ this.touchMoved(i, j, u); ++ if (t == 0 && shouldTouchGenerateMouseEvents()) { ++ this.mouseClickMove(i, j, 0, EagRuntime.steadyTimeMillis() - lastTouchEvent); ++ } ++ break; ++ case TOUCHEND: ++ if (ck == null) ++ break; ++ if (t == 0) { ++ touchModeCursorPosX = -1; ++ touchModeCursorPosY = -1; ++ } ++ if (ck != null && ck[2] == 1) { ++ this.touchEndMove(i, j, u); ++ } else { ++ if (ck != null) { ++ i = ck[0]; ++ j = ck[1]; ++ } ++ this.touchTapped(i, j, u); ++ } ++ break; ++ } ++ } ++ } ++ ++ public boolean isTouchPointDragging(int uid) { ++ int[] ret = touchStarts.get(uid); ++ return ret != null && ret[2] == 1; ++ } ++ + +> CHANGE 1 : 5 @ 1 : 3 + +~ float f = getEaglerScale(); +~ int i = applyEaglerScale(f, Mouse.getEventX() * this.width / this.mc.displayWidth, this.width); +~ int j = applyEaglerScale(f, this.height - Mouse.getEventY() * this.height / this.mc.displayHeight - 1, +~ this.height); + +> INSERT 2 : 3 @ 2 + ++ PointerInputAbstraction.enterMouseModeHook(); + +> INSERT 39 : 43 @ 39 + ++ protected boolean isPartOfPauseMenu() { ++ return false; ++ } ++ + +> CHANGE 2 : 53 @ 2 : 3 + +~ boolean ingame = isPartOfPauseMenu(); +~ ResourceLocation loc = (ingame && PauseMenuCustomizeState.icon_background_pause != null) +~ ? PauseMenuCustomizeState.icon_background_pause +~ : PauseMenuCustomizeState.icon_background_all; +~ float aspect = (ingame && PauseMenuCustomizeState.icon_background_pause != null) +~ ? 1.0f / PauseMenuCustomizeState.icon_background_pause_aspect +~ : 1.0f / PauseMenuCustomizeState.icon_background_all_aspect; +~ if (loc != null) { +~ GlStateManager.disableLighting(); +~ GlStateManager.disableFog(); +~ GlStateManager.enableBlend(); +~ GlStateManager.disableAlpha(); +~ GlStateManager.enableTexture2D(); +~ GlStateManager.tryBlendFuncSeparate(770, 771, 1, 0); +~ Tessellator tessellator = Tessellator.getInstance(); +~ WorldRenderer worldrenderer = tessellator.getWorldRenderer(); +~ this.mc.getTextureManager().bindTexture(loc); +~ GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); +~ float f = 64.0F; +~ worldrenderer.begin(7, DefaultVertexFormats.POSITION_TEX_COLOR); +~ worldrenderer.pos(0.0D, (double) this.height, 0.0D).tex(0.0D, (double) ((float) this.height / f)) +~ .color(64, 64, 64, 192).endVertex(); +~ worldrenderer.pos((double) this.width, (double) this.height, 0.0D) +~ .tex((double) ((float) this.width / f * aspect), (double) ((float) this.height / f)) +~ .color(64, 64, 64, 192).endVertex(); +~ worldrenderer.pos((double) this.width, 0.0D, 0.0D) +~ .tex((double) ((float) this.width / f * aspect), (double) 0).color(64, 64, 64, 192).endVertex(); +~ worldrenderer.pos(0.0D, 0.0D, 0.0D).tex(0.0D, (double) 0).color(64, 64, 64, 192).endVertex(); +~ tessellator.draw(); +~ GlStateManager.enableAlpha(); +~ } else { +~ this.drawGradientRect(0, 0, this.width, this.height, -1072689136, -804253680); +~ } +~ if (!(this instanceof GuiScreenServerInfo)) { +~ loc = (ingame && PauseMenuCustomizeState.icon_watermark_pause != null) +~ ? PauseMenuCustomizeState.icon_watermark_pause +~ : PauseMenuCustomizeState.icon_watermark_all; +~ aspect = (ingame && PauseMenuCustomizeState.icon_watermark_pause != null) +~ ? PauseMenuCustomizeState.icon_watermark_pause_aspect +~ : PauseMenuCustomizeState.icon_watermark_all_aspect; +~ if (loc != null) { +~ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); +~ mc.getTextureManager().bindTexture(loc); +~ GlStateManager.pushMatrix(); +~ GlStateManager.translate(8, height - 72, 0.0f); +~ float f2 = 64.0f / 256.0f; +~ GlStateManager.scale(f2 * aspect, f2, f2); +~ this.drawTexturedModalRect(0, 0, 0, 0, 256, 256); +~ GlStateManager.popMatrix(); +~ } +~ } + +> CHANGE 42 : 44 @ 42 : 51 ~ private void openWebLink(String parURI) { ~ EagRuntime.openLink(parURI); -> INSERT 34 : 42 @ 34 +> INSERT 34 : 75 @ 34 + + public boolean shouldHangupIntegratedServer() { @@ -188,5 +477,38 @@ + public boolean blockPTTKey() { + return false; + } ++ ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ ++ } ++ ++ public boolean showCopyPasteButtons() { ++ return false; ++ } ++ ++ public static int applyEaglerScale(float scaleFac, int coord, int screenDim) { ++ return (int) ((coord - (1.0f - scaleFac) * screenDim * 0.5f) / scaleFac); ++ } ++ ++ public float getEaglerScale() { ++ return PointerInputAbstraction.isTouchMode() ? getTouchModeScale() : 1.0f; ++ } ++ ++ protected float getTouchModeScale() { ++ return 1.0f; ++ } ++ ++ public boolean canCloseGui() { ++ return true; ++ } ++ ++ protected boolean isTouchDraggingStateLocked(int uid) { ++ return false; ++ } ++ ++ protected boolean shouldTouchGenerateMouseEvents() { ++ return true; ++ } ++ > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiScreenAddServer.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiScreenAddServer.edit.java index 2b3c03f4..f0c9b4bc 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiScreenAddServer.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiScreenAddServer.edit.java @@ -5,16 +5,18 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 4 @ 2 : 8 +> CHANGE 2 : 5 @ 2 : 8 ~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; > DELETE 2 @ 2 : 3 -> CHANGE 7 : 8 @ 7 : 26 +> CHANGE 7 : 9 @ 7 : 26 ~ private GuiButton hideAddress; +~ private GuiButton enableCookies; > INSERT 13 : 14 @ 13 @@ -40,11 +42,21 @@ ~ } ~ this.buttonList.add(this.serverResourcePacks = new GuiButton(2, this.width / 2 - 100, i + 54, -> INSERT 2 : 5 @ 2 +> INSERT 2 : 15 @ 2 -+ this.buttonList.add(this.hideAddress = new GuiButton(3, this.width / 2 - 100, i + 78, -+ I18n.format("addServer.hideAddress", new Object[0]) + ": " -+ + I18n.format(this.serverData.hideAddress ? "gui.yes" : "gui.no", new Object[0]))); ++ if (EagRuntime.getConfiguration().isEnableServerCookies()) { ++ this.buttonList.add(this.enableCookies = new GuiButton(4, this.width / 2 - 100, i + 78, 99, 20, ++ I18n.format("addServer.enableCookies") + ": " ++ + I18n.format(this.serverData.enableCookies ? "addServer.enableCookies.enabled" ++ : "addServer.enableCookies.disabled"))); ++ this.buttonList.add(this.hideAddress = new GuiButton(3, this.width / 2 + 1, i + 78, 99, 20, ++ I18n.format("addServer.hideAddr", new Object[0]) + ": " ++ + I18n.format(this.serverData.hideAddress ? "gui.yes" : "gui.no", new Object[0]))); ++ } else { ++ this.buttonList.add(this.hideAddress = new GuiButton(3, this.width / 2 - 100, i + 78, ++ I18n.format("addServer.hideAddress", new Object[0]) + ": " ++ + I18n.format(this.serverData.hideAddress ? "gui.yes" : "gui.no", new Object[0]))); ++ } > CHANGE 6 : 7 @ 6 : 9 @@ -54,12 +66,19 @@ ~ protected void actionPerformed(GuiButton parGuiButton) { -> CHANGE 1 : 6 @ 1 : 2 +> CHANGE 1 : 13 @ 1 : 2 ~ if (parGuiButton.id == 3) { ~ this.serverData.hideAddress = !this.serverData.hideAddress; -~ this.hideAddress.displayString = I18n.format("addServer.hideAddress", new Object[0]) + ": " -~ + I18n.format(this.serverData.hideAddress ? "gui.yes" : "gui.no", new Object[0]); +~ this.hideAddress.displayString = I18n +~ .format(EagRuntime.getConfiguration().isEnableServerCookies() ? "addServer.hideAddr" +~ : "addServer.hideAddress", new Object[0]) +~ + ": " + I18n.format(this.serverData.hideAddress ? "gui.yes" : "gui.no", new Object[0]); +~ } else if (parGuiButton.id == 4) { +~ this.serverData.enableCookies = !this.serverData.enableCookies; +~ this.enableCookies.displayString = I18n.format("addServer.enableCookies") + ": " +~ + I18n.format(this.serverData.enableCookies ? "addServer.enableCookies.enabled" +~ : "addServer.enableCookies.disabled"); ~ } else if (parGuiButton.id == 2) { > CHANGE 1 : 3 @ 1 : 3 @@ -93,4 +112,19 @@ + 0xccccff); + } +> INSERT 4 : 16 @ 4 + ++ ++ @Override ++ public boolean showCopyPasteButtons() { ++ return serverNameField.isFocused() || serverIPField.isFocused(); ++ } ++ ++ @Override ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ serverNameField.fireInputEvent(event, param); ++ serverIPField.fireInputEvent(event, param); ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiScreenBook.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiScreenBook.edit.java index 9fde9055..1c1b9def 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiScreenBook.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiScreenBook.edit.java @@ -7,7 +7,7 @@ > DELETE 2 @ 2 : 6 -> INSERT 1 : 11 @ 1 +> INSERT 1 : 12 @ 1 + + import org.json.JSONException; @@ -18,13 +18,23 @@ + import net.lax1dude.eaglercraft.v1_8.Keyboard; + import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; + import net.lax1dude.eaglercraft.v1_8.log4j.Logger; ++ import net.lax1dude.eaglercraft.v1_8.minecraft.GuiScreenVisualViewport; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; > DELETE 1 @ 1 : 5 > DELETE 16 @ 16 : 19 -> CHANGE 139 : 140 @ 139 : 140 +> CHANGE 1 : 2 @ 1 : 2 + +~ public class GuiScreenBook extends GuiScreenVisualViewport { + +> CHANGE 47 : 49 @ 47 : 49 + +~ public void updateScreen0() { +~ super.updateScreen0(); + +> CHANGE 88 : 89 @ 88 : 89 ~ protected void actionPerformed(GuiButton parGuiButton) { @@ -34,15 +44,31 @@ > DELETE 6 @ 6 : 7 -> CHANGE 129 : 130 @ 129 : 130 +> CHANGE 78 : 79 @ 78 : 79 + +~ public void drawScreen0(int i, int j, float f) { + +> CHANGE 50 : 51 @ 50 : 51 ~ } catch (JSONException var13) { -> CHANGE 34 : 35 @ 34 : 35 +> CHANGE 31 : 32 @ 31 : 32 -~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { +~ super.drawScreen0(i, j, f); -> INSERT 102 : 106 @ 102 +> CHANGE 2 : 3 @ 2 : 3 + +~ protected void mouseClicked0(int parInt1, int parInt2, int parInt3) { + +> CHANGE 7 : 8 @ 7 : 8 + +~ super.mouseClicked0(parInt1, parInt2, parInt3); + +> CHANGE 2 : 3 @ 2 : 3 + +~ public boolean handleComponentClick(IChatComponent ichatcomponent) { + +> INSERT 91 : 95 @ 91 + + public boolean blockPTTKey() { diff --git a/patches/minecraft/net/minecraft/client/gui/GuiScreenCustomizePresets.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiScreenCustomizePresets.edit.java index f65476ca..74c12e47 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiScreenCustomizePresets.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiScreenCustomizePresets.edit.java @@ -5,10 +5,11 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 5 : 9 @ 5 : 11 +> CHANGE 5 : 10 @ 5 : 11 ~ ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; ~ import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; @@ -16,7 +17,15 @@ > DELETE 4 @ 4 : 5 -> CHANGE 41 : 42 @ 41 : 42 +> INSERT 37 : 42 @ 37 + ++ public void handleTouchInput() throws IOException { ++ super.handleTouchInput(); ++ this.field_175311_g.handleTouchInput(); ++ } ++ + +> CHANGE 4 : 5 @ 4 : 5 ~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { @@ -28,4 +37,17 @@ ~ protected void actionPerformed(GuiButton parGuiButton) { +> INSERT 35 : 45 @ 35 + ++ @Override ++ public boolean showCopyPasteButtons() { ++ return field_175317_i.isFocused(); ++ } ++ ++ @Override ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ field_175317_i.fireInputEvent(event, param); ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiScreenOptionsSounds.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiScreenOptionsSounds.edit.java index a934d7c2..5f6f77ca 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiScreenOptionsSounds.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiScreenOptionsSounds.edit.java @@ -21,4 +21,11 @@ ~ protected void actionPerformed(GuiButton parGuiButton) { +> INSERT 90 : 94 @ 90 + ++ ++ public boolean isSliderTouchEvents() { ++ return true; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiScreenResourcePacks.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiScreenResourcePacks.edit.java index 37a6e9b6..2c632717 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiScreenResourcePacks.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiScreenResourcePacks.edit.java @@ -40,7 +40,16 @@ ~ this.selectedResourcePacks ~ .add(new ResourcePackListEntryFound(this, (ResourcePackRepository.Entry) arraylist.get(i))); -> CHANGE 38 : 39 @ 38 : 39 +> INSERT 21 : 27 @ 21 + ++ public void handleTouchInput() throws IOException { ++ super.handleTouchInput(); ++ this.selectedResourcePacksList.handleTouchInput(); ++ this.availableResourcePacksList.handleTouchInput(); ++ } ++ + +> CHANGE 17 : 18 @ 17 : 18 ~ protected void actionPerformed(GuiButton parGuiButton) { diff --git a/patches/minecraft/net/minecraft/client/gui/GuiScreenServerList.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiScreenServerList.edit.java index fb590b21..824578d7 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiScreenServerList.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiScreenServerList.edit.java @@ -5,10 +5,11 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 4 @ 2 : 6 +> CHANGE 2 : 5 @ 2 : 6 ~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; > DELETE 2 @ 2 : 3 @@ -59,4 +60,18 @@ ~ 100, 10526880); ~ } +> INSERT 3 : 14 @ 3 + ++ ++ @Override ++ public boolean showCopyPasteButtons() { ++ return field_146302_g.isFocused(); ++ } ++ ++ @Override ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ field_146302_g.fireInputEvent(event, param); ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiSelectWorld.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiSelectWorld.edit.java index 039a01c9..4969828c 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiSelectWorld.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiSelectWorld.edit.java @@ -9,12 +9,13 @@ + import java.util.ArrayList; -> CHANGE 2 : 7 @ 2 : 3 +> CHANGE 2 : 8 @ 2 : 3 ~ ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; ~ import net.lax1dude.eaglercraft.v1_8.sp.gui.GuiScreenLANConnect; ~ import net.lax1dude.eaglercraft.v1_8.sp.gui.GuiScreenLANNotSupported; +~ import net.lax1dude.eaglercraft.v1_8.sp.ipc.IPCPacket1CIssueDetected; ~ import net.lax1dude.eaglercraft.v1_8.sp.lan.LANServerController; > CHANGE 1 : 2 @ 1 : 9 @@ -44,18 +45,27 @@ + import net.lax1dude.eaglercraft.v1_8.sp.gui.GuiScreenLANInfo; + -> INSERT 17 : 19 @ 17 +> INSERT 17 : 20 @ 17 + private boolean hasRequestedWorlds = false; + private boolean waitingForWorlds = false; ++ private boolean ramdiskMode = false; > INSERT 3 : 4 @ 3 + this.field_146639_s = new ArrayList(); -> DELETE 4 @ 4 : 13 +> INSERT 3 : 4 @ 3 -> INSERT 13 : 30 @ 13 ++ this.ramdiskMode = SingleplayerServerController.isIssueDetected(IPCPacket1CIssueDetected.ISSUE_RAMDISK_MODE); + +> DELETE 1 @ 1 : 10 + +> CHANGE 8 : 9 @ 8 : 9 + +~ this.field_146638_t = new GuiSelectWorld.List(this.mc, ramdiskMode ? -10 : 0); + +> INSERT 4 : 21 @ 4 + public void updateScreen() { + if (!hasRequestedWorlds && SingleplayerServerController.isReady()) { @@ -75,8 +85,13 @@ + } + -> CHANGE 5 : 6 @ 5 : 6 +> CHANGE 5 : 11 @ 5 : 6 +~ public void handleTouchInput() throws IOException { +~ super.handleTouchInput(); +~ this.field_146638_t.handleTouchInput(); +~ } +~ ~ private void func_146627_h() { > CHANGE 29 : 30 @ 29 : 30 @@ -118,8 +133,13 @@ > DELETE 1 @ 1 : 3 -> INSERT 7 : 22 @ 7 +> INSERT 7 : 27 @ 7 ++ ++ if (ramdiskMode) { ++ this.drawCenteredString(this.fontRendererObj, I18n.format("selectWorld.ramdiskWarning"), this.width / 2, ++ height - 68, 11184810); ++ } + + GlStateManager.pushMatrix(); + GlStateManager.scale(0.75f, 0.75f, 0.75f); @@ -155,4 +175,10 @@ + } + +> CHANGE 10 : 13 @ 10 : 12 + +~ public List(Minecraft mcIn, int i) { +~ super(mcIn, GuiSelectWorld.this.width, GuiSelectWorld.this.height, 32, GuiSelectWorld.this.height - 64 + i, +~ 36); + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiSlider.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiSlider.edit.java index b28e0d58..10d2bba7 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiSlider.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiSlider.edit.java @@ -11,4 +11,11 @@ > DELETE 1 @ 1 : 4 +> INSERT 106 : 110 @ 106 + ++ ++ public boolean isSliderTouchEvents() { ++ return true; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiSlot.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiSlot.edit.java index 244974fe..d689e821 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiSlot.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiSlot.edit.java @@ -5,10 +5,13 @@ # Version: 1.0 # Author: lax1dude -> INSERT 2 : 8 @ 2 +> INSERT 2 : 11 @ 2 + import net.lax1dude.eaglercraft.v1_8.Mouse; ++ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; ++ import net.lax1dude.eaglercraft.v1_8.Touch; + import net.lax1dude.eaglercraft.v1_8.internal.EnumCursorType; ++ import net.lax1dude.eaglercraft.v1_8.internal.EnumTouchEvent; + import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; + import net.lax1dude.eaglercraft.v1_8.log4j.Logger; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; @@ -34,17 +37,44 @@ ~ this.drawSelectionBox(k, l, mouseXIn, mouseYIn, this.getSize()); -> CHANGE 168 : 169 @ 168 : 170 +> INSERT 74 : 84 @ 74 + ++ handleInput(Mouse.getEventButton(), Mouse.getEventButtonState(), Mouse.getDWheel()); ++ } ++ ++ public void handleTouchInput() { ++ mouseX = PointerInputAbstraction.getVCursorX() * width / mc.displayWidth; ++ mouseY = height - PointerInputAbstraction.getVCursorY() * height / mc.displayHeight - 1; ++ handleInput(0, Touch.getEventType() == EnumTouchEvent.TOUCHSTART, 0); ++ } ++ ++ protected void handleInput(int eventButton, boolean eventState, int dWheel) { + +> CHANGE 1 : 2 @ 1 : 3 + +~ if (eventButton == 0 && eventState && this.mouseY >= this.top && this.mouseY <= this.bottom) { + +> CHANGE 12 : 13 @ 12 : 13 + +~ if (PointerInputAbstraction.getVCursorButtonDown(0) && this.getEnabled()) { + +> CHANGE 52 : 57 @ 52 : 58 + +~ if (dWheel != 0) { +~ if (dWheel > 0) { +~ dWheel = -1; +~ } else if (dWheel < 0) { +~ dWheel = 1; + +> CHANGE 2 : 3 @ 2 : 3 + +~ this.amountScrolled += (float) (dWheel * this.slotHeight / 2); + +> CHANGE 17 : 18 @ 17 : 19 ~ protected void drawSelectionBox(int mouseXIn, int mouseYIn, int parInt3, int parInt4, int i) { -> INSERT 3 : 6 @ 3 - -+ int mx = Mouse.getX(); -+ int my = Mouse.getY(); -+ - -> INSERT 31 : 34 @ 31 +> INSERT 34 : 37 @ 34 + if (parInt3 >= i1 && parInt3 <= j1 && parInt4 >= k - 2 && parInt4 <= k + l + 1) { + Mouse.showCursor(EnumCursorType.HAND); diff --git a/patches/minecraft/net/minecraft/client/gui/GuiTextField.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiTextField.edit.java index f378dee4..80740421 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiTextField.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiTextField.edit.java @@ -5,9 +5,10 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 4 : 7 @ 4 : 9 +> CHANGE 4 : 8 @ 4 : 9 ~ +~ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; ~ import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; @@ -44,4 +45,25 @@ ~ GlStateManager.disableBlend(); +> INSERT 104 : 122 @ 104 + ++ ++ public void fireInputEvent(EnumInputEvent clipboardPaste, String param) { ++ if (!isFocused) ++ return; ++ switch (clipboardPaste) { ++ case CLIPBOARD_COPY: ++ GuiScreen.setClipboardString(this.getSelectedText()); ++ break; ++ case CLIPBOARD_PASTE: ++ if (this.isEnabled) { ++ this.writeText(param != null ? param : GuiScreen.getClipboardString()); ++ } ++ break; ++ default: ++ break; ++ } ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiUtilRenderComponents.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiUtilRenderComponents.edit.java index 924a79c4..88eb0628 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiUtilRenderComponents.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiUtilRenderComponents.edit.java @@ -15,4 +15,11 @@ > DELETE 1 @ 1 : 2 +> INSERT 11 : 15 @ 11 + ++ /** ++ * This function is like the FontRenderer wrap function, except for chat ++ * components ++ */ + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/GuiVideoSettings.edit.java b/patches/minecraft/net/minecraft/client/gui/GuiVideoSettings.edit.java index 18744aa1..505bdaf2 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiVideoSettings.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/GuiVideoSettings.edit.java @@ -5,12 +5,17 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 3 : 4 @ 3 : 9 +> CHANGE 3 : 8 @ 3 : 9 ~ +~ import net.lax1dude.eaglercraft.v1_8.Display; +~ import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; +~ import net.lax1dude.eaglercraft.v1_8.opengl.ext.dynamiclights.DynamicLightsStateManager; +~ import net.lax1dude.eaglercraft.v1_8.recording.ScreenRecordingController; -> INSERT 8 : 11 @ 8 +> INSERT 8 : 12 @ 8 ++ private boolean vsyncLock = false; + /** + * + An array of all of GameSettings.Options's video options. + */ @@ -26,21 +31,77 @@ ~ GameSettings.Options.HUD_COORDS, GameSettings.Options.HUD_PLAYER, GameSettings.Options.HUD_STATS, ~ GameSettings.Options.HUD_WORLD, GameSettings.Options.HUD_24H, GameSettings.Options.CHUNK_FIX }; -> CHANGE 11 : 13 @ 11 : 31 +> CHANGE 11 : 18 @ 11 : 22 ~ this.optionsRowList = new GuiOptionsRowList(this.mc, this.width, this.height, 32, this.height - 32, 25, ~ videoOptions); +~ if (!DynamicLightsStateManager.isSupported()) { +~ GuiOptionButton btn = ((GuiOptionsRowList) optionsRowList) +~ .getButtonFor(GameSettings.Options.EAGLER_DYNAMIC_LIGHTS); +~ if (btn != null) { +~ btn.enabled = false; -> CHANGE 7 : 8 @ 7 : 8 +> DELETE 1 @ 1 : 7 +> CHANGE 1 : 17 @ 1 : 2 + +~ if (EaglercraftGPU.checkOpenGLESVersion() < 300) { +~ GuiOptionSlider btn = ((GuiOptionsRowList) optionsRowList).getSliderFor(GameSettings.Options.MIPMAP_LEVELS); +~ if (btn != null) { +~ btn.displayString = I18n.format(GameSettings.Options.MIPMAP_LEVELS.getEnumString()) + ": N/A"; +~ btn.sliderValue = 0.0f; +~ btn.enabled = false; +~ } +~ } +~ if (!Display.supportsFullscreen()) { +~ GuiOptionButton btn = ((GuiOptionsRowList) optionsRowList).getButtonFor(GameSettings.Options.FULLSCREEN); +~ if (btn != null) { +~ btn.displayString = I18n.format(GameSettings.Options.FULLSCREEN.getEnumString()) + ": " +~ + I18n.format("options.off"); +~ btn.enabled = false; +~ } +~ } + +> CHANGE 7 : 13 @ 7 : 8 + +~ public void handleTouchInput() throws IOException { +~ super.handleTouchInput(); +~ this.optionsRowList.handleTouchInput(); +~ } +~ ~ protected void actionPerformed(GuiButton parGuiButton) { > CHANGE 9 : 10 @ 9 : 10 ~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { -> INSERT 8 : 9 @ 8 +> CHANGE 4 : 5 @ 4 : 5 + +~ ScaledResolution scaledresolution = mc.scaledResolution = new ScaledResolution(mc); + +> INSERT 3 : 5 @ 3 + this.mc.voiceOverlay.setResolution(j, k); ++ this.mc.notifRenderer.setResolution(this.mc, j, k, scaledresolution.getScaleFactor()); + +> CHANGE 9 : 10 @ 9 : 10 + +~ ScaledResolution scaledresolution = mc.scaledResolution = new ScaledResolution(mc); + +> INSERT 13 : 26 @ 13 + ++ ++ @Override ++ public void updateScreen() { ++ boolean vsyncLockEn = ScreenRecordingController.isVSyncLocked(); ++ if (vsyncLockEn != vsyncLock) { ++ vsyncLock = vsyncLockEn; ++ GuiOptionButton btn = ((GuiOptionsRowList) optionsRowList).getButtonFor(GameSettings.Options.EAGLER_VSYNC); ++ if (btn != null) { ++ btn.enabled = !vsyncLockEn; ++ } ++ } ++ } ++ > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/ScaledResolution.edit.java b/patches/minecraft/net/minecraft/client/gui/ScaledResolution.edit.java new file mode 100644 index 00000000..2d8714a4 --- /dev/null +++ b/patches/minecraft/net/minecraft/client/gui/ScaledResolution.edit.java @@ -0,0 +1,20 @@ + +# Eagler Context Redacted Diff +# Copyright (c) 2024 lax1dude. All rights reserved. + +# Version: 1.0 +# Author: lax1dude + +> INSERT 12 : 16 @ 12 + ++ /** ++ * EAGLER NOTE: This constructor is deprecated! Use ++ * Minecraft.getMinecraft().scaledResolution ++ */ + +> INSERT 10 : 12 @ 10 + ++ i = Math.round(i * Math.max(parMinecraft.displayDPI, 0.5f)); ++ + +> EOF diff --git a/patches/minecraft/net/minecraft/client/gui/ScreenChatOptions.edit.java b/patches/minecraft/net/minecraft/client/gui/ScreenChatOptions.edit.java index 7a83650f..892d37c0 100644 --- a/patches/minecraft/net/minecraft/client/gui/ScreenChatOptions.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/ScreenChatOptions.edit.java @@ -5,14 +5,32 @@ # Version: 1.0 # Author: lax1dude -> DELETE 2 @ 2 : 7 +> CHANGE 2 : 3 @ 2 : 7 -> CHANGE 22 : 24 @ 22 : 23 +~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; -~ for (int j = 0; j < field_146399_a.length; ++j) { -~ GameSettings.Options gamesettings$options = field_146399_a[j]; +> INSERT 8 : 14 @ 8 -> CHANGE 16 : 17 @ 16 : 17 ++ GameSettings.Options.CHAT_WIDTH, GameSettings.Options.REDUCED_DEBUG_INFO, ++ GameSettings.Options.EAGLER_PROFANITY_FILTER }; ++ private static final GameSettings.Options[] no_profanity_filter = new GameSettings.Options[] { ++ GameSettings.Options.CHAT_VISIBILITY, GameSettings.Options.CHAT_COLOR, GameSettings.Options.CHAT_LINKS, ++ GameSettings.Options.CHAT_OPACITY, GameSettings.Options.CHAT_LINKS_PROMPT, GameSettings.Options.CHAT_SCALE, ++ GameSettings.Options.CHAT_HEIGHT_FOCUSED, GameSettings.Options.CHAT_HEIGHT_UNFOCUSED, + +> CHANGE 14 : 18 @ 14 : 15 + +~ boolean profanityFilterForce = EagRuntime.getConfiguration().isForceProfanityFilter(); +~ GameSettings.Options[] opts = profanityFilterForce ? no_profanity_filter : field_146399_a; +~ for (int j = 0; j < opts.length; ++j) { +~ GameSettings.Options gamesettings$options = opts[j]; + +> CHANGE 12 : 14 @ 12 : 14 + +~ this.buttonList.add(new GuiButton(200, this.width / 2 - 100, +~ this.height / 6 + (profanityFilterForce ? 130 : 154), I18n.format("gui.done", new Object[0]))); + +> CHANGE 2 : 3 @ 2 : 3 ~ protected void actionPerformed(GuiButton parGuiButton) { diff --git a/patches/minecraft/net/minecraft/client/gui/achievement/GuiAchievement.edit.java b/patches/minecraft/net/minecraft/client/gui/achievement/GuiAchievement.edit.java index a211f58e..3c0d6ef1 100644 --- a/patches/minecraft/net/minecraft/client/gui/achievement/GuiAchievement.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/achievement/GuiAchievement.edit.java @@ -11,7 +11,11 @@ > DELETE 3 @ 3 : 4 -> INSERT 114 : 146 @ 114 +> CHANGE 48 : 49 @ 48 : 49 + +~ ScaledResolution scaledresolution = mc.scaledResolution; + +> INSERT 65 : 97 @ 65 + public int getHeight() { + if (this.theAchievement != null && this.notificationTime != 0L && Minecraft.getMinecraft().thePlayer != null) { diff --git a/patches/minecraft/net/minecraft/client/gui/achievement/GuiAchievements.edit.java b/patches/minecraft/net/minecraft/client/gui/achievement/GuiAchievements.edit.java index 42a6e0de..11f866c8 100644 --- a/patches/minecraft/net/minecraft/client/gui/achievement/GuiAchievements.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/achievement/GuiAchievements.edit.java @@ -5,11 +5,12 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 7 @ 2 : 4 +> CHANGE 2 : 8 @ 2 : 4 ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; ~ ~ import net.lax1dude.eaglercraft.v1_8.Mouse; +~ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; ~ import net.lax1dude.eaglercraft.v1_8.minecraft.EaglerTextureAtlasSprite; ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; @@ -28,7 +29,11 @@ ~ protected int getCloseKey() { ~ return this.mc.gameSettings.keyBindInventory.getKeyCode(); -> CHANGE 76 : 77 @ 76 : 77 +> CHANGE 11 : 12 @ 11 : 12 + +~ if (PointerInputAbstraction.getVCursorButtonDown(0)) { + +> CHANGE 64 : 65 @ 64 : 65 ~ GlStateManager.disableLighting(); diff --git a/patches/minecraft/net/minecraft/client/gui/achievement/GuiStats.edit.java b/patches/minecraft/net/minecraft/client/gui/achievement/GuiStats.edit.java index 3692a793..35e7d51a 100644 --- a/patches/minecraft/net/minecraft/client/gui/achievement/GuiStats.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/achievement/GuiStats.edit.java @@ -12,7 +12,7 @@ + + import com.google.common.collect.Lists; + -+ import net.lax1dude.eaglercraft.v1_8.Mouse; ++ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; + import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; @@ -22,11 +22,25 @@ > DELETE 11 @ 11 : 12 -> CHANGE 71 : 72 @ 71 : 72 +> INSERT 32 : 39 @ 32 + ++ public void handleTouchInput() throws IOException { ++ super.handleTouchInput(); ++ if (this.displaySlot != null) { ++ this.displaySlot.handleTouchInput(); ++ } ++ } ++ + +> CHANGE 39 : 40 @ 39 : 40 ~ protected void actionPerformed(GuiButton parGuiButton) { -> CHANGE 270 : 272 @ 270 : 271 +> CHANGE 111 : 112 @ 111 : 112 + +~ if (!PointerInputAbstraction.getVCursorButtonDown(0)) { + +> CHANGE 158 : 160 @ 158 : 159 ~ for (int m = 0, l = StatList.objectMineStats.size(); m < l; ++m) { ~ StatCrafting statcrafting = StatList.objectMineStats.get(m); diff --git a/patches/minecraft/net/minecraft/client/gui/inventory/GuiContainer.edit.java b/patches/minecraft/net/minecraft/client/gui/inventory/GuiContainer.edit.java index 484f4f58..2dc25596 100644 --- a/patches/minecraft/net/minecraft/client/gui/inventory/GuiContainer.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/inventory/GuiContainer.edit.java @@ -9,12 +9,14 @@ ~ import java.util.List; -> INSERT 1 : 8 @ 1 +> INSERT 1 : 10 @ 1 + + import com.google.common.collect.Sets; + ++ import net.lax1dude.eaglercraft.v1_8.EagRuntime; + import net.lax1dude.eaglercraft.v1_8.Keyboard; ++ import net.lax1dude.eaglercraft.v1_8.Touch; + import net.lax1dude.eaglercraft.v1_8.minecraft.EaglerTextureAtlasSprite; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; + import net.lax1dude.eaglercraft.v1_8.opengl.OpenGlHelper; @@ -25,11 +27,29 @@ > DELETE 8 @ 8 : 9 -> INSERT 81 : 82 @ 81 +> INSERT 39 : 43 @ 39 + ++ if (primaryTouchPoint != -1 && Touch.fetchPointIdx(primaryTouchPoint) == -1) { ++ primaryTouchPoint = -1; ++ mouseReleased(lastTouchX, lastTouchY, 0); ++ } + +> CHANGE 30 : 31 @ 30 : 31 + +~ if (!this.mc.gameSettings.touchscreen && slot.canBeHovered() && this.isMouseOverSlot(slot, i, j)) { + +> INSERT 11 : 12 @ 11 + GlStateManager.enableAlpha(); -> CHANGE 107 : 108 @ 107 : 108 +> DELETE 21 @ 21 : 22 + +> CHANGE 18 : 20 @ 18 : 19 + +~ if (!this.mc.gameSettings.touchscreen && inventoryplayer.getItemStack() == null && this.theSlot != null +~ && this.theSlot.getHasStack()) { + +> CHANGE 66 : 67 @ 66 : 67 ~ EaglerTextureAtlasSprite textureatlassprite = this.mc.getTextureMapBlocks().getAtlasSprite(s1); @@ -37,7 +57,14 @@ ~ protected void mouseClicked(int parInt1, int parInt2, int parInt3) { -> CHANGE 126 : 129 @ 126 : 127 +> CHANGE 20 : 24 @ 20 : 24 + +~ // if (this.mc.gameSettings.touchscreen && flag1 && this.mc.thePlayer.inventory.getItemStack() == null) { +~ // this.mc.displayGuiScreen((GuiScreen) null); +~ // return; +~ // } + +> CHANGE 102 : 105 @ 102 : 103 ~ List lst = this.inventorySlots.inventorySlots; ~ for (int n = 0, m = lst.size(); n < m; ++n) { @@ -58,7 +85,7 @@ > INSERT 1 : 12 @ 1 + } else if (parInt1 == 1) { -+ showingCloseKey = System.currentTimeMillis(); ++ showingCloseKey = EagRuntime.steadyTimeMillis(); + } else { + this.checkHotbarKeys(parInt1); + if (this.theSlot != null && this.theSlot.getHasStack()) { @@ -71,4 +98,67 @@ > DELETE 1 @ 1 : 2 +> INSERT 29 : 30 @ 29 + ++ return; + +> INSERT 1 : 6 @ 1 + ++ if (primaryTouchPoint != -1 && Touch.fetchPointIdx(primaryTouchPoint) == -1) { ++ primaryTouchPoint = -1; ++ mouseReleased(lastTouchX, lastTouchY, 0); ++ } ++ } + +> INSERT 1 : 3 @ 1 + ++ protected float getTouchModeScale() { ++ return 1.25f; + +> INSERT 1 : 44 @ 1 + ++ ++ private int primaryTouchPoint = -1; ++ private int lastTouchX = -1; ++ private int lastTouchY = -1; ++ ++ protected void touchStarted(int touchX, int touchY, int uid) { ++ if (primaryTouchPoint == -1) { ++ primaryTouchPoint = uid; ++ lastTouchX = touchX; ++ lastTouchY = touchY; ++ mouseClicked(touchX, touchY, 0); ++ } ++ } ++ ++ protected void touchMoved(int touchX, int touchY, int uid) { ++ if (primaryTouchPoint == uid) { ++ lastTouchX = touchX; ++ lastTouchY = touchY; ++ mouseClickMove(touchX, touchY, 0, 0l); ++ } ++ } ++ ++ protected void touchEndMove(int touchX, int touchY, int uid) { ++ if (primaryTouchPoint == uid) { ++ primaryTouchPoint = -1; ++ lastTouchX = touchX; ++ lastTouchY = touchY; ++ mouseReleased(touchX, touchY, 0); ++ } ++ } ++ ++ protected void touchTapped(int touchX, int touchY, int uid) { ++ if (primaryTouchPoint == uid) { ++ primaryTouchPoint = -1; ++ lastTouchX = touchX; ++ lastTouchY = touchY; ++ mouseReleased(touchX, touchY, 0); ++ } ++ } ++ ++ protected boolean shouldTouchGenerateMouseEvents() { ++ return false; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/inventory/GuiContainerCreative.edit.java b/patches/minecraft/net/minecraft/client/gui/inventory/GuiContainerCreative.edit.java index 80af564d..ce5ef317 100644 --- a/patches/minecraft/net/minecraft/client/gui/inventory/GuiContainerCreative.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/inventory/GuiContainerCreative.edit.java @@ -7,14 +7,16 @@ > DELETE 2 @ 2 : 3 -> INSERT 4 : 11 @ 4 +> INSERT 4 : 13 @ 4 + + import com.google.common.collect.Lists; + + import net.lax1dude.eaglercraft.v1_8.Keyboard; + import net.lax1dude.eaglercraft.v1_8.Mouse; ++ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; + import net.lax1dude.eaglercraft.v1_8.internal.EnumCursorType; ++ import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; > DELETE 4 @ 4 : 7 @@ -64,13 +66,41 @@ ~ for (int m = 0; m < CreativeTabs.creativeTabArray.length; ++m) { ~ CreativeTabs creativetabs = CreativeTabs.creativeTabArray[m]; -> CHANGE 127 : 130 @ 127 : 129 +> INSERT 10 : 26 @ 10 + ++ @Override ++ protected void touchTapped(int touchX, int touchY, int uid) { ++ int l = touchX - this.guiLeft; ++ int i1 = touchY - this.guiTop; ++ ++ for (int m = 0; m < CreativeTabs.creativeTabArray.length; ++m) { ++ CreativeTabs creativetabs = CreativeTabs.creativeTabArray[m]; ++ if (this.func_147049_a(creativetabs, l, i1)) { ++ this.setCurrentCreativeTab(creativetabs); ++ break; ++ } ++ } ++ ++ super.touchTapped(touchX, touchY, uid); ++ } ++ + +> CHANGE 93 : 94 @ 93 : 94 + +~ boolean flag = PointerInputAbstraction.getVCursorButtonDown(0); + +> CHANGE 23 : 26 @ 23 : 25 ~ for (int m = 0; m < CreativeTabs.creativeTabArray.length; ++m) { ~ if (this.renderCreativeInventoryHoveringText(CreativeTabs.creativeTabArray[m], i, j)) { ~ Mouse.showCursor(EnumCursorType.HAND); -> CHANGE 24 : 26 @ 24 : 25 +> CHANGE 16 : 18 @ 16 : 17 + +~ List list = itemstack.getTooltipProfanityFilter(this.mc.thePlayer, +~ this.mc.gameSettings.advancedItemTooltips); + +> CHANGE 7 : 9 @ 7 : 8 ~ for (int m = 0; m < CreativeTabs.creativeTabArray.length; ++m) { ~ CreativeTabs creativetabs1 = CreativeTabs.creativeTabArray[m]; @@ -84,11 +114,22 @@ ~ protected void actionPerformed(GuiButton parGuiButton) { -> INSERT 139 : 143 @ 139 +> INSERT 139 : 154 @ 139 + + public boolean blockPTTKey() { + return searchField.isFocused(); + } ++ ++ @Override ++ public boolean showCopyPasteButtons() { ++ return searchField.isFocused(); ++ } ++ ++ @Override ++ public void fireInputEvent(EnumInputEvent event, String param) { ++ searchField.fireInputEvent(event, param); ++ } ++ > EOF diff --git a/patches/minecraft/net/minecraft/client/gui/inventory/GuiEditSign.edit.java b/patches/minecraft/net/minecraft/client/gui/inventory/GuiEditSign.edit.java index 91e4dea9..dda3843d 100644 --- a/patches/minecraft/net/minecraft/client/gui/inventory/GuiEditSign.edit.java +++ b/patches/minecraft/net/minecraft/client/gui/inventory/GuiEditSign.edit.java @@ -5,16 +5,31 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 4 @ 2 : 3 +> CHANGE 2 : 7 @ 2 : 3 +~ import net.lax1dude.eaglercraft.v1_8.Display; ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; +~ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; +~ import net.lax1dude.eaglercraft.v1_8.minecraft.GuiScreenVisualViewport; ~ import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; > DELETE 4 @ 4 : 5 -> DELETE 7 @ 7 : 8 +> INSERT 1 : 2 @ 1 -> CHANGE 34 : 35 @ 34 : 35 ++ import net.minecraft.client.renderer.tileentity.TileEntitySignRenderer; + +> DELETE 6 @ 6 : 7 + +> CHANGE 1 : 2 @ 1 : 2 + +~ public class GuiEditSign extends GuiScreenVisualViewport { + +> CHANGE 28 : 29 @ 28 : 29 + +~ public void updateScreen0() { + +> CHANGE 3 : 4 @ 3 : 4 ~ protected void actionPerformed(GuiButton parGuiButton) { @@ -22,11 +37,33 @@ ~ protected void keyTyped(char parChar1, int parInt1) { -> INSERT 68 : 72 @ 68 +> CHANGE 25 : 26 @ 25 : 26 + +~ public void drawScreen0(int i, int j, float f) { + +> CHANGE 37 : 47 @ 37 : 38 + +~ try { +~ TileEntitySignRenderer.disableProfanityFilter = true; +~ TileEntityRendererDispatcher.instance.renderTileEntityAt(this.tileSign, -0.5D, +~ (PointerInputAbstraction.isTouchMode() && (Display.getVisualViewportH() / mc.displayHeight) < 0.75f) +~ ? -0.25D +~ : -0.75D, +~ -0.5D, 0.0F); +~ } finally { +~ TileEntitySignRenderer.disableProfanityFilter = false; +~ } + +> CHANGE 2 : 3 @ 2 : 3 + +~ super.drawScreen0(i, j, f); + +> INSERT 1 : 6 @ 1 + + public boolean blockPTTKey() { + return true; + } ++ > EOF diff --git a/patches/minecraft/net/minecraft/client/multiplayer/ChunkProviderClient.edit.java b/patches/minecraft/net/minecraft/client/multiplayer/ChunkProviderClient.edit.java index ef37860a..37b40806 100644 --- a/patches/minecraft/net/minecraft/client/multiplayer/ChunkProviderClient.edit.java +++ b/patches/minecraft/net/minecraft/client/multiplayer/ChunkProviderClient.edit.java @@ -7,19 +7,32 @@ > DELETE 2 @ 2 : 3 -> INSERT 1 : 6 @ 1 +> INSERT 1 : 7 @ 1 + + import com.google.common.collect.Lists; + ++ import net.lax1dude.eaglercraft.v1_8.EagRuntime; + import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; + import net.lax1dude.eaglercraft.v1_8.log4j.Logger; > DELETE 10 @ 10 : 12 -> CHANGE 50 : 52 @ 50 : 52 +> CHANGE 48 : 49 @ 48 : 49 + +~ long i = EagRuntime.steadyTimeMillis(); + +> CHANGE 1 : 3 @ 1 : 3 ~ for (int j = 0, k = this.chunkListing.size(); j < k; ++j) { -~ this.chunkListing.get(j).func_150804_b(System.currentTimeMillis() - i > 5L); +~ this.chunkListing.get(j).func_150804_b(EagRuntime.steadyTimeMillis() - i > 5L); + +> CHANGE 2 : 3 @ 2 : 3 + +~ if (EagRuntime.steadyTimeMillis() - i > 100L) { + +> CHANGE 1 : 2 @ 1 : 2 + +~ new Object[] { Long.valueOf(EagRuntime.steadyTimeMillis() - i) }); > EOF diff --git a/patches/minecraft/net/minecraft/client/multiplayer/GuiConnecting.edit.java b/patches/minecraft/net/minecraft/client/multiplayer/GuiConnecting.edit.java index db5e1813..430109d9 100644 --- a/patches/minecraft/net/minecraft/client/multiplayer/GuiConnecting.edit.java +++ b/patches/minecraft/net/minecraft/client/multiplayer/GuiConnecting.edit.java @@ -5,11 +5,15 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 3 : 13 @ 3 : 6 +> CHANGE 3 : 21 @ 3 : 6 +~ import java.util.List; ~ +~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; +~ import net.lax1dude.eaglercraft.v1_8.cookie.ServerCookieDataStore; ~ import net.lax1dude.eaglercraft.v1_8.internal.EnumEaglerConnectionState; -~ import net.lax1dude.eaglercraft.v1_8.internal.EnumServerRateLimit; +~ import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketClient; +~ import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketFrame; ~ import net.lax1dude.eaglercraft.v1_8.internal.PlatformNetworking; ~ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; ~ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; @@ -17,23 +21,31 @@ ~ import net.lax1dude.eaglercraft.v1_8.socket.ConnectionHandshake; ~ import net.lax1dude.eaglercraft.v1_8.socket.EaglercraftNetworkManager; ~ import net.lax1dude.eaglercraft.v1_8.socket.RateLimitTracker; +~ import net.lax1dude.eaglercraft.v1_8.socket.WebSocketNetworkManager; +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageConstants; +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageProtocol; +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.client.GameProtocolMessageController; > CHANGE 4 : 5 @ 4 : 8 ~ import net.minecraft.client.network.NetHandlerPlayClient; -> DELETE 2 @ 2 : 5 +> CHANGE 2 : 3 @ 2 : 5 + +~ import net.minecraft.network.play.client.C17PacketCustomPayload; > DELETE 1 @ 1 : 4 > DELETE 2 @ 2 : 3 -> CHANGE 1 : 5 @ 1 : 2 +> CHANGE 1 : 7 @ 1 : 2 +~ private IWebSocketClient webSocket; ~ private EaglercraftNetworkManager networkManager; ~ private String currentAddress; ~ private String currentPassword; ~ private boolean allowPlaintext; +~ private boolean allowCookies; > INSERT 1 : 2 @ 1 @@ -63,36 +75,52 @@ ~ String serveraddress = AddressResolver.resolveURI(parServerData); -> CHANGE 2 : 7 @ 2 : 3 +> CHANGE 2 : 8 @ 2 : 3 ~ if (RateLimitTracker.isLockedOut(serveraddress)) { ~ logger.error("Server locked this client out on a previous connection, will not attempt to reconnect"); ~ } else { -~ this.connect(serveraddress, password, allowPlaintext); +~ this.connect(serveraddress, password, allowPlaintext, +~ parServerData.enableCookies && EagRuntime.getConfiguration().isEnableServerCookies()); ~ } -> INSERT 3 : 16 @ 3 +> CHANGE 3 : 4 @ 3 : 7 -+ this(parGuiScreen, mcIn, hostName, port, false); -+ } -+ -+ public GuiConnecting(GuiScreen parGuiScreen, Minecraft mcIn, String hostName, int port, boolean allowPlaintext) { -+ this(parGuiScreen, mcIn, hostName, port, null, allowPlaintext); -+ } -+ -+ public GuiConnecting(GuiScreen parGuiScreen, Minecraft mcIn, String hostName, int port, String password) { -+ this(parGuiScreen, mcIn, hostName, port, password, false); -+ } -+ -+ public GuiConnecting(GuiScreen parGuiScreen, Minecraft mcIn, String hostName, int port, String password, -+ boolean allowPlaintext) { - -> CHANGE 3 : 4 @ 3 : 4 - -~ this.connect(hostName, password, allowPlaintext); +~ this(parGuiScreen, mcIn, hostName, port, false, EagRuntime.getConfiguration().isEnableServerCookies()); > CHANGE 2 : 5 @ 2 : 7 +~ public GuiConnecting(GuiScreen parGuiScreen, Minecraft mcIn, String hostName, int port, boolean allowCookies) { +~ this(parGuiScreen, mcIn, hostName, port, false, allowCookies); +~ } + +> CHANGE 1 : 5 @ 1 : 5 + +~ public GuiConnecting(GuiScreen parGuiScreen, Minecraft mcIn, String hostName, int port, boolean allowPlaintext, +~ boolean allowCookies) { +~ this(parGuiScreen, mcIn, hostName, port, null, allowPlaintext, allowCookies); +~ } + +> CHANGE 1 : 5 @ 1 : 15 + +~ public GuiConnecting(GuiScreen parGuiScreen, Minecraft mcIn, String hostName, int port, String password, +~ boolean allowCookies) { +~ this(parGuiScreen, mcIn, hostName, port, password, false, allowCookies); +~ } + +> CHANGE 1 : 9 @ 1 : 9 + +~ public GuiConnecting(GuiScreen parGuiScreen, Minecraft mcIn, String hostName, int port, String password, +~ boolean allowPlaintext, boolean allowCookies) { +~ this.mc = mcIn; +~ this.previousGuiScreen = parGuiScreen; +~ mcIn.loadWorld((WorldClient) null); +~ this.connect(hostName, password, allowPlaintext, +~ allowCookies && EagRuntime.getConfiguration().isEnableServerCookies()); +~ } + +> CHANGE 1 : 4 @ 1 : 7 + ~ public GuiConnecting(GuiConnecting previous, String password) { ~ this(previous, password, false); ~ } @@ -102,73 +130,87 @@ ~ public GuiConnecting(GuiConnecting previous, String password, boolean allowPlaintext) { ~ this.mc = previous.mc; ~ this.previousGuiScreen = previous.previousGuiScreen; -~ this.connect(previous.currentAddress, password, allowPlaintext); +~ this.connect(previous.currentAddress, password, allowPlaintext, previous.allowCookies); ~ } -> CHANGE 1 : 6 @ 1 : 15 +> CHANGE 1 : 6 @ 1 : 3 -~ private void connect(String ip, String password, boolean allowPlaintext) { +~ private void connect(String ip, String password, boolean allowPlaintext, boolean allowCookies) { ~ this.currentAddress = ip; ~ this.currentPassword = password; ~ this.allowPlaintext = allowPlaintext; -~ } +~ this.allowCookies = allowCookies; -> CHANGE 1 : 41 @ 1 : 8 +> CHANGE 3 : 14 @ 3 : 6 -~ public void updateScreen() { ~ ++timer; ~ if (timer > 1) { ~ if (this.currentAddress == null) { ~ mc.displayGuiScreen(GuiDisconnected.createRateLimitKick(previousGuiScreen)); -~ } else if (this.networkManager == null) { +~ } else if (webSocket == null) { ~ logger.info("Connecting to: {}", currentAddress); -~ this.networkManager = new EaglercraftNetworkManager(currentAddress); -~ this.networkManager.connect(); -~ } else { -~ if (this.networkManager.isChannelOpen()) { +~ webSocket = PlatformNetworking.openWebSocket(currentAddress); +~ if (webSocket == null) { +~ mc.displayGuiScreen(new GuiDisconnected(previousGuiScreen, "connect.failed", +~ new ChatComponentText("Could not open WebSocket to \"" + currentAddress + "\"!"))); +~ } + +> CHANGE 1 : 79 @ 1 : 2 + +~ if (webSocket.getState() == EnumEaglerConnectionState.CONNECTED) { ~ if (!hasOpened) { ~ hasOpened = true; ~ logger.info("Logging in: {}", currentAddress); -~ if (ConnectionHandshake.attemptHandshake(this.mc, this, previousGuiScreen, currentPassword, -~ allowPlaintext)) { +~ byte[] cookieData = null; +~ if (allowCookies) { +~ ServerCookieDataStore.ServerCookie cookie = ServerCookieDataStore +~ .loadCookie(currentAddress); +~ if (cookie != null) { +~ cookieData = cookie.cookie; +~ } +~ } +~ if (ConnectionHandshake.attemptHandshake(this.mc, webSocket, this, previousGuiScreen, +~ currentPassword, allowPlaintext, allowCookies, cookieData)) { ~ logger.info("Handshake Success"); +~ this.networkManager = new WebSocketNetworkManager(webSocket); ~ this.networkManager.setPluginInfo(ConnectionHandshake.pluginBrand, ~ ConnectionHandshake.pluginVersion); ~ mc.bungeeOutdatedMsgTimer = 80; ~ mc.clearTitles(); ~ this.networkManager.setConnectionState(EnumConnectionState.PLAY); -~ this.networkManager.setNetHandler(new NetHandlerPlayClient(this.mc, previousGuiScreen, -~ this.networkManager, this.mc.getSession().getProfile())); +~ NetHandlerPlayClient netHandler = new NetHandlerPlayClient(this.mc, previousGuiScreen, +~ this.networkManager, this.mc.getSession().getProfile()); +~ this.networkManager.setNetHandler(netHandler); +~ netHandler.setEaglerMessageController(new GameProtocolMessageController( +~ GamePluginMessageProtocol.getByVersion(ConnectionHandshake.protocolVersion), +~ GamePluginMessageConstants.CLIENT_TO_SERVER, +~ GameProtocolMessageController +~ .createClientHandler(ConnectionHandshake.protocolVersion, netHandler), +~ (ch, msg) -> netHandler.addToSendQueue(new C17PacketCustomPayload(ch, msg)))); ~ } else { ~ if (mc.currentScreen == this) { -~ checkLowLevelRatelimit(); -~ } -~ if (mc.currentScreen == this) { +~ checkRatelimit(); ~ logger.info("Handshake Failure"); ~ mc.getSession().reset(); ~ mc.displayGuiScreen( ~ new GuiDisconnected(previousGuiScreen, "connect.failed", new ChatComponentText( ~ "Handshake Failure\n\nAre you sure this is an eagler 1.8 server?"))); ~ } -~ if (!PlatformNetworking.playConnectionState().isClosed()) { -~ PlatformNetworking.playDisconnect(); -~ } +~ webSocket.close(); ~ return; ~ } - -> CHANGE 1 : 4 @ 1 : 7 - -~ try { -~ this.networkManager.processReceivedPackets(); -~ } catch (IOException ex) { - -> CHANGE 1 : 29 @ 1 : 5 - +~ } +~ if (this.networkManager != null) { +~ try { +~ this.networkManager.processReceivedPackets(); +~ } catch (IOException ex) { +~ } +~ } ~ } else { -~ if (PlatformNetworking.playConnectionState() == EnumEaglerConnectionState.FAILED) { +~ if (webSocket.getState() == EnumEaglerConnectionState.FAILED) { ~ if (!hasOpened) { ~ mc.getSession().reset(); -~ checkLowLevelRatelimit(); +~ checkRatelimit(); ~ if (mc.currentScreen == this) { ~ if (RateLimitTracker.isProbablyLockedOut(currentAddress)) { ~ mc.displayGuiScreen(GuiDisconnected.createRateLimitKick(previousGuiScreen)); @@ -179,9 +221,9 @@ ~ } ~ } ~ } else { -~ if (this.networkManager.checkDisconnected()) { +~ if (this.networkManager != null && this.networkManager.checkDisconnected()) { ~ this.mc.getSession().reset(); -~ checkLowLevelRatelimit(); +~ checkRatelimit(); ~ if (mc.currentScreen == this) { ~ if (RateLimitTracker.isProbablyLockedOut(currentAddress)) { ~ mc.displayGuiScreen(GuiDisconnected.createRateLimitKick(previousGuiScreen)); @@ -192,12 +234,21 @@ ~ } ~ } ~ } +~ } + +> INSERT 1 : 8 @ 1 + ++ if (timer > 200) { ++ if (webSocket != null) { ++ webSocket.close(); ++ } ++ mc.displayGuiScreen(new GuiDisconnected(previousGuiScreen, "connect.failed", ++ new ChatComponentText("Handshake timed out"))); ++ } > DELETE 1 @ 1 : 2 -> DELETE 1 @ 1 : 11 - -> CHANGE 4 : 5 @ 4 : 5 +> CHANGE 2 : 3 @ 2 : 3 ~ protected void keyTyped(char parChar1, int parInt1) { @@ -210,25 +261,41 @@ ~ protected void actionPerformed(GuiButton parGuiButton) { -> CHANGE 13 : 14 @ 13 : 14 +> INSERT 4 : 6 @ 4 + ++ } else if (this.webSocket != null) { ++ this.webSocket.close(); + +> CHANGE 9 : 10 @ 9 : 10 ~ if (this.networkManager == null || !this.networkManager.isChannelOpen()) { -> INSERT 9 : 23 @ 9 +> INSERT 9 : 34 @ 9 + -+ private void checkLowLevelRatelimit() { -+ EnumServerRateLimit rateLimit = PlatformNetworking.getRateLimit(); -+ if (rateLimit == EnumServerRateLimit.BLOCKED) { -+ RateLimitTracker.registerBlock(currentAddress); -+ mc.displayGuiScreen(GuiDisconnected.createRateLimitKick(previousGuiScreen)); -+ logger.info("Handshake Failure: Too Many Requests!"); -+ } else if (rateLimit == EnumServerRateLimit.LOCKED_OUT) { -+ RateLimitTracker.registerLockOut(currentAddress); -+ mc.displayGuiScreen(GuiDisconnected.createRateLimitKick(previousGuiScreen)); -+ logger.info("Handshake Failure: Too Many Requests!"); -+ logger.info("Server has locked this client out"); ++ private void checkRatelimit() { ++ if (this.webSocket != null) { ++ List strFrames = webSocket.getNextStringFrames(); ++ if (strFrames != null) { ++ for (int i = 0; i < strFrames.size(); ++i) { ++ String str = strFrames.get(i).getString(); ++ if (str.equalsIgnoreCase("BLOCKED")) { ++ RateLimitTracker.registerBlock(currentAddress); ++ mc.displayGuiScreen(GuiDisconnected.createRateLimitKick(previousGuiScreen)); ++ logger.info("Handshake Failure: Too Many Requests!"); ++ } else if (str.equalsIgnoreCase("LOCKED")) { ++ RateLimitTracker.registerLockOut(currentAddress); ++ mc.displayGuiScreen(GuiDisconnected.createRateLimitKick(previousGuiScreen)); ++ logger.info("Handshake Failure: Too Many Requests!"); ++ logger.info("Server has locked this client out"); ++ } ++ } ++ } + } + } ++ ++ public boolean canCloseGui() { ++ return false; ++ } > EOF diff --git a/patches/minecraft/net/minecraft/client/multiplayer/PlayerControllerMP.edit.java b/patches/minecraft/net/minecraft/client/multiplayer/PlayerControllerMP.edit.java index 979b68aa..057f10a4 100644 --- a/patches/minecraft/net/minecraft/client/multiplayer/PlayerControllerMP.edit.java +++ b/patches/minecraft/net/minecraft/client/multiplayer/PlayerControllerMP.edit.java @@ -5,10 +5,11 @@ # Version: 1.0 # Author: lax1dude -> INSERT 2 : 5 @ 2 +> INSERT 2 : 6 @ 2 + import java.io.IOException; + ++ import net.lax1dude.eaglercraft.v1_8.ClientUUIDLoadingCache; + import net.lax1dude.eaglercraft.v1_8.socket.EaglercraftNetworkManager; > DELETE 6 @ 6 : 7 @@ -17,7 +18,7 @@ + import net.minecraft.util.ChatComponentText; -> CHANGE 228 : 240 @ 228 : 229 +> CHANGE 228 : 242 @ 228 : 229 ~ try { ~ this.netClientHandler.getNetworkManager().processReceivedPackets(); @@ -31,6 +32,8 @@ ~ } ~ this.netClientHandler.getSkinCache().flush(); ~ this.netClientHandler.getCapeCache().flush(); +~ this.netClientHandler.getNotifManager().runTick(); +~ ClientUUIDLoadingCache.update(); > CHANGE 96 : 98 @ 96 : 98 diff --git a/patches/minecraft/net/minecraft/client/multiplayer/ServerData.edit.java b/patches/minecraft/net/minecraft/client/multiplayer/ServerData.edit.java index 2d8c84de..e4f32ad8 100644 --- a/patches/minecraft/net/minecraft/client/multiplayer/ServerData.edit.java +++ b/patches/minecraft/net/minecraft/client/multiplayer/ServerData.edit.java @@ -5,13 +5,14 @@ # Version: 1.0 # Author: lax1dude -> INSERT 2 : 13 @ 2 +> INSERT 2 : 14 @ 2 + import java.io.IOException; + + import org.json.JSONArray; + import org.json.JSONObject; + ++ import net.lax1dude.eaglercraft.v1_8.EagRuntime; + import net.lax1dude.eaglercraft.v1_8.internal.IServerQuery; + import net.lax1dude.eaglercraft.v1_8.internal.QueryResponse; + import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; @@ -33,7 +34,7 @@ ~ public boolean hideAddress = false; -> INSERT 1 : 9 @ 1 +> INSERT 1 : 10 @ 1 + public IServerQuery currentQuery = null; + public final ResourceLocation iconResourceLocation; @@ -43,6 +44,7 @@ + public boolean hasPing = false; + public boolean serverIconEnabled = false; + public boolean isDefault = false; ++ public boolean enableCookies; > INSERT 1 : 5 @ 1 @@ -51,35 +53,50 @@ + private static int serverTextureId = 0; + -> INSERT 4 : 5 @ 4 +> INSERT 4 : 6 @ 4 + this.iconResourceLocation = new ResourceLocation("eagler:servers/icons/tex_" + serverTextureId++); ++ this.enableCookies = EagRuntime.getConfiguration().isEnableServerCookies(); > DELETE 6 @ 6 : 9 -> INSERT 7 : 9 @ 7 +> INSERT 7 : 10 @ 7 + nbttagcompound.setBoolean("hideAddress", this.hideAddress); ++ nbttagcompound.setBoolean("enableCookies", this.enableCookies); + > DELETE 13 @ 13 : 16 -> INSERT 11 : 17 @ 11 +> CHANGE 11 : 16 @ 11 : 13 -+ if (nbtCompound.hasKey("hideAddress", 1)) { -+ serverdata.hideAddress = nbtCompound.getBoolean("hideAddress"); -+ } else { -+ serverdata.hideAddress = false; -+ } -+ +~ if (nbtCompound.hasKey("hideAddress", 1)) { +~ serverdata.hideAddress = nbtCompound.getBoolean("hideAddress"); +~ } else { +~ serverdata.hideAddress = false; +~ } -> DELETE 3 @ 3 : 11 +> CHANGE 1 : 6 @ 1 : 4 -> CHANGE 8 : 9 @ 8 : 9 +~ if (nbtCompound.hasKey("enableCookies", 1)) { +~ serverdata.enableCookies = nbtCompound.getBoolean("enableCookies"); +~ } else { +~ serverdata.enableCookies = true; +~ } + +> CHANGE 1 : 2 @ 1 : 3 + +~ return serverdata; + +> CHANGE 10 : 11 @ 10 : 11 ~ this.hideAddress = serverDataIn.hideAddress; -> INSERT 6 : 8 @ 6 +> INSERT 1 : 2 @ 1 + ++ this.enableCookies = serverDataIn.enableCookies; + +> INSERT 5 : 7 @ 5 + public static final ServerResourceMode[] _VALUES = values(); + diff --git a/patches/minecraft/net/minecraft/client/multiplayer/ServerList.edit.java b/patches/minecraft/net/minecraft/client/multiplayer/ServerList.edit.java index 1d6a6fcf..5e33c581 100644 --- a/patches/minecraft/net/minecraft/client/multiplayer/ServerList.edit.java +++ b/patches/minecraft/net/minecraft/client/multiplayer/ServerList.edit.java @@ -60,7 +60,7 @@ + + public void loadServerList(byte[] localStorage) { -> CHANGE 1 : 8 @ 1 : 5 +> CHANGE 1 : 9 @ 1 : 5 ~ freeServerIcons(); ~ @@ -68,6 +68,7 @@ ~ for (DefaultServer srv : EagRuntime.getConfiguration().getDefaultServerList()) { ~ ServerData dat = new ServerData(srv.name, srv.addr, true); ~ dat.isDefault = true; +~ dat.hideAddress = srv.hideAddress; ~ this.allServers.add(dat); > CHANGE 2 : 8 @ 2 : 3 @@ -135,7 +136,7 @@ ~ data.iconTextureObject = null; ~ } -> INSERT 36 : 144 @ 36 +> INSERT 36 : 145 @ 36 + + public void freeServerIcons() { @@ -170,7 +171,7 @@ + for (int i = 0, l = this.servers.size(); i < l; ++i) { + ServerData dat = this.servers.get(i); + if (dat.pingSentTime <= 0l) { -+ dat.pingSentTime = System.currentTimeMillis(); ++ dat.pingSentTime = EagRuntime.steadyTimeMillis(); + if (RateLimitTracker.isLockedOut(dat.serverIP)) { + logger.error( + "Server {} locked this client out on a previous connection, will not attempt to reconnect", @@ -192,6 +193,7 @@ + } + } + } else if (dat.currentQuery != null) { ++ dat.currentQuery.update(); + if (!dat.hasPing) { + ++total; + EnumServerRateLimit rateLimit = dat.currentQuery.getRateLimit(); @@ -228,7 +230,7 @@ + dat.setIconPacket(r); + } + if (!dat.currentQuery.isOpen() && dat.pingSentTime > 0l -+ && (System.currentTimeMillis() - dat.pingSentTime) > 2000l && !dat.hasPing) { ++ && (EagRuntime.steadyTimeMillis() - dat.pingSentTime) > 2000l && !dat.hasPing) { + if (RateLimitTracker.isProbablyLockedOut(dat.serverIP)) { + logger.error("Server {} ratelimited this client out on a previous connection, assuming lockout", + dat.serverIP); diff --git a/patches/minecraft/net/minecraft/client/multiplayer/WorldClient.edit.java b/patches/minecraft/net/minecraft/client/multiplayer/WorldClient.edit.java index 7bf823cc..927cc2f9 100644 --- a/patches/minecraft/net/minecraft/client/multiplayer/WorldClient.edit.java +++ b/patches/minecraft/net/minecraft/client/multiplayer/WorldClient.edit.java @@ -17,7 +17,29 @@ > DELETE 5 @ 5 : 6 -> CHANGE 211 : 212 @ 211 : 212 +> DELETE 9 @ 9 : 10 + +> CHANGE 25 : 26 @ 25 : 26 + +~ EnumDifficulty parEnumDifficulty) { + +> CHANGE 1 : 2 @ 1 : 2 + +~ WorldProvider.getProviderForDimension(parInt1), true); + +> DELETE 17 @ 17 : 19 + +> DELETE 8 @ 8 : 9 + +> DELETE 1 @ 1 : 2 + +> DELETE 1 @ 1 : 2 + +> DELETE 24 @ 24 : 25 + +> DELETE 2 @ 2 : 3 + +> CHANGE 113 : 114 @ 113 : 114 ~ EaglercraftRandom random = new EaglercraftRandom(); diff --git a/patches/minecraft/net/minecraft/client/network/NetHandlerPlayClient.edit.java b/patches/minecraft/net/minecraft/client/network/NetHandlerPlayClient.edit.java index 7e1f3bad..e4253bea 100644 --- a/patches/minecraft/net/minecraft/client/network/NetHandlerPlayClient.edit.java +++ b/patches/minecraft/net/minecraft/client/network/NetHandlerPlayClient.edit.java @@ -9,25 +9,27 @@ > DELETE 4 @ 4 : 6 -> INSERT 1 : 22 @ 1 +> INSERT 1 : 24 @ 1 + -+ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ++ import net.lax1dude.eaglercraft.v1_8.ClientUUIDLoadingCache; + import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; + import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; + + import com.google.common.collect.Maps; + + import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; -+ import net.lax1dude.eaglercraft.v1_8.profile.CapePackets; ++ import net.lax1dude.eaglercraft.v1_8.notifications.ServerNotificationManager; + import net.lax1dude.eaglercraft.v1_8.profile.ServerCapeCache; + import net.lax1dude.eaglercraft.v1_8.profile.ServerSkinCache; -+ import net.lax1dude.eaglercraft.v1_8.profile.SkinPackets; + import net.lax1dude.eaglercraft.v1_8.socket.EaglercraftNetworkManager; ++ import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageProtocol; ++ import net.lax1dude.eaglercraft.v1_8.socket.protocol.client.GameProtocolMessageController; ++ import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; + import net.lax1dude.eaglercraft.v1_8.sp.lan.LANClientNetworkManager; + import net.lax1dude.eaglercraft.v1_8.sp.socket.ClientIntegratedServerNetworkManager; -+ import net.lax1dude.eaglercraft.v1_8.update.UpdateService; + import net.lax1dude.eaglercraft.v1_8.voice.VoiceClientController; ++ import net.lax1dude.eaglercraft.v1_8.webview.WebViewOverlayController; + import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; + import net.lax1dude.eaglercraft.v1_8.log4j.Logger; + import net.lax1dude.eaglercraft.v1_8.minecraft.EaglerFolderResourcePack; @@ -67,31 +69,38 @@ ~ private final Map playerInfoMap = Maps.newHashMap(); -> CHANGE 2 : 7 @ 2 : 3 +> CHANGE 2 : 12 @ 2 : 3 ~ private boolean isIntegratedServer = false; ~ private final EaglercraftRandom avRandomizer = new EaglercraftRandom(); ~ private final ServerSkinCache skinCache; ~ private final ServerCapeCache capeCache; +~ private final ServerNotificationManager notifManager; ~ public boolean currentFNAWSkinAllowedState = true; +~ public boolean currentFNAWSkinForcedState = true; +~ private GameProtocolMessageController eaglerMessageController = null; +~ public boolean hasRequestedServerInfo = false; +~ public byte[] cachedServerInfoData = null; > CHANGE 1 : 2 @ 1 : 2 ~ public NetHandlerPlayClient(Minecraft mcIn, GuiScreen parGuiScreen, EaglercraftNetworkManager parNetworkManager, -> INSERT 5 : 9 @ 5 +> INSERT 5 : 10 @ 5 -+ this.skinCache = new ServerSkinCache(parNetworkManager, mcIn.getTextureManager()); -+ this.capeCache = new ServerCapeCache(parNetworkManager, mcIn.getTextureManager()); ++ this.skinCache = new ServerSkinCache(this, mcIn.getTextureManager()); ++ this.capeCache = new ServerCapeCache(this, mcIn.getTextureManager()); ++ this.notifManager = new ServerNotificationManager(); + this.isIntegratedServer = (parNetworkManager instanceof ClientIntegratedServerNetworkManager) + || (parNetworkManager instanceof LANClientNetworkManager); -> INSERT 4 : 6 @ 4 +> INSERT 4 : 7 @ 4 + this.skinCache.destroy(); + this.capeCache.destroy(); ++ this.notifManager.destroy(); -> INSERT 2 : 10 @ 2 +> INSERT 2 : 51 @ 2 + public ServerSkinCache getSkinCache() { + return this.skinCache; @@ -101,15 +110,61 @@ + return this.capeCache; + } + ++ public ServerNotificationManager getNotifManager() { ++ return this.notifManager; ++ } ++ ++ public GameProtocolMessageController getEaglerMessageController() { ++ return eaglerMessageController; ++ } ++ ++ public void setEaglerMessageController(GameProtocolMessageController eaglerMessageController) { ++ this.eaglerMessageController = eaglerMessageController; ++ } ++ ++ public GamePluginMessageProtocol getEaglerMessageProtocol() { ++ return eaglerMessageController != null ? eaglerMessageController.protocol : null; ++ } ++ ++ public void sendEaglerMessage(GameMessagePacket packet) { ++ try { ++ eaglerMessageController.sendPacket(packet); ++ } catch (IOException e) { ++ logger.error("Failed to send eaglercraft plugin message packet: " + packet); ++ logger.error(e); ++ } ++ } ++ ++ public boolean webViewSendHandler(GameMessagePacket pkt) { ++ if (eaglerMessageController == null) { ++ return false; ++ } ++ if (this.gameController.thePlayer == null || this.gameController.thePlayer.sendQueue != this) { ++ logger.error("WebView sent message on a dead handler!"); ++ return false; ++ } ++ if (eaglerMessageController.protocol.ver >= 4) { ++ sendEaglerMessage(pkt); ++ return true; ++ } else { ++ return false; ++ } ++ } ++ > DELETE 1 @ 1 : 2 -> INSERT 16 : 20 @ 16 +> CHANGE 1 : 3 @ 1 : 5 + +~ this.clientWorldController = new WorldClient(this, new WorldSettings(0L, packetIn.getGameType(), false, +~ packetIn.isHardcoreMode(), packetIn.getWorldType()), packetIn.getDimension(), packetIn.getDifficulty()); + +> INSERT 11 : 15 @ 11 + if (VoiceClientController.isClientSupported()) { -+ VoiceClientController.initializeVoiceClient((pkt) -> this.netManager -+ .sendPacket(new C17PacketCustomPayload(VoiceClientController.SIGNAL_CHANNEL, pkt))); ++ VoiceClientController.initializeVoiceClient(this::sendEaglerMessage, eaglerMessageController.protocol.ver); + } ++ WebViewOverlayController.setPacketSendCallback(this::webViewSendHandler); > DELETE 3 @ 3 : 4 @@ -191,7 +246,11 @@ > DELETE 5 @ 5 : 6 -> DELETE 17 @ 17 : 18 +> CHANGE 5 : 6 @ 5 : 6 + +~ packetIn.getDimensionID(), packetIn.getDifficulty()); + +> DELETE 11 @ 11 : 12 > DELETE 9 @ 9 : 10 @@ -205,7 +264,11 @@ > DELETE 11 @ 11 : 12 -> DELETE 22 @ 22 : 23 +> INSERT 8 : 9 @ 8 + ++ tileentitysign.clearProfanityFilterCache(); + +> DELETE 14 @ 14 : 15 > DELETE 16 @ 16 : 17 @@ -262,12 +325,13 @@ ~ for (int i = 0, l = lst.size(); i < l; ++i) { ~ S38PacketPlayerListItem.AddPlayerData s38packetplayerlistitem$addplayerdata = lst.get(i); -> CHANGE 1 : 5 @ 1 : 2 +> CHANGE 1 : 6 @ 1 : 2 ~ EaglercraftUUID uuid = s38packetplayerlistitem$addplayerdata.getProfile().getId(); ~ this.playerInfoMap.remove(uuid); ~ this.skinCache.evictSkin(uuid); ~ this.capeCache.evictCape(uuid); +~ ClientUUIDLoadingCache.evict(uuid); > DELETE 34 @ 34 : 35 @@ -357,42 +421,16 @@ > DELETE 11 @ 11 : 13 -> INSERT 9 : 43 @ 9 +> INSERT 9 : 17 @ 9 -+ } else if ("EAG|Skins-1.8".equals(packetIn.getChannelName())) { ++ } else { + try { -+ SkinPackets.readPluginMessage(packetIn.getBufferData(), skinCache); ++ eaglerMessageController.handlePacket(packetIn.getChannelName(), packetIn.getBufferData()); + } catch (IOException e) { -+ logger.error("Couldn't read EAG|Skins-1.8 packet!"); ++ logger.error("Couldn't read \"{}\" packet as an eaglercraft plugin message!", ++ packetIn.getChannelName()); + logger.error(e); + } -+ } else if ("EAG|Capes-1.8".equals(packetIn.getChannelName())) { -+ try { -+ CapePackets.readPluginMessage(packetIn.getBufferData(), capeCache); -+ } catch (IOException e) { -+ logger.error("Couldn't read EAG|Capes-1.8 packet!"); -+ logger.error(e); -+ } -+ } else if ("EAG|UpdateCert-1.8".equals(packetIn.getChannelName())) { -+ if (EagRuntime.getConfiguration().allowUpdateSvc()) { -+ try { -+ PacketBuffer pb = packetIn.getBufferData(); -+ byte[] c = new byte[pb.readableBytes()]; -+ pb.readBytes(c); -+ UpdateService.addCertificateToSet(c); -+ } catch (Throwable e) { -+ logger.error("Couldn't process EAG|UpdateCert-1.8 packet!"); -+ logger.error(e); -+ } -+ } -+ } else if (VoiceClientController.SIGNAL_CHANNEL.equals(packetIn.getChannelName())) { -+ if (VoiceClientController.isClientSupported()) { -+ VoiceClientController.handleVoiceSignalPacket(packetIn.getBufferData()); -+ } -+ } else if ("EAG|FNAWSEn-1.8".equals(packetIn.getChannelName())) { -+ this.currentFNAWSkinAllowedState = packetIn.getBufferData().readBoolean(); -+ Minecraft.getMinecraft().getRenderManager().setEnableFNAWSkins( -+ this.currentFNAWSkinAllowedState && Minecraft.getMinecraft().gameSettings.enableFNAWSkins); > DELETE 1 @ 1 : 2 diff --git a/patches/minecraft/net/minecraft/client/network/NetworkPlayerInfo.edit.java b/patches/minecraft/net/minecraft/client/network/NetworkPlayerInfo.edit.java index 41f67141..0d3b9611 100644 --- a/patches/minecraft/net/minecraft/client/network/NetworkPlayerInfo.edit.java +++ b/patches/minecraft/net/minecraft/client/network/NetworkPlayerInfo.edit.java @@ -5,16 +5,29 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 4 @ 2 : 6 +> CHANGE 2 : 5 @ 2 : 6 ~ import net.lax1dude.eaglercraft.v1_8.mojang.authlib.GameProfile; +~ import net.lax1dude.eaglercraft.v1_8.profanity_filter.ProfanityFilter; ~ import net.lax1dude.eaglercraft.v1_8.profile.SkinModel; > DELETE 1 @ 1 : 3 -> DELETE 10 @ 10 : 13 +> INSERT 8 : 9 @ 8 -> CHANGE 40 : 41 @ 40 : 41 ++ private String gameProfileProfanityFilter; + +> DELETE 2 @ 2 : 5 + +> INSERT 2 : 3 @ 2 + ++ private IChatComponent displayNameProfanityFilter; + +> INSERT 15 : 16 @ 15 + ++ this.displayNameProfanityFilter = null; + +> CHANGE 23 : 24 @ 23 : 24 ~ return true; @@ -41,4 +54,39 @@ > DELETE 6 @ 6 : 33 +> INSERT 2 : 3 @ 2 + ++ this.displayNameProfanityFilter = null; + +> INSERT 6 : 34 @ 6 + ++ public IChatComponent getDisplayNameProfanityFilter() { ++ if (Minecraft.getMinecraft().isEnableProfanityFilter()) { ++ if (this.displayName != null) { ++ if (this.displayNameProfanityFilter == null) { ++ this.displayNameProfanityFilter = ProfanityFilter.getInstance() ++ .profanityFilterChatComponent(this.displayName); ++ } ++ return this.displayNameProfanityFilter; ++ } else { ++ return null; ++ } ++ } else { ++ return this.displayName; ++ } ++ } ++ ++ public String getGameProfileNameProfanityFilter() { ++ if (Minecraft.getMinecraft().isEnableProfanityFilter()) { ++ if (this.gameProfileProfanityFilter == null) { ++ this.gameProfileProfanityFilter = ProfanityFilter.getInstance() ++ .profanityFilterString(this.gameProfile.getName()); ++ } ++ return this.gameProfileProfanityFilter; ++ } else { ++ return this.gameProfile.getName(); ++ } ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/client/particle/EffectRenderer.edit.java b/patches/minecraft/net/minecraft/client/particle/EffectRenderer.edit.java index c81df64b..606886fc 100644 --- a/patches/minecraft/net/minecraft/client/particle/EffectRenderer.edit.java +++ b/patches/minecraft/net/minecraft/client/particle/EffectRenderer.edit.java @@ -14,12 +14,13 @@ ~ import net.lax1dude.eaglercraft.v1_8.minecraft.IAcceleratedParticleEngine; ~ -> INSERT 1 : 9 @ 1 +> INSERT 1 : 10 @ 1 + + import com.google.common.collect.Lists; + import com.google.common.collect.Maps; + ++ import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; + import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; + import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.DeferredStateManager; @@ -43,10 +44,15 @@ > INSERT 2 : 5 @ 2 + public static final AcceleratedEffectRenderer vanillaAcceleratedParticleRenderer = new AcceleratedEffectRenderer(); -+ public IAcceleratedParticleEngine acceleratedParticleRenderer = vanillaAcceleratedParticleRenderer; ++ public IAcceleratedParticleEngine acceleratedParticleRenderer = null; + -> CHANGE 104 : 106 @ 104 : 105 +> INSERT 13 : 15 @ 13 + ++ this.acceleratedParticleRenderer = EaglercraftGPU.checkInstancingCapable() ? vanillaAcceleratedParticleRenderer ++ : null; + +> CHANGE 91 : 93 @ 91 : 92 ~ for (int i = 0, l = this.particleEmitters.size(); i < l; ++i) { ~ EntityParticleEmitter entityparticleemitter = this.particleEmitters.get(i); @@ -117,17 +123,20 @@ + texCoordWidth = 1.0f / blockMap.getWidth(); + texCoordHeight = 1.0f / blockMap.getHeight(); -> INSERT 7 : 11 @ 7 +> INSERT 7 : 13 @ 7 + boolean legacyRenderingHasOccured = false; + -+ acceleratedParticleRenderer.begin(partialTicks); ++ if (acceleratedParticleRenderer != null) { ++ acceleratedParticleRenderer.begin(partialTicks); ++ } + -> CHANGE 4 : 9 @ 4 : 5 +> CHANGE 4 : 10 @ 4 : 5 -~ if (!entityfx.renderAccelerated(acceleratedParticleRenderer, entityIn, partialTicks, f, f4, -~ f1, f2, f3)) { +~ if (acceleratedParticleRenderer == null +~ || !entityfx.renderAccelerated(acceleratedParticleRenderer, entityIn, partialTicks, +~ f, f4, f1, f2, f3)) { ~ entityfx.renderParticle(worldrenderer, entityIn, partialTicks, f, f4, f1, f2, f3); ~ legacyRenderingHasOccured = true; ~ } @@ -142,7 +151,7 @@ ~ : (l == 1 ? "TERRAIN_TEXTURE" ~ : (l == 3 ? "ENTITY_PARTICLE_TEXTURE" : "Unknown - " + l)); -> CHANGE 6 : 13 @ 6 : 7 +> CHANGE 6 : 15 @ 6 : 7 ~ if (legacyRenderingHasOccured) { ~ tessellator.draw(); @@ -150,6 +159,8 @@ ~ worldrenderer.finishDrawing(); ~ } ~ -~ acceleratedParticleRenderer.draw(texCoordWidth, texCoordHeight); +~ if (acceleratedParticleRenderer != null) { +~ acceleratedParticleRenderer.draw(texCoordWidth, texCoordHeight); +~ } > EOF diff --git a/patches/minecraft/net/minecraft/client/renderer/EntityRenderer.edit.java b/patches/minecraft/net/minecraft/client/renderer/EntityRenderer.edit.java index 309af9a3..c1c54ac6 100644 --- a/patches/minecraft/net/minecraft/client/renderer/EntityRenderer.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/EntityRenderer.edit.java @@ -11,10 +11,12 @@ ~ ~ import java.util.Arrays; -> CHANGE 1 : 3 @ 1 : 2 +> CHANGE 1 : 5 @ 1 : 2 ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; ~ import net.lax1dude.eaglercraft.v1_8.HString; +~ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; +~ > INSERT 1 : 29 @ 1 @@ -23,7 +25,7 @@ + import com.google.common.base.Predicates; + + import net.lax1dude.eaglercraft.v1_8.Display; -+ import net.lax1dude.eaglercraft.v1_8.Mouse; ++ import net.lax1dude.eaglercraft.v1_8.EagRuntime; + import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; + import net.lax1dude.eaglercraft.v1_8.log4j.Logger; + import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; @@ -47,7 +49,11 @@ + import net.lax1dude.eaglercraft.v1_8.voice.VoiceTagRenderer; + import net.lax1dude.eaglercraft.v1_8.vector.Matrix4f; -> CHANGE 10 : 13 @ 10 : 20 +> INSERT 6 : 7 @ 6 + ++ import net.minecraft.client.gui.GuiScreen; + +> CHANGE 4 : 7 @ 4 : 14 ~ import net.minecraft.client.particle.EntityFX; ~ import net.minecraft.client.renderer.RenderGlobal.ChunkCullAdapter; @@ -137,7 +143,11 @@ > DELETE 1 @ 1 : 8 -> CHANGE 111 : 112 @ 111 : 112 +> DELETE 6 @ 6 : 7 + +> DELETE 79 @ 79 : 81 + +> CHANGE 23 : 24 @ 23 : 24 ~ public float getFOVModifier(float partialTicks, boolean parFlag) { @@ -179,7 +189,9 @@ + } + -> CHANGE 117 : 118 @ 117 : 118 +> DELETE 10 @ 10 : 11 + +> CHANGE 106 : 107 @ 106 : 107 ~ this.lightmapColors[i] = short1 << 24 | j | k << 8 | l << 16; @@ -201,17 +213,45 @@ + GlStateManager.setActiveTexture(OpenGlHelper.defaultTexUnit); + -> DELETE 23 @ 23 : 28 +> DELETE 1 @ 1 : 2 -> INSERT 4 : 7 @ 4 +> CHANGE 10 : 11 @ 10 : 11 + +~ boolean flag = Display.isActive() || mc.gameSettings.touchscreen; + +> CHANGE 1 : 2 @ 1 : 2 + +~ && (!this.mc.gameSettings.touchscreen || !PointerInputAbstraction.getVCursorButtonDown(1))) { + +> DELETE 7 @ 7 : 14 + +> INSERT 3 : 6 @ 3 + if (this.mc.gameSettings.keyBindZoomCamera.isKeyDown()) { + f *= 0.7f; + } -> DELETE 39 @ 39 : 52 +> DELETE 23 @ 23 : 24 -> CHANGE 4 : 45 @ 4 : 5 +> CHANGE 2 : 3 @ 2 : 3 + +~ final ScaledResolution scaledresolution = mc.scaledResolution; + +> CHANGE 2 : 4 @ 2 : 4 + +~ final int j1 = PointerInputAbstraction.getVCursorX() * l / this.mc.displayWidth; +~ final int k1 = i1 - PointerInputAbstraction.getVCursorY() * i1 / this.mc.displayHeight - 1; + +> DELETE 2 @ 2 : 3 + +> DELETE 5 @ 5 : 18 + +> CHANGE 1 : 3 @ 1 : 3 + +~ final boolean b = !this.mc.gameSettings.hideGUI || this.mc.currentScreen != null; +~ if (b) { + +> CHANGE 1 : 14 @ 1 : 2 ~ long framebufferAge = this.overlayFramebuffer.getAge(); ~ if (framebufferAge == -1l || framebufferAge > (Minecraft.getDebugFPS() < 25 ? 125l : 75l)) { @@ -220,10 +260,16 @@ ~ GlStateManager.clearColor(0.0f, 0.0f, 0.0f, 0.0f); ~ GlStateManager.clear(16640); ~ GlStateManager.enableOverlayFramebufferBlending(); -~ this.mc.ingameGUI.renderGameOverlay(parFloat1); +~ if (b) { +~ this.mc.ingameGUI.renderGameOverlay(parFloat1); +~ } ~ GlStateManager.disableOverlayFramebufferBlending(); ~ this.overlayFramebuffer.endRender(); ~ } + +> CHANGE 1 : 32 @ 1 : 3 + +~ if (b) { ~ this.setupOverlayRendering(); ~ GlStateManager.disableLighting(); ~ GlStateManager.enableBlend(); @@ -235,7 +281,7 @@ ~ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); ~ GlStateManager.enableBlend(); ~ GlStateManager.blendFunc(1, 771); -~ GlStateManager.disableAlpha(); +~ GlStateManager.enableAlpha(); ~ GlStateManager.disableDepth(); ~ GlStateManager.depthMask(false); ~ Tessellator tessellator = Tessellator.getInstance(); @@ -248,14 +294,45 @@ ~ tessellator.draw(); ~ GlStateManager.depthMask(true); ~ GlStateManager.enableDepth(); -~ GlStateManager.enableAlpha(); ~ GlStateManager.disableBlend(); ~ if (this.mc.gameSettings.hudPlayer) { // give the player model HUD good fps ~ this.mc.ingameGUI.drawEaglerPlayerOverlay(l - 3, ~ 3 + this.mc.ingameGUI.overlayDebug.playerOffset, parFloat1); ~ } +~ } -> CHANGE 23 : 24 @ 23 : 24 +> INSERT 10 : 12 @ 10 + ++ this.mc.notifRenderer.renderOverlay(j1, k1); ++ + +> INSERT 2 : 4 @ 2 + ++ float f = 1.0f; ++ final float[] ff = new float[] { 1.0f }; + +> CHANGE 2 : 20 @ 2 : 3 + +~ f = mc.currentScreen.getEaglerScale(); +~ int mx, my; +~ if (f == 1.0f) { +~ mx = j1; +~ my = k1; +~ } else { +~ mx = GuiScreen.applyEaglerScale(f, j1, l); +~ my = GuiScreen.applyEaglerScale(f, k1, i1); +~ GlStateManager.pushMatrix(); +~ float fff = (1.0f - f) * 0.5f; +~ GlStateManager.translate(fff * l, fff * i1, 0.0f); +~ GlStateManager.scale(f, f, f); +~ } +~ ff[0] = f; +~ this.mc.currentScreen.drawScreen(mx, my, parFloat1); +~ if (f != 1.0f) { +~ GlStateManager.popMatrix(); +~ } + +> CHANGE 5 : 6 @ 5 : 6 ~ return EntityRenderer.this.mc.currentScreen.getClass().getName(); @@ -263,16 +340,31 @@ ~ return HString.format("Scaled: (%d, %d). Absolute: (%d, %d)", -> CHANGE 6 : 7 @ 6 : 7 +> CHANGE 1 : 3 @ 1 : 2 + +~ Integer.valueOf(PointerInputAbstraction.getVCursorX()), +~ Integer.valueOf(PointerInputAbstraction.getVCursorY()) }); + +> CHANGE 4 : 5 @ 4 : 5 ~ return HString.format("Scaled: (%d, %d). Absolute: (%d, %d). Scale factor of %d", -> INSERT 9 : 11 @ 9 +> INSERT 7 : 12 @ 7 + ++ crashreportcategory.addCrashSectionCallable("Eagler Scale", new Callable() { ++ public String call() throws Exception { ++ return "" + ff[0]; ++ } ++ }); + +> DELETE 2 @ 2 : 3 + +> INSERT 1 : 3 @ 1 -+ + this.mc.voiceOverlay.drawOverlay(); ++ } -> DELETE 6 @ 6 : 8 +> DELETE 4 @ 4 : 6 > CHANGE 32 : 33 @ 32 : 33 @@ -291,7 +383,7 @@ + VoiceTagRenderer.clearTagsDrawnSet(); + -> CHANGE 4 : 25 @ 4 : 12 +> CHANGE 3 : 24 @ 3 : 12 ~ boolean dlights = DynamicLightsStateManager.isDynamicLightsRender(); ~ if (dlights) { @@ -315,7 +407,7 @@ ~ } ~ } -> CHANGE 1 : 26 @ 1 : 2 +> CHANGE 1 : 28 @ 1 : 2 ~ if (this.mc.gameSettings.shaders) { ~ try { @@ -330,7 +422,9 @@ ~ } ~ mc.effectRenderer.acceleratedParticleRenderer = EffectRenderer.vanillaAcceleratedParticleRenderer; ~ } else { -~ mc.effectRenderer.acceleratedParticleRenderer = EffectRenderer.vanillaAcceleratedParticleRenderer; +~ mc.effectRenderer.acceleratedParticleRenderer = EaglercraftGPU.checkInstancingCapable() +~ ? EffectRenderer.vanillaAcceleratedParticleRenderer +~ : null; ~ if (dlights) { ~ GlStateManager.enableExtensionPipeline(); ~ } @@ -343,29 +437,34 @@ ~ } ~ } -> INSERT 2 : 6 @ 2 +> CHANGE 2 : 5 @ 2 : 3 -+ if (fxaa) { -+ EffectPipelineFXAA.end(); -+ } -+ +~ if (fxaa) { +~ EffectPipelineFXAA.end(); +~ } -> INSERT 14 : 18 @ 14 +> DELETE 7 @ 7 : 8 + +> DELETE 3 @ 3 : 4 + +> INSERT 1 : 5 @ 1 + boolean isDynamicLights = DynamicLightsStateManager.isDynamicLightsRender(); + if (isDynamicLights) { + DynamicLightsStateManager.setupInverseViewMatrix(); + } -> DELETE 1 @ 1 : 3 +> DELETE 1 @ 1 : 4 -> INSERT 6 : 9 @ 6 +> INSERT 5 : 8 @ 5 + TileEntityRendererDispatcher.staticPlayerX = d0; // hack, needed for some eagler stuff + TileEntityRendererDispatcher.staticPlayerY = d1; + TileEntityRendererDispatcher.staticPlayerZ = d2; -> CHANGE 6 : 9 @ 6 : 8 +> DELETE 3 @ 3 : 4 + +> CHANGE 2 : 5 @ 2 : 4 ~ float vigg = this.getFOVModifier(partialTicks, true); ~ GlStateManager.gluPerspective(vigg, (float) this.mc.displayWidth / (float) this.mc.displayHeight, 0.05F, @@ -375,7 +474,15 @@ ~ GlStateManager.gluPerspective(vigg, (float) this.mc.displayWidth / (float) this.mc.displayHeight, 0.05F, -> INSERT 26 : 27 @ 26 +> DELETE 10 @ 10 : 11 + +> DELETE 3 @ 3 : 4 + +> DELETE 3 @ 3 : 4 + +> DELETE 3 @ 3 : 4 + +> INSERT 3 : 4 @ 3 + GlStateManager.disableBlend(); @@ -385,11 +492,13 @@ + GlStateManager.shadeModel(7424); -> INSERT 16 : 19 @ 16 +> DELETE 5 @ 5 : 6 -+ if (isDynamicLights) { -+ GlStateManager.disableExtensionPipeline(); -+ } +> CHANGE 9 : 12 @ 9 : 10 + +~ if (isDynamicLights) { +~ GlStateManager.disableExtensionPipeline(); +~ } > INSERT 2 : 5 @ 2 @@ -397,19 +506,23 @@ + GlStateManager.enableExtensionPipeline(); + } -> INSERT 8 : 11 @ 8 +> CHANGE 8 : 11 @ 8 : 9 -+ if (isDynamicLights) { -+ GlStateManager.disableExtensionPipeline(); -+ } +~ if (isDynamicLights) { +~ GlStateManager.disableExtensionPipeline(); +~ } -> INSERT 3 : 6 @ 3 +> INSERT 2 : 5 @ 2 + if (isDynamicLights) { + GlStateManager.enableExtensionPipeline(); + } -> CHANGE 17 : 25 @ 17 : 18 +> DELETE 2 @ 2 : 3 + +> DELETE 9 @ 9 : 10 + +> CHANGE 3 : 11 @ 3 : 5 ~ if (isDynamicLights) { ~ DynamicLightsStateManager.bindAcceleratedEffectRenderer(effectrenderer); @@ -420,7 +533,15 @@ ~ effectrenderer.acceleratedParticleRenderer = null; ~ } -> INSERT 39 : 53 @ 39 +> DELETE 5 @ 5 : 6 + +> DELETE 12 @ 12 : 13 + +> DELETE 7 @ 7 : 8 + +> DELETE 3 @ 3 : 4 + +> INSERT 8 : 22 @ 8 + private void updateDynamicLightListEagler(float partialTicks) { + DynamicLightsStateManager.clearRenderList(); @@ -437,7 +558,9 @@ + } + -> CHANGE 5 : 6 @ 5 : 6 +> DELETE 2 @ 2 : 3 + +> CHANGE 2 : 3 @ 2 : 3 ~ GlStateManager.gluPerspective(this.getFOVModifier(partialTicks, true), @@ -523,7 +646,11 @@ ~ EaglerDeferredPipeline.instance.setForwardRenderLightFactors(1.0f, 1.0f, 1.0f, 1.0f); ~ } -> CHANGE 153 : 154 @ 153 : 154 +> CHANGE 6 : 7 @ 6 : 7 + +~ ScaledResolution scaledresolution = mc.scaledResolution; + +> CHANGE 146 : 147 @ 146 : 147 ~ GlStateManager.clearColor(this.fogColorRed, this.fogColorGreen, this.fogColorBlue, 1.0F); @@ -539,7 +666,7 @@ > INSERT 14 : 17 @ 14 -+ } else if (!this.mc.gameSettings.fog) { ++ } else if (partialTicks != -1 && !this.mc.gameSettings.fog) { + GlStateManager.setFog(2048); + GlStateManager.setFogDensity(0.0F); @@ -553,7 +680,7 @@ > DELETE 9 @ 9 : 10 -> INSERT 12 : 988 @ 12 +> INSERT 12 : 952 @ 12 + + private static final Vector4f tmpVec4f_1 = new Vector4f(); @@ -570,28 +697,23 @@ + EaglerDeferredPipeline.renderSuspended(); + return; + } -+ mc.mcProfiler.endStartSection("eaglercraftShaders"); + EaglerDeferredPipeline.instance.setPartialTicks(partialTicks); + eagPartialTicks = partialTicks; + EaglerDeferredConfig conf = mc.gameSettings.deferredShaderConf; + boolean flag = isDrawBlockOutline(); + GlStateManager.viewport(0, 0, mc.displayWidth, mc.displayHeight); -+ mc.mcProfiler.startSection("camera"); + setupCameraTransform(partialTicks, 2); + EaglerDeferredPipeline.instance.loadViewMatrix(); + ActiveRenderInfo.updateRenderInfo(mc.thePlayer, mc.gameSettings.thirdPersonView == 2); -+ mc.mcProfiler.endStartSection("culling"); + Frustum frustum = new Frustum(); + Entity entity = mc.getRenderViewEntity(); + if (entity == null) { + entity = mc.thePlayer; + } -+ double d0 = EaglerDeferredPipeline.instance.currentRenderX = entity.lastTickPosX -+ + (entity.posX - entity.lastTickPosX) * (double) partialTicks; -+ double d1 = EaglerDeferredPipeline.instance.currentRenderY = entity.lastTickPosY -+ + (entity.posY - entity.lastTickPosY) * (double) partialTicks; -+ double d2 = EaglerDeferredPipeline.instance.currentRenderZ = entity.lastTickPosZ -+ + (entity.posZ - entity.lastTickPosZ) * (double) partialTicks; ++ double d0 = entity.lastTickPosX + (entity.posX - entity.lastTickPosX) * (double) partialTicks; ++ double d1 = entity.lastTickPosY + (entity.posY - entity.lastTickPosY) * (double) partialTicks; ++ double d2 = entity.lastTickPosZ + (entity.posZ - entity.lastTickPosZ) * (double) partialTicks; ++ EaglerDeferredPipeline.instance.setRenderPosGlobal(d0, d1, d2); + EaglerDeferredPipeline.instance.updateReprojectionCoordinates(d0, d1, d2); + float eyeHeight = entity.getEyeHeight(); + frustum.setPosition(d0, d1, d2); @@ -603,7 +725,7 @@ + // } + // System.out.println(builder.toString()); + -+ float waveTimer = (float) ((System.currentTimeMillis() % 600000l) * 0.001); ++ float waveTimer = (float) ((EagRuntime.steadyTimeMillis() % 600000l) * 0.001); + DeferredStateManager.setWaterWindOffset(0.0f, 0.0f, waveTimer, waveTimer); + + float blockWaveDistX = (float) (d0 - blockWaveOffsetX); @@ -628,7 +750,6 @@ + + // if (mc.gameSettings.renderDistanceChunks >= 4) vanilla shows sky not fog + -+ mc.mcProfiler.endStartSection("terrain_setup"); + mc.renderGlobal.setupTerrain(entity, (double) partialTicks, frustum, frameCount++, mc.thePlayer.isSpectator()); + + // clear some state: @@ -646,11 +767,8 @@ + + EaglerDeferredPipeline.instance.beginDrawMainGBufferTerrain(); + -+ mc.mcProfiler.endStartSection("updatechunks"); + mc.renderGlobal.updateChunks(finishTimeNano); + -+ mc.mcProfiler.endStartSection("terrain"); -+ + mc.renderGlobal.renderBlockLayer(EnumWorldBlockLayer.SOLID, (double) partialTicks, 2, entity); + GlStateManager.enableAlpha(); + GlStateManager.alphaFunc(516, 0.5F); @@ -679,20 +797,17 @@ + NameTagRenderer.doRenderNameTags = true; + NameTagRenderer.nameTagsCount = 0; + GlStateManager.pushMatrix(); -+ mc.mcProfiler.endStartSection("entities"); + DeferredStateManager.setDefaultMaterialConstants(); + DeferredStateManager.startUsingEnvMap(); + mc.renderGlobal.renderEntities(entity, frustum, partialTicks); + GlStateManager.matrixMode(5888); + GlStateManager.popMatrix(); -+ mc.mcProfiler.endStartSection("litParticles"); + EntityFX.interpPosX = d0; + EntityFX.interpPosY = d1; + EntityFX.interpPosZ = d2; + enableLightmap(); + GlStateManager.pushMatrix(); + mc.effectRenderer.renderLitParticles(entity, partialTicks); -+ mc.mcProfiler.endStartSection("gbufferParticles"); + GlStateManager.matrixMode(5888); + GlStateManager.popMatrix(); + GlStateManager.pushMatrix(); @@ -706,9 +821,7 @@ + DynamicLightManager.setIsRenderingLights(false); + NameTagRenderer.doRenderNameTags = false; + -+ mc.mcProfiler.endStartSection("endDrawMainGBuffer"); + EaglerDeferredPipeline.instance.endDrawMainGBuffer(); -+ mc.mcProfiler.endStartSection("shadowSetup"); + + // calculate sun matrix and angle: + @@ -1113,11 +1226,9 @@ + } + } + -+ mc.mcProfiler.endStartSection("combineGBuffersAndIlluminate"); + EaglerDeferredPipeline.instance.combineGBuffersAndIlluminate(); + + if (conf.is_rendering_useEnvMap) { -+ mc.mcProfiler.endStartSection("envMap"); + DeferredStateManager.forwardCallbackHandler = null; + EaglerDeferredPipeline.instance.beginDrawEnvMap(); + GlStateManager.enableCull(); @@ -1188,7 +1299,6 @@ + } + + if (conf.is_rendering_realisticWater) { -+ mc.mcProfiler.endStartSection("realisticWaterMask"); + EaglerDeferredPipeline.instance.beginDrawRealisticWaterMask(); + enableLightmap(); + mc.renderGlobal.renderBlockLayer(EnumWorldBlockLayer.REALISTIC_WATER, (double) partialTicks, 2, entity); @@ -1196,8 +1306,6 @@ + EaglerDeferredPipeline.instance.endDrawRealisticWaterMask(); + } + -+ mc.mcProfiler.endStartSection("setupShaderFog"); -+ + int dim = mc.theWorld.provider.getDimensionId(); + float ff; + if (dim == 0) { @@ -1262,16 +1370,13 @@ + DeferredStateManager.setDefaultMaterialConstants(); + + if (conf.is_rendering_realisticWater) { -+ mc.mcProfiler.endStartSection("realisticWaterSurface"); + EaglerDeferredPipeline.instance.beginDrawRealisticWaterSurface(); + mc.renderGlobal.renderBlockLayer(EnumWorldBlockLayer.REALISTIC_WATER, (double) partialTicks, 2, entity); + EaglerDeferredPipeline.instance.endDrawRealisticWaterSurface(); + } + -+ mc.mcProfiler.endStartSection("gbufferFog"); + EaglerDeferredPipeline.instance.applyGBufferFog(); + -+ mc.mcProfiler.endStartSection("translucentEntities"); + EaglerDeferredPipeline.instance.beginDrawTranslucentEntities(); + + TileEntityRendererDispatcher.staticPlayerX = d0; @@ -1295,14 +1400,11 @@ + DeferredStateManager.forwardCallbackGBuffer.reset(); + + EaglerDeferredPipeline.instance.beginDrawTranslucentBlocks(); -+ mc.mcProfiler.endStartSection("translucentBlocks"); + mc.getTextureManager().bindTexture(TextureMap.locationBlocksTexture); + mc.renderGlobal.renderBlockLayer(EnumWorldBlockLayer.TRANSLUCENT, (double) partialTicks, 2, entity); + + EaglerDeferredPipeline.instance.beginDrawMainGBufferDestroyProgress(); + -+ mc.mcProfiler.endStartSection("destroyProgress"); -+ + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(0, 770, 0, 0); + GlStateManager.color(1.0f, 1.0f, 1.0f, 0.5f); @@ -1314,7 +1416,6 @@ + EaglerDeferredPipeline.instance.endDrawMainGBufferDestroyProgress(); + + if (mc.effectRenderer.hasParticlesInAlphaLayer()) { -+ mc.mcProfiler.endStartSection("transparentParticles"); + GlStateManager.pushMatrix(); + mc.effectRenderer.acceleratedParticleRenderer = EaglerDeferredPipeline.instance.forwardEffectRenderer; + DeferredStateManager.setHDRTranslucentPassBlendFunc(); @@ -1330,22 +1431,18 @@ + } + + if (conf.is_rendering_useEnvMap) { -+ mc.mcProfiler.endStartSection("glassHighlights"); + EaglerDeferredPipeline.instance.beginDrawGlassHighlights(); + mc.renderGlobal.renderBlockLayer(EnumWorldBlockLayer.GLASS_HIGHLIGHTS, (double) partialTicks, 2, entity); + EaglerDeferredPipeline.instance.endDrawGlassHighlights(); + } + -+ mc.mcProfiler.endStartSection("saveReprojData"); + EaglerDeferredPipeline.instance.saveReprojData(); + -+ mc.mcProfiler.endStartSection("rainSnow"); + renderRainSnow(partialTicks); + + GlStateManager.disableBlend(); + + if (renderHand) { -+ mc.mcProfiler.endStartSection("renderHandOverlay"); + EaglerDeferredPipeline.instance.beginDrawHandOverlay(); + DeferredStateManager.reportForwardRenderObjectPosition2(0.0f, 0.0f, 0.0f); + DeferredStateManager.forwardCallbackHandler = DeferredStateManager.forwardCallbackGBuffer; @@ -1371,7 +1468,6 @@ + GlStateManager.disableAlpha(); + } + -+ mc.mcProfiler.endStartSection("endDrawDeferred"); + EaglerDeferredPipeline.instance.endDrawHDRTranslucent(); + + EaglerDeferredPipeline.instance.endDrawDeferred(); @@ -1391,11 +1487,9 @@ + if (!DebugFramebufferView.debugViewShown) { + GlStateManager.disableAlpha(); + if (isDrawBlockOutline()) { -+ this.mc.mcProfiler.endStartSection("outline"); + mc.renderGlobal.drawSelectionBox(mc.thePlayer, this.mc.objectMouseOver, 0, partialTicks); + } + GlStateManager.enableAlpha(); -+ this.mc.mcProfiler.endStartSection("nameTags"); + if (NameTagRenderer.nameTagsCount > 0) { + enableLightmap(); + Arrays.sort(NameTagRenderer.nameTagsThisFrame, 0, NameTagRenderer.nameTagsCount, (n1, n2) -> { @@ -1422,11 +1516,8 @@ + } + disableLightmap(); + GlStateManager.disableLighting(); -+ this.mc.mcProfiler.endStartSection("worldBorder"); + mc.renderGlobal.renderWorldBorder(entity, partialTicks); + } -+ -+ mc.mcProfiler.endSection(); + } + + public boolean renderHeldItemLight(EntityLivingBase entityLiving, float mag) { diff --git a/patches/minecraft/net/minecraft/client/renderer/RenderGlobal.edit.java b/patches/minecraft/net/minecraft/client/renderer/RenderGlobal.edit.java index 7fc3db90..7aaa170e 100644 --- a/patches/minecraft/net/minecraft/client/renderer/RenderGlobal.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/RenderGlobal.edit.java @@ -7,8 +7,10 @@ > DELETE 2 @ 2 : 7 -> CHANGE 6 : 10 @ 6 : 7 +> CHANGE 6 : 12 @ 6 : 7 +~ +~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; ~ import net.lax1dude.eaglercraft.v1_8.HString; ~ import net.lax1dude.eaglercraft.v1_8.Keyboard; @@ -220,7 +222,11 @@ + boolean light = DynamicLightManager.isRenderingLights(); -> CHANGE 27 : 36 @ 27 : 54 +> DELETE 6 @ 6 : 7 + +> DELETE 16 @ 16 : 17 + +> CHANGE 3 : 12 @ 3 : 30 ~ if (!DeferredStateManager.isDeferredRenderer()) { ~ for (int i = 0; i < this.theWorld.weatherEffects.size(); ++i) { @@ -234,7 +240,7 @@ > DELETE 2 @ 2 : 16 -> CHANGE 4 : 7 @ 4 : 5 +> CHANGE 2 : 5 @ 2 : 5 ~ label738: for (int ii = 0, ll = this.renderInfos.size(); ii < ll; ++ii) { ~ RenderGlobal.ContainerLocalRenderInformation renderglobal$containerlocalrenderinformation = this.renderInfos @@ -246,7 +252,9 @@ + entity2.renderDynamicLightsEagler(partialTicks, flag2); + } -> CHANGE 27 : 30 @ 27 : 28 +> DELETE 24 @ 24 : 25 + +> CHANGE 2 : 5 @ 2 : 3 ~ for (int ii = 0, ll = this.renderInfos.size(); ii < ll; ++ii) { ~ RenderGlobal.ContainerLocalRenderInformation renderglobal$containerlocalrenderinformation1 = this.renderInfos @@ -258,7 +266,9 @@ ~ TileEntityRendererDispatcher.instance.renderTileEntity((TileEntity) list1.get(m), partialTicks, ~ -1); -> INSERT 40 : 199 @ 40 +> DELETE 36 @ 36 : 37 + +> INSERT 3 : 154 @ 3 + public static interface EntityChunkCullAdapter { + boolean shouldCull(RenderChunk renderChunk); @@ -271,8 +281,6 @@ + public void renderShadowLODEntities(Entity renderViewEntity, float partialTicks, + EntityChunkCullAdapter entityChunkCull, EntityObjectCullAdapter entityObjectCull) { // TODO + if (renderEntitiesStartupCounter <= 0) { -+ theWorld.theProfiler.startSection("shadow_entity_prepare"); -+ + TileEntityRendererDispatcher.instance.cacheActiveRenderInfo(theWorld, mc.getTextureManager(), + mc.fontRendererObj, renderViewEntity, partialTicks); + renderManager.cacheActiveRenderInfo(theWorld, mc.fontRendererObj, renderViewEntity, mc.pointedEntity, @@ -289,7 +297,6 @@ + TileEntityRendererDispatcher.staticPlayerZ = d5; + renderManager.setRenderPosition(d3, d4, d5); + -+ this.theWorld.theProfiler.endStartSection("shadow_entities"); + for (RenderGlobal.ContainerLocalRenderInformation containerlocalrenderinformation : this.renderInfos) { + RenderChunk currentRenderChunk = containerlocalrenderinformation.renderChunk; + @@ -343,14 +350,11 @@ + GlStateManager.depthMask(true); + } + } -+ theWorld.theProfiler.endSection(); + } + } + + public void renderParaboloidTileEntities(Entity renderViewEntity, float partialTicks, int up) { + if (renderEntitiesStartupCounter <= 0) { -+ theWorld.theProfiler.startSection("paraboloid_entity_prepare"); -+ + TileEntityRendererDispatcher.instance.cacheActiveRenderInfo(theWorld, mc.getTextureManager(), + mc.fontRendererObj, renderViewEntity, partialTicks); + renderManager.cacheActiveRenderInfo(theWorld, mc.fontRendererObj, renderViewEntity, mc.pointedEntity, @@ -391,7 +395,6 @@ + maxY = MathHelper.floor_double(maxY / 16.0) * 16; + maxZ = MathHelper.floor_double(maxZ / 16.0) * 16; + -+ this.theWorld.theProfiler.endStartSection("paraboloid_entities"); + for (int cx = minX; cx <= maxX; cx += 16) { + for (int cz = minZ; cz <= maxZ; cz += 16) { + for (int cy = minY; cy <= maxY; cy += 16) { @@ -414,7 +417,6 @@ + } + } + } -+ theWorld.theProfiler.endSection(); + mc.entityRenderer.disableLightmap(); + } + } @@ -430,7 +432,15 @@ ~ return HString.format("C: %d/%d %sD: %d, %s", -> CHANGE 53 : 55 @ 53 : 54 +> DELETE 15 @ 15 : 16 + +> DELETE 15 @ 15 : 16 + +> DELETE 4 @ 4 : 5 + +> DELETE 7 @ 7 : 8 + +> CHANGE 8 : 10 @ 8 : 9 ~ || (double) viewEntity.rotationYaw != this.lastViewEntityYaw ~ || this.mc.entityRenderer.currentProjMatrixFOV != this.lastViewProjMatrixFOV; @@ -457,12 +467,16 @@ ~ RenderGlobal.ContainerLocalRenderInformation renderglobal$containerlocalrenderinformation2 = this.renderInfos ~ .get(ii); -> CHANGE 3 : 5 @ 3 : 4 +> CHANGE 3 : 5 @ 3 : 5 ~ if (this.mc.gameSettings.chunkFix ? this.isPositionInRenderChunkHack(blockpos1, renderchunk4) ~ : this.isPositionInRenderChunk(blockpos, renderchunk4)) { -> INSERT 21 : 31 @ 21 +> DELETE 2 @ 2 : 3 + +> DELETE 7 @ 7 : 8 + +> INSERT 9 : 19 @ 9 + /** + * WARNING: use only in the above "build near" logic @@ -479,13 +493,23 @@ + ((ClippingHelperImpl) this.debugFixedClippingHelper).destroy(); -> CHANGE 58 : 61 @ 58 : 59 +> DELETE 48 @ 48 : 49 + +> CHANGE 9 : 12 @ 9 : 10 ~ for (int ii = 0, ll = this.renderInfos.size(); ii < ll; ++ii) { ~ RenderGlobal.ContainerLocalRenderInformation renderglobal$containerlocalrenderinformation = this.renderInfos ~ .get(ii); -> INSERT 33 : 70 @ 33 +> DELETE 7 @ 7 : 9 + +> DELETE 2 @ 2 : 3 + +> DELETE 15 @ 15 : 16 + +> DELETE 1 @ 1 : 2 + +> INSERT 3 : 39 @ 3 + public static interface ChunkCullAdapter { + boolean shouldCull(RenderChunk chunk); @@ -518,7 +542,6 @@ + } + } + if (i > 0) { -+ this.mc.mcProfiler.endStartSection("render_shadow_" + blockLayerIn); + this.renderContainer.renderChunkLayer(blockLayerIn); + } + return i; @@ -579,10 +602,9 @@ ~ ++i; ~ } -> CHANGE 3 : 10 @ 3 : 5 +> CHANGE 3 : 9 @ 3 : 5 ~ if (i > 0) { -~ this.mc.mcProfiler.endStartSection("render_paraboloid_" + up + "_" + blockLayerIn); ~ this.mc.entityRenderer.enableLightmap(); ~ this.renderContainer.renderChunkLayer(blockLayerIn); ~ this.mc.entityRenderer.disableLightmap(); @@ -609,7 +631,11 @@ ~ this.displayListEntitiesDirty |= this.renderDispatcher.updateChunks(finishTimeNano); -> DELETE 17 @ 17 : 18 +> CHANGE 11 : 12 @ 11 : 12 + +~ long i = finishTimeNano - EagRuntime.nanoTime(); + +> DELETE 5 @ 5 : 6 > CHANGE 155 : 159 @ 155 : 156 diff --git a/patches/minecraft/net/minecraft/client/renderer/entity/Render.edit.java b/patches/minecraft/net/minecraft/client/renderer/entity/Render.edit.java index 331e4262..6223d48a 100644 --- a/patches/minecraft/net/minecraft/client/renderer/entity/Render.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/entity/Render.edit.java @@ -35,7 +35,11 @@ + return true; + } -> INSERT 21 : 25 @ 21 +> CHANGE 17 : 18 @ 17 : 18 + +~ this.renderLivingLabel(entity, entity.getDisplayNameProfanityFilter().getFormattedText(), x, y, z, 64); + +> INSERT 3 : 7 @ 3 + public static void renderNameAdapter(Render r, Entity e, double x, double y, double z) { + r.renderName(e, x, y, z); diff --git a/patches/minecraft/net/minecraft/client/renderer/entity/RenderPlayer.edit.java b/patches/minecraft/net/minecraft/client/renderer/entity/RenderPlayer.edit.java index e02a7be6..21709fb5 100644 --- a/patches/minecraft/net/minecraft/client/renderer/entity/RenderPlayer.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/entity/RenderPlayer.edit.java @@ -62,7 +62,11 @@ ~ modelplayer_.bipedRightArmwear.showModel = clientPlayer.isWearing(EnumPlayerModelParts.RIGHT_SLEEVE); ~ } -> CHANGE 50 : 60 @ 50 : 58 +> CHANGE 41 : 42 @ 41 : 42 + +~ score.getScorePoints() + " " + scoreobjective.getDisplayNameProfanityFilter(), d0, d1, d2, 64); + +> CHANGE 8 : 18 @ 8 : 16 ~ if (!zombieModel) { ~ float f = 1.0F; diff --git a/patches/minecraft/net/minecraft/client/renderer/entity/RendererLivingEntity.edit.java b/patches/minecraft/net/minecraft/client/renderer/entity/RendererLivingEntity.edit.java index b60a41f5..455565d8 100644 --- a/patches/minecraft/net/minecraft/client/renderer/entity/RendererLivingEntity.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/entity/RendererLivingEntity.edit.java @@ -147,7 +147,11 @@ ~ for (int i = 0, l = this.layerRenderers.size(); i < l; ++i) { ~ LayerRenderer layerrenderer = this.layerRenderers.get(i); -> INSERT 30 : 34 @ 30 +> CHANGE 26 : 27 @ 26 : 27 + +~ String s = entitylivingbase.getDisplayNameProfanityFilter().getFormattedText(); + +> INSERT 3 : 7 @ 3 + if (DeferredStateManager.isInDeferredPass()) { + NameTagRenderer.renderNameTag(entitylivingbase, null, d0, d1, d2, -69); diff --git a/patches/minecraft/net/minecraft/client/renderer/texture/AbstractTexture.edit.java b/patches/minecraft/net/minecraft/client/renderer/texture/AbstractTexture.edit.java index 1c2f70a4..81fc2047 100644 --- a/patches/minecraft/net/minecraft/client/renderer/texture/AbstractTexture.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/texture/AbstractTexture.edit.java @@ -38,7 +38,7 @@ + hasAllocated = false; -> INSERT 12 : 26 @ 12 +> INSERT 12 : 28 @ 12 + + /** @@ -49,7 +49,9 @@ + protected void regenerateIfNotAllocated() { + if (this.glTextureId != -1) { + if (hasAllocated) { -+ EaglercraftGPU.regenerateTexture(glTextureId); ++ if (EaglercraftGPU.checkTexStorageCapable()) { ++ EaglercraftGPU.regenerateTexture(glTextureId); ++ } + } + hasAllocated = true; + } diff --git a/patches/minecraft/net/minecraft/client/renderer/texture/TextureMap.edit.java b/patches/minecraft/net/minecraft/client/renderer/texture/TextureMap.edit.java index 184c9f17..6c0afd2d 100644 --- a/patches/minecraft/net/minecraft/client/renderer/texture/TextureMap.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/texture/TextureMap.edit.java @@ -43,7 +43,7 @@ ~ private final Map mapRegisteredSprites; ~ private final Map mapUploadedSprites; -> CHANGE 3 : 10 @ 3 : 4 +> CHANGE 3 : 11 @ 3 : 4 ~ private final EaglerTextureAtlasSprite missingImage; ~ private final EaglerTextureAtlasSpritePBR missingImagePBR; @@ -52,6 +52,7 @@ ~ private boolean isEaglerPBRMode = false; ~ public int eaglerPBRMaterialTexture = -1; ~ private boolean hasAllocatedEaglerPBRMaterialTexture = false; +~ private boolean isGLES2 = false; > INSERT 1 : 7 @ 1 @@ -67,7 +68,11 @@ ~ this.missingImage = new EaglerTextureAtlasSprite("missingno"); ~ this.missingImagePBR = new EaglerTextureAtlasSpritePBR("missingno"); -> INSERT 11 : 27 @ 11 +> INSERT 2 : 3 @ 2 + ++ this.isGLES2 = EaglercraftGPU.checkOpenGLESVersion() == 200; + +> INSERT 9 : 25 @ 9 + this.missingImagePBR.setIconWidth(16); + this.missingImagePBR.setIconHeight(16); @@ -308,8 +313,9 @@ + regenerateIfNotAllocated(); -> INSERT 2 : 23 @ 2 +> INSERT 2 : 24 @ 2 ++ + if (isEaglerPBRMode) { + if (hasAllocatedEaglerPBRMaterialTexture) { + EaglercraftGPU.regenerateTexture(eaglerPBRMaterialTexture); @@ -423,12 +429,21 @@ ~ textureatlassprite = EaglerTextureAtlasSprite.makeAtlasSprite(location); ~ } -> CHANGE 15 : 17 @ 15 : 17 +> CHANGE 12 : 18 @ 12 : 13 + +~ if (!isGLES2) { +~ this.mipmapLevels = mipmapLevelsIn; +~ } else { +~ this.mipmapLevels = 0; // Due to limitations in OpenGL ES 2.0 texture completeness, its easier to just +~ // make this zero +~ } + +> CHANGE 2 : 4 @ 2 : 4 ~ public EaglerTextureAtlasSprite getMissingSprite() { ~ return isEaglerPBRMode ? missingImagePBR : missingImage; -> INSERT 1 : 23 @ 1 +> INSERT 1 : 27 @ 1 + + public int getWidth() { @@ -444,12 +459,16 @@ + } + + public void setBlurMipmapDirect0(boolean parFlag, boolean parFlag2) { -+ super.setBlurMipmapDirect0(parFlag, parFlag2); -+ if (isEaglerPBRMode && eaglerPBRMaterialTexture != -1) { -+ GlStateManager.setActiveTexture(33986); -+ GlStateManager.bindTexture(eaglerPBRMaterialTexture); ++ if (isGLES2) { ++ super.setBlurMipmapDirect0(parFlag, false); ++ } else { + super.setBlurMipmapDirect0(parFlag, parFlag2); -+ GlStateManager.setActiveTexture(33984); ++ if (isEaglerPBRMode && eaglerPBRMaterialTexture != -1) { ++ GlStateManager.setActiveTexture(33986); ++ GlStateManager.bindTexture(eaglerPBRMaterialTexture); ++ super.setBlurMipmapDirect0(parFlag, parFlag2); ++ GlStateManager.setActiveTexture(33984); ++ } + } + } diff --git a/patches/minecraft/net/minecraft/client/renderer/texture/TextureUtil.edit.java b/patches/minecraft/net/minecraft/client/renderer/texture/TextureUtil.edit.java index 1dc65448..f17a7443 100644 --- a/patches/minecraft/net/minecraft/client/renderer/texture/TextureUtil.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/texture/TextureUtil.edit.java @@ -29,7 +29,15 @@ ~ public static int uploadTextureImage(int parInt1, ImageData parBufferedImage) { -> CHANGE 120 : 122 @ 120 : 121 +> INSERT 112 : 117 @ 112 + ++ if (!parFlag2 && !EaglercraftGPU.checkNPOTCapable() && ImageData.isNPOTStatic(parInt2, parInt3)) { ++ parFlag2 = true; ++ logger.warn( ++ "An NPOT (non-power-of-two) texture was allocated with GL_REPEAT wrapping in an OpenGL context where that isn't supported, changing to GL_CLAMP_TO_EDGE to avoid errors"); ++ } + +> CHANGE 8 : 10 @ 8 : 9 ~ EaglercraftGPU.glTexSubImage2D(GL_TEXTURE_2D, parInt1, parInt4, parInt5 + k, parInt2, l, GL_RGBA, ~ GL_UNSIGNED_BYTE, dataBuffer); @@ -46,12 +54,14 @@ ~ // deleteTexture(parInt1); //TODO: why -> CHANGE 2 : 6 @ 2 : 6 +> CHANGE 2 : 8 @ 2 : 6 -~ EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, '\u813d', parInt2); -~ EaglercraftGPU.glTexParameterf(GL_TEXTURE_2D, '\u813a', 0.0F); -~ EaglercraftGPU.glTexParameterf(GL_TEXTURE_2D, '\u813b', (float) parInt2); -~ // EaglercraftGPU.glTexParameterf(GL_TEXTURE_2D, '\u8501', 0.0F); +~ if (EaglercraftGPU.checkOpenGLESVersion() >= 300) { +~ EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, '\u813d', parInt2); +~ EaglercraftGPU.glTexParameterf(GL_TEXTURE_2D, '\u813a', 0.0F); +~ EaglercraftGPU.glTexParameterf(GL_TEXTURE_2D, '\u813b', (float) parInt2); +~ // EaglercraftGPU.glTexParameterf(GL_TEXTURE_2D, '\u8501', 0.0F); +~ } > CHANGE 1 : 2 @ 1 : 6 @@ -68,7 +78,15 @@ ~ int i = parBufferedImage.width; ~ int j = parBufferedImage.height; -> CHANGE 11 : 13 @ 11 : 12 +> INSERT 3 : 8 @ 3 + ++ if (!parFlag2 && !EaglercraftGPU.checkNPOTCapable() && parBufferedImage.isNPOT()) { ++ parFlag2 = true; ++ logger.warn( ++ "An NPOT (non-power-of-two) texture was allocated with GL_REPEAT wrapping in an OpenGL context where that isn't supported, changing to GL_CLAMP_TO_EDGE to avoid errors"); ++ } + +> CHANGE 8 : 10 @ 8 : 9 ~ EaglercraftGPU.glTexSubImage2D(GL_TEXTURE_2D, 0, parInt1, parInt2 + i1, i, j1, GL_RGBA, GL_UNSIGNED_BYTE, ~ dataBuffer); diff --git a/patches/minecraft/net/minecraft/client/renderer/tileentity/RenderItemFrame.edit.java b/patches/minecraft/net/minecraft/client/renderer/tileentity/RenderItemFrame.edit.java index ceb73e10..72a7ff3a 100644 --- a/patches/minecraft/net/minecraft/client/renderer/tileentity/RenderItemFrame.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/tileentity/RenderItemFrame.edit.java @@ -52,7 +52,11 @@ ~ if (emissive) { ~ DeferredStateManager.setEmissionConstant(0.0f); -> INSERT 19 : 23 @ 19 +> CHANGE 17 : 18 @ 17 : 18 + +~ String s = entityitemframe.getDisplayedItem().getDisplayNameProfanityFilter(); + +> INSERT 1 : 5 @ 1 + if (DeferredStateManager.isInDeferredPass()) { + NameTagRenderer.renderNameTag(entityitemframe, null, d0, d1, d2, -69); diff --git a/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityBannerRenderer.edit.java b/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityBannerRenderer.edit.java index e670e4cb..116da42f 100644 --- a/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityBannerRenderer.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityBannerRenderer.edit.java @@ -7,22 +7,31 @@ > DELETE 2 @ 2 : 4 -> INSERT 4 : 9 @ 4 +> INSERT 4 : 10 @ 4 + + import com.google.common.collect.Lists; + import com.google.common.collect.Maps; + ++ import net.lax1dude.eaglercraft.v1_8.EagRuntime; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; > DELETE 2 @ 2 : 3 > DELETE 1 @ 1 : 2 -> CHANGE 95 : 98 @ 95 : 98 +> CHANGE 72 : 73 @ 72 : 73 + +~ long i = EagRuntime.steadyTimeMillis(); + +> CHANGE 22 : 25 @ 22 : 25 ~ for (int i = 0, l = list1.size(); i < l; ++i) { ~ arraylist.add("textures/entity/banner/" ~ + ((TileEntityBanner.EnumBannerPattern) list1.get(i)).getPatternName() + ".png"); +> CHANGE 10 : 11 @ 10 : 11 + +~ tileentitybannerrenderer$timedbannertexture.systemTime = EagRuntime.steadyTimeMillis(); + > EOF diff --git a/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityChestRenderer.edit.java b/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityChestRenderer.edit.java index c94fc0ea..dea0afda 100644 --- a/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityChestRenderer.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityChestRenderer.edit.java @@ -5,19 +5,14 @@ # Version: 1.0 # Author: lax1dude -> INSERT 3 : 6 @ 3 +> INSERT 3 : 5 @ 3 + -+ import net.lax1dude.eaglercraft.v1_8.EagRuntime; + import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; > DELETE 4 @ 4 : 6 -> CHANGE 19 : 20 @ 19 : 20 - -~ Calendar calendar = EagRuntime.getLocaleCalendar(); - -> DELETE 36 @ 36 : 38 +> DELETE 56 @ 56 : 58 > INSERT 2 : 4 @ 2 diff --git a/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntitySignRenderer.edit.java b/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntitySignRenderer.edit.java index 227bdaa5..f29ef566 100644 --- a/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntitySignRenderer.edit.java +++ b/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntitySignRenderer.edit.java @@ -23,19 +23,39 @@ > DELETE 4 @ 4 : 5 -> CHANGE 55 : 56 @ 55 : 56 +> INSERT 5 : 7 @ 5 + ++ public static boolean disableProfanityFilter = false; ++ + +> CHANGE 50 : 51 @ 50 : 51 ~ EaglercraftGPU.glNormal3f(0.0F, 0.0F, -1.0F * f3); -> INSERT 3 : 8 @ 3 +> CHANGE 3 : 13 @ 3 : 6 -+ if (DeferredStateManager.isInDeferredPass()) { -+ _wglDrawBuffers(_GL_COLOR_ATTACHMENT0); -+ GlStateManager.colorMask(true, true, true, false); -+ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); -+ } +~ if (DeferredStateManager.isInDeferredPass()) { +~ _wglDrawBuffers(_GL_COLOR_ATTACHMENT0); +~ GlStateManager.colorMask(true, true, true, false); +~ GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); +~ } +~ IChatComponent[] signText = disableProfanityFilter ? tileentitysign.signText +~ : tileentitysign.getSignTextProfanityFilter(); +~ for (int j = 0; j < signText.length; ++j) { +~ if (signText[j] != null) { +~ IChatComponent ichatcomponent = signText[j]; -> INSERT 15 : 19 @ 15 +> CHANGE 4 : 6 @ 4 : 6 + +~ fontrenderer.drawString(s, -fontrenderer.getStringWidth(s) / 2, j * 10 - signText.length * 5, +~ b0); + +> CHANGE 1 : 3 @ 1 : 3 + +~ fontrenderer.drawString(s, -fontrenderer.getStringWidth(s) / 2, j * 10 - signText.length * 5, +~ b0); + +> INSERT 3 : 7 @ 3 + if (DeferredStateManager.isInDeferredPass()) { + _wglDrawBuffers(EaglerDeferredPipeline.instance.gBufferDrawBuffers); diff --git a/patches/minecraft/net/minecraft/client/resources/DefaultResourcePack.edit.java b/patches/minecraft/net/minecraft/client/resources/DefaultResourcePack.edit.java index 90c6935c..7b3cd4c9 100644 --- a/patches/minecraft/net/minecraft/client/resources/DefaultResourcePack.edit.java +++ b/patches/minecraft/net/minecraft/client/resources/DefaultResourcePack.edit.java @@ -34,20 +34,22 @@ ~ return EagRuntime ~ .getResourceStream("/assets/" + location.getResourceDomain() + "/" + location.getResourcePath()); -> CHANGE 3 : 4 @ 3 : 5 +> CHANGE 2 : 5 @ 2 : 5 -~ return this.getResourceStream(resourcelocation) != null; +~ public boolean resourceExists(ResourceLocation location) { +~ return EagRuntime +~ .getResourceExists("/assets/" + location.getResourceDomain() + "/" + location.getResourcePath()); > CHANGE 9 : 11 @ 9 : 11 ~ return AbstractResourcePack.readMetadata(parIMetadataSerializer, -~ EagRuntime.getResourceStream("pack.mcmeta"), parString1); +~ EagRuntime.getRequiredResourceStream("pack.mcmeta"), parString1); > DELETE 2 @ 2 : 4 > CHANGE 3 : 5 @ 3 : 6 ~ public ImageData getPackImage() throws IOException { -~ return TextureUtil.readBufferedImage(EagRuntime.getResourceStream("pack.png")); +~ return TextureUtil.readBufferedImage(EagRuntime.getRequiredResourceStream("pack.png")); > EOF diff --git a/patches/minecraft/net/minecraft/client/resources/Locale.edit.java b/patches/minecraft/net/minecraft/client/resources/Locale.edit.java index eba9bf1c..79305514 100644 --- a/patches/minecraft/net/minecraft/client/resources/Locale.edit.java +++ b/patches/minecraft/net/minecraft/client/resources/Locale.edit.java @@ -31,7 +31,7 @@ > INSERT 7 : 9 @ 7 -+ private static final Set hasShownMissing = new HashSet(); ++ private static final Set hasShownMissing = new HashSet<>(); + > CHANGE 4 : 5 @ 4 : 5 diff --git a/patches/minecraft/net/minecraft/client/settings/GameSettings.edit.java b/patches/minecraft/net/minecraft/client/settings/GameSettings.edit.java index c2b73915..b99e5cee 100644 --- a/patches/minecraft/net/minecraft/client/settings/GameSettings.edit.java +++ b/patches/minecraft/net/minecraft/client/settings/GameSettings.edit.java @@ -14,7 +14,7 @@ > DELETE 1 @ 1 : 3 -> INSERT 3 : 27 @ 3 +> INSERT 3 : 31 @ 3 + + import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayManager; @@ -34,12 +34,16 @@ + import net.lax1dude.eaglercraft.v1_8.EaglerZLIB; + import net.lax1dude.eaglercraft.v1_8.HString; + import net.lax1dude.eaglercraft.v1_8.Keyboard; -+ import net.lax1dude.eaglercraft.v1_8.Mouse; ++ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; + import net.lax1dude.eaglercraft.v1_8.internal.EnumPlatformType; + import net.lax1dude.eaglercraft.v1_8.internal.KeyboardConstants; + import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; + import net.lax1dude.eaglercraft.v1_8.log4j.Logger; + import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.EaglerDeferredConfig; ++ import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.EaglerDeferredPipeline; ++ import net.lax1dude.eaglercraft.v1_8.opengl.ext.dynamiclights.DynamicLightsStateManager; ++ import net.lax1dude.eaglercraft.v1_8.recording.EnumScreenRecordingCodec; ++ import net.lax1dude.eaglercraft.v1_8.recording.ScreenRecordingController; > DELETE 5 @ 5 : 7 @@ -91,7 +95,7 @@ ~ public int guiScale = 3; -> INSERT 3 : 18 @ 3 +> INSERT 3 : 22 @ 3 + public boolean hudFps = true; + public boolean hudCoords = true; @@ -108,14 +112,26 @@ + public boolean enableUpdateSvc = true; + public boolean enableFNAWSkins = true; + public boolean enableDynamicLights = false; ++ public boolean hasHiddenPhishWarning = false; ++ public boolean enableProfanityFilter = false; ++ public boolean hasShownProfanityFilter = false; ++ public float touchControlOpacity = 1.0f; -> CHANGE 1 : 7 @ 1 : 2 +> CHANGE 1 : 15 @ 1 : 2 ~ public int voiceListenRadius = 16; ~ public float voiceListenVolume = 0.5f; ~ public float voiceSpeakVolume = 0.5f; ~ public int voicePTTKey = 47; // V ~ +~ public EnumScreenRecordingCodec screenRecordCodec; +~ public int screenRecordFPS = ScreenRecordingController.DEFAULT_FPS; +~ public int screenRecordResolution = ScreenRecordingController.DEFAULT_RESOLUTION; +~ public int screenRecordAudioBitrate = ScreenRecordingController.DEFAULT_AUDIO_BITRATE; +~ public int screenRecordVideoBitrate = ScreenRecordingController.DEFAULT_VIDEO_BITRATE; +~ public float screenRecordGameVolume = ScreenRecordingController.DEFAULT_GAME_VOLUME; +~ public float screenRecordMicVolume = ScreenRecordingController.DEFAULT_MIC_VOLUME; +~ ~ public GameSettings(Minecraft mcIn) { > CHANGE 4 : 6 @ 4 : 7 @@ -133,13 +149,10 @@ ~ this.gammaSetting = 1.0F; ~ this.language = EagRuntime.getConfiguration().getDefaultLocale(); -> CHANGE 2 : 3 @ 2 : 8 - -~ GameSettings.Options.RENDER_DISTANCE.setValueMax(18.0F); - -> CHANGE 1 : 2 @ 1 : 2 +> CHANGE 2 : 4 @ 2 : 10 ~ this.renderDistanceChunks = 4; +~ this.screenRecordCodec = ScreenRecordingController.getDefaultCodec(); > DELETE 3 @ 3 : 18 @@ -147,55 +160,106 @@ ~ : HString.format("%c", new Object[] { Character.valueOf((char) (parInt1 - 256)) }) -> DELETE 76 @ 76 : 99 +> CHANGE 5 : 7 @ 5 : 6 + +~ : (parKeyBinding.getKeyCode() < 0 +~ ? PointerInputAbstraction.getVCursorButtonDown(parKeyBinding.getKeyCode() + 100) + +> CHANGE 71 : 73 @ 71 : 73 + +~ if (parOptions == GameSettings.Options.EAGLER_TOUCH_CONTROL_OPACITY) { +~ this.touchControlOpacity = parFloat1; + +> DELETE 1 @ 1 : 20 > INSERT 35 : 37 @ 35 + this.mc.loadingScreen.eaglerShow(I18n.format("resourcePack.load.refreshing"), + I18n.format("resourcePack.load.pleaseWait")); -> DELETE 18 @ 18 : 38 +> CHANGE 18 : 20 @ 18 : 20 -> DELETE 20 @ 20 : 37 +~ if (parOptions == GameSettings.Options.CHAT_COLOR) { +~ this.chatColours = !this.chatColours; -> INSERT 13 : 67 @ 13 +> CHANGE 2 : 4 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.CHAT_LINKS) { +~ this.chatLinks = !this.chatLinks; + +> CHANGE 2 : 4 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.CHAT_LINKS_PROMPT) { +~ this.chatLinksPrompt = !this.chatLinksPrompt; + +> CHANGE 2 : 4 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.SNOOPER_ENABLED) { +~ this.snooperEnabled = !this.snooperEnabled; + +> CHANGE 2 : 5 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.BLOCK_ALTERNATIVES) { +~ this.allowBlockAlternatives = !this.allowBlockAlternatives; +~ this.mc.renderGlobal.loadRenderers(); + +> CHANGE 2 : 4 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.REDUCED_DEBUG_INFO) { +~ this.reducedDebugInfo = !this.reducedDebugInfo; + +> CHANGE 2 : 4 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.ENTITY_SHADOWS) { +~ this.field_181151_V = !this.field_181151_V; + +> CHANGE 2 : 4 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.HUD_FPS) { +~ this.hudFps = !this.hudFps; + +> CHANGE 2 : 4 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.HUD_COORDS) { +~ this.hudCoords = !this.hudCoords; + +> CHANGE 2 : 4 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.HUD_PLAYER) { +~ this.hudPlayer = !this.hudPlayer; + +> CHANGE 2 : 4 @ 2 : 7 + +~ if (parOptions == GameSettings.Options.HUD_STATS) { +~ this.hudStats = !this.hudStats; + +> CHANGE 2 : 4 @ 2 : 5 + +~ if (parOptions == GameSettings.Options.HUD_WORLD) { +~ this.hudWorld = !this.hudWorld; + +> CHANGE 2 : 4 @ 2 : 5 + +~ if (parOptions == GameSettings.Options.HUD_24H) { +~ this.hud24h = !this.hud24h; + +> CHANGE 2 : 4 @ 2 : 5 + +~ if (parOptions == GameSettings.Options.CHUNK_FIX) { +~ this.chunkFix = !this.chunkFix; + +> CHANGE 2 : 4 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.FOG) { +~ this.fog = !this.fog; + +> CHANGE 2 : 4 @ 2 : 4 + +~ if (parOptions == GameSettings.Options.FXAA) { +~ this.fxaa = (this.fxaa + parInt1) % 3; + +> INSERT 2 : 24 @ 2 -+ if (parOptions == GameSettings.Options.HUD_FPS) { -+ this.hudFps = !this.hudFps; -+ } -+ -+ if (parOptions == GameSettings.Options.HUD_COORDS) { -+ this.hudCoords = !this.hudCoords; -+ } -+ -+ if (parOptions == GameSettings.Options.HUD_PLAYER) { -+ this.hudPlayer = !this.hudPlayer; -+ } -+ -+ if (parOptions == GameSettings.Options.HUD_STATS) { -+ this.hudStats = !this.hudStats; -+ } -+ -+ if (parOptions == GameSettings.Options.HUD_WORLD) { -+ this.hudWorld = !this.hudWorld; -+ } -+ -+ if (parOptions == GameSettings.Options.HUD_24H) { -+ this.hud24h = !this.hud24h; -+ } -+ -+ if (parOptions == GameSettings.Options.CHUNK_FIX) { -+ this.chunkFix = !this.chunkFix; -+ } -+ -+ if (parOptions == GameSettings.Options.FOG) { -+ this.fog = !this.fog; -+ } -+ -+ if (parOptions == GameSettings.Options.FXAA) { -+ this.fxaa = (this.fxaa + parInt1) % 3; -+ } -+ + if (parOptions == GameSettings.Options.FULLSCREEN) { + this.mc.toggleFullscreen(); + } @@ -214,16 +278,20 @@ + this.mc.renderGlobal.loadRenderers(); + } + ++ if (parOptions == GameSettings.Options.EAGLER_PROFANITY_FILTER) { ++ this.enableProfanityFilter = !this.enableProfanityFilter; ++ } ++ -> CHANGE 23 : 24 @ 23 : 34 +> CHANGE 23 : 26 @ 23 : 34 -~ : 0.0F))))))))))); +~ : (parOptions == GameSettings.Options.EAGLER_TOUCH_CONTROL_OPACITY +~ ? this.touchControlOpacity +~ : 0.0F)))))))))))); -> DELETE 20 @ 20 : 26 +> DELETE 20 @ 20 : 30 -> DELETE 2 @ 2 : 4 - -> INSERT 8 : 32 @ 8 +> INSERT 8 : 34 @ 8 + case HUD_COORDS: + return this.hudCoords; @@ -249,6 +317,8 @@ + return this.enableVsync; + case EAGLER_DYNAMIC_LIGHTS: + return this.enableDynamicLights; ++ case EAGLER_PROFANITY_FILTER: ++ return this.enableProfanityFilter; > CHANGE 43 : 46 @ 43 : 47 @@ -264,7 +334,7 @@ ~ .calculateChatboxHeight( -> CHANGE 2 : 21 @ 2 : 36 +> CHANGE 2 : 25 @ 2 : 36 ~ : (parOptions == GameSettings.Options.CHAT_WIDTH ~ ? s + GuiNewChat @@ -284,7 +354,11 @@ ~ : s + (int) (f ~ * 100.0F) ~ + "%") -~ : "yee")))))))))))); +~ : (parOptions == GameSettings.Options.EAGLER_TOUCH_CONTROL_OPACITY +~ ? (s + (int) (f +~ * 100.0F) +~ + "%") +~ : "yee"))))))))))))); > DELETE 11 @ 11 : 19 @@ -354,7 +428,9 @@ > DELETE 3 @ 3 : 7 -> CHANGE 52 : 54 @ 52 : 54 +> DELETE 12 @ 12 : 16 + +> CHANGE 36 : 38 @ 36 : 38 ~ if (astring[0].equals("forceUnicodeFont")) { ~ this.forceUnicodeFont = astring[1].equals("true"); @@ -459,7 +535,7 @@ ~ for (EnumPlayerModelParts enumplayermodelparts : EnumPlayerModelParts._VALUES) { -> INSERT 4 : 14 @ 4 +> INSERT 4 : 66 @ 4 + + if (astring[0].equals("enableFNAWSkins")) { @@ -470,17 +546,79 @@ + this.enableDynamicLights = astring[1].equals("true"); + } + ++ if (astring[0].equals("hasHiddenPhishWarning")) { ++ this.hasHiddenPhishWarning = astring[1].equals("true"); ++ } ++ ++ if (astring[0].equals("enableProfanityFilter")) { ++ this.enableProfanityFilter = astring[1].equals("true"); ++ } ++ ++ if (astring[0].equals("hasShownProfanityFilter")) { ++ this.hasShownProfanityFilter = astring[1].equals("true"); ++ } ++ ++ if (astring[0].equals("screenRecordCodec")) { ++ EnumScreenRecordingCodec codec = EnumScreenRecordingCodec.valueOf(astring[1]); ++ if (!ScreenRecordingController.codecs.contains(codec)) { ++ throw new IllegalStateException("Selected codec is not supported: " + codec.name); ++ } ++ screenRecordCodec = codec; ++ } ++ ++ if (astring[0].equals("screenRecordFPS")) { ++ screenRecordFPS = Integer.parseInt(astring[1]); ++ } ++ ++ if (astring[0].equals("screenRecordFPS")) { ++ screenRecordFPS = Integer.parseInt(astring[1]); ++ } ++ ++ if (astring[0].equals("screenRecordResolution")) { ++ screenRecordResolution = Integer.parseInt(astring[1]); ++ } ++ ++ if (astring[0].equals("screenRecordAudioBitrate")) { ++ screenRecordAudioBitrate = Integer.parseInt(astring[1]); ++ } ++ ++ if (astring[0].equals("screenRecordVideoBitrate")) { ++ screenRecordVideoBitrate = Integer.parseInt(astring[1]); ++ } ++ ++ if (astring[0].equals("screenRecordGameVolume")) { ++ screenRecordGameVolume = parseFloat(astring[1]); ++ } ++ ++ if (astring[0].equals("screenRecordMicVolume")) { ++ screenRecordMicVolume = parseFloat(astring[1]); ++ } ++ ++ if (astring[0].equals("touchControlOpacity")) { ++ touchControlOpacity = parseFloat(astring[1]); ++ } ++ + deferredShaderConf.readOption(astring[0], astring[1]); -> CHANGE 6 : 13 @ 6 : 7 +> CHANGE 6 : 23 @ 6 : 7 ~ ~ Keyboard.setFunctionKeyModifier(keyBindFunction.getKeyCode()); ~ VoiceClientController.setVoiceListenVolume(voiceListenVolume); ~ VoiceClientController.setVoiceSpeakVolume(voiceSpeakVolume); ~ VoiceClientController.setVoiceProximity(voiceListenRadius); +~ ScreenRecordingController.setGameVolume(screenRecordGameVolume); +~ ScreenRecordingController.setMicrophoneVolume(screenRecordMicVolume); ~ if (this.mc.getRenderManager() != null) ~ this.mc.getRenderManager().setEnableFNAWSkins(this.enableFNAWSkins); +~ if (this.shaders && !EaglerDeferredPipeline.isSupported()) { +~ logger.error("Setting shaders to false because they are not supported"); +~ this.shaders = false; +~ } +~ if (this.enableDynamicLights && !DynamicLightsStateManager.isSupported()) { +~ logger.error("Setting dynamic lights to false because they are not supported"); +~ this.enableDynamicLights = false; +~ } > CHANGE 1 : 3 @ 1 : 2 @@ -519,9 +657,11 @@ ~ printwriter.println("enableVsyncEag:" + this.enableVsync); -> DELETE 13 @ 13 : 24 +> DELETE 3 @ 3 : 4 -> INSERT 5 : 22 @ 5 +> DELETE 9 @ 9 : 20 + +> INSERT 5 : 35 @ 5 + printwriter.println("hudFps:" + this.hudFps); + printwriter.println("hudWorld:" + this.hudWorld); @@ -540,6 +680,19 @@ + printwriter.println("voicePTTKey:" + this.voicePTTKey); + printwriter.println("enableFNAWSkins:" + this.enableFNAWSkins); + printwriter.println("enableDynamicLights:" + this.enableDynamicLights); ++ printwriter.println("hasHiddenPhishWarning:" + this.hasHiddenPhishWarning); ++ printwriter.println("enableProfanityFilter:" + this.enableProfanityFilter); ++ printwriter.println("hasShownProfanityFilter:" + this.hasShownProfanityFilter); ++ if (screenRecordCodec != null) { ++ printwriter.println("screenRecordCodec:" + this.screenRecordCodec); ++ } ++ printwriter.println("screenRecordFPS:" + this.screenRecordFPS); ++ printwriter.println("screenRecordResolution:" + this.screenRecordResolution); ++ printwriter.println("screenRecordAudioBitrate:" + this.screenRecordAudioBitrate); ++ printwriter.println("screenRecordVideoBitrate:" + this.screenRecordVideoBitrate); ++ printwriter.println("screenRecordGameVolume:" + this.screenRecordGameVolume); ++ printwriter.println("screenRecordMicVolume:" + this.screenRecordMicVolume); ++ printwriter.println("touchControlOpacity:" + this.touchControlOpacity); > CHANGE 5 : 8 @ 5 : 6 @@ -568,11 +721,7 @@ > DELETE 2 @ 2 : 3 -> CHANGE 5 : 6 @ 5 : 6 - -~ : (parSoundCategory == SoundCategory.VOICE ? 0.0F : 1.0F); - -> CHANGE 16 : 17 @ 16 : 17 +> CHANGE 22 : 23 @ 22 : 23 ~ Math.max(this.renderDistanceChunks, 2), this.chatVisibility, this.chatColours, i)); @@ -591,12 +740,9 @@ ~ RENDER_DISTANCE("options.renderDistance", true, false, 1.0F, 18.0F, 1.0F), -> CHANGE 8 : 10 @ 8 : 12 +> DELETE 8 @ 8 : 10 -~ TOUCHSCREEN("options.touchscreen", false, true), CHAT_SCALE("options.chat.scale", true, false), -~ CHAT_WIDTH("options.chat.width", true, false), CHAT_HEIGHT_FOCUSED("options.chat.height.focused", true, false), - -> CHANGE 14 : 22 @ 14 : 15 +> CHANGE 16 : 26 @ 16 : 17 ~ ENTITY_SHADOWS("options.entityShadows", false, true), HUD_FPS("options.hud.fps", false, true), ~ HUD_COORDS("options.hud.coords", false, true), HUD_STATS("options.hud.stats", false, true), @@ -605,6 +751,8 @@ ~ FOG("options.fog", false, true), FXAA("options.fxaa", false, false), ~ FULLSCREEN("options.fullscreen", false, true), ~ FNAW_SKINS("options.skinCustomisation.enableFNAWSkins", false, true), -~ EAGLER_VSYNC("options.vsync", false, true), EAGLER_DYNAMIC_LIGHTS("options.dynamicLights", false, true); +~ EAGLER_VSYNC("options.vsync", false, true), EAGLER_DYNAMIC_LIGHTS("options.dynamicLights", false, true), +~ EAGLER_PROFANITY_FILTER("options.profanityFilterButton", false, true), +~ EAGLER_TOUCH_CONTROL_OPACITY("options.touchControlOpacity", true, false); > EOF diff --git a/patches/minecraft/net/minecraft/entity/Entity.edit.java b/patches/minecraft/net/minecraft/entity/Entity.edit.java index bcd091bd..e7263859 100644 --- a/patches/minecraft/net/minecraft/entity/Entity.edit.java +++ b/patches/minecraft/net/minecraft/entity/Entity.edit.java @@ -5,21 +5,23 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 3 : 9 @ 3 : 5 +> CHANGE 3 : 10 @ 3 : 5 ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; ~ import net.lax1dude.eaglercraft.v1_8.HString; ~ import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.DynamicLightManager; ~ import net.lax1dude.eaglercraft.v1_8.opengl.ext.dynamiclights.DynamicLightsStateManager; +~ import net.lax1dude.eaglercraft.v1_8.profanity_filter.ProfanityFilter; ~ > INSERT 1 : 2 @ 1 + -> INSERT 8 : 9 @ 8 +> INSERT 8 : 10 @ 8 ++ import net.minecraft.client.Minecraft; + import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; > DELETE 6 @ 6 : 9 @@ -36,7 +38,17 @@ ~ this.rand = new EaglercraftRandom(); -> CHANGE 294 : 296 @ 294 : 295 +> DELETE 100 @ 100 : 101 + +> DELETE 11 @ 11 : 12 + +> DELETE 32 @ 32 : 34 + +> DELETE 35 @ 35 : 36 + +> DELETE 44 @ 44 : 45 + +> CHANGE 66 : 68 @ 66 : 67 ~ List list1 = this.worldObj.getCollidingBoundingBoxes(this, ~ this.getEntityBoundingBox().addCoord(x, y, z)); @@ -95,7 +107,11 @@ ~ for (int i = 0, l = list.size(); i < l; ++i) { ~ y = list.get(i).calculateYOffset(this.getEntityBoundingBox(), y); -> CHANGE 347 : 353 @ 347 : 348 +> DELETE 11 @ 11 : 13 + +> DELETE 93 @ 93 : 95 + +> CHANGE 239 : 245 @ 239 : 240 ~ int i = 0; ~ if (DynamicLightsStateManager.isDynamicLightsRender()) { @@ -135,15 +151,74 @@ ~ for (AxisAlignedBB axisalignedbb : (List) list) { -> CHANGE 256 : 257 @ 256 : 257 +> INSERT 229 : 242 @ 229 + ++ public String getNameProfanityFilter() { ++ if (this.hasCustomName()) { ++ return this.getCustomNameTagProfanityFilter(); ++ } else { ++ String s = EntityList.getEntityString(this); ++ if (s == null) { ++ s = "generic"; ++ } ++ ++ return StatCollector.translateToLocal("entity." + s + ".name"); ++ } ++ } ++ + +> CHANGE 27 : 28 @ 27 : 28 ~ return HString.format("%s[\'%s\'/%d, l=\'%s\', x=%.2f, y=%.2f, z=%.2f]", -> CHANGE 121 : 122 @ 121 : 122 +> DELETE 26 @ 26 : 27 + +> DELETE 12 @ 12 : 13 + +> DELETE 1 @ 1 : 2 + +> DELETE 12 @ 12 : 13 + +> DELETE 2 @ 2 : 3 + +> CHANGE 63 : 64 @ 63 : 64 ~ public EaglercraftUUID getUniqueID() { -> INSERT 151 : 205 @ 151 +> INSERT 14 : 21 @ 14 + ++ public IChatComponent getDisplayNameProfanityFilter() { ++ ChatComponentText chatcomponenttext = new ChatComponentText(this.getNameProfanityFilter()); ++ chatcomponenttext.getChatStyle().setChatHoverEvent(this.getHoverEvent()); ++ chatcomponenttext.getChatStyle().setInsertion(this.getUniqueID().toString()); ++ return chatcomponenttext; ++ } ++ + +> INSERT 8 : 28 @ 8 + ++ private String lastNameTagForFilter = null; ++ private String lastFilteredNameTagForFilter = null; ++ ++ public String getCustomNameTagProfanityFilter() { ++ if (Minecraft.getMinecraft().isEnableProfanityFilter()) { ++ String str = getCustomNameTag(); ++ if (str != null) { ++ if (!str.equals(lastNameTagForFilter)) { ++ lastNameTagForFilter = str; ++ lastFilteredNameTagForFilter = ProfanityFilter.getInstance().profanityFilterString(str); ++ } ++ return lastFilteredNameTagForFilter; ++ } else { ++ return null; ++ } ++ } else { ++ return getCustomNameTag(); ++ } ++ } ++ + +> INSERT 129 : 183 @ 129 + + public void renderDynamicLightsEagler(float partialTicks, boolean isInFrustum) { diff --git a/patches/minecraft/net/minecraft/entity/EntityLiving.edit.java b/patches/minecraft/net/minecraft/entity/EntityLiving.edit.java index 2abff347..5860c561 100644 --- a/patches/minecraft/net/minecraft/entity/EntityLiving.edit.java +++ b/patches/minecraft/net/minecraft/entity/EntityLiving.edit.java @@ -13,14 +13,47 @@ > DELETE 1 @ 1 : 8 -> CHANGE 316 : 320 @ 316 : 318 +> CHANGE 56 : 58 @ 56 : 59 + +~ this.tasks = new EntityAITasks(); +~ this.targetTasks = new EntityAITasks(); + +> DELETE 76 @ 76 : 77 + +> DELETE 4 @ 4 : 6 + +> DELETE 171 @ 171 : 172 + +> CHANGE 2 : 6 @ 2 : 4 ~ List lst = this.worldObj.getEntitiesWithinAABB(EntityItem.class, ~ this.getEntityBoundingBox().expand(1.0D, 0.0D, 1.0D)); ~ for (int i = 0, l = lst.size(); i < l; ++i) { ~ EntityItem entityitem = lst.get(i); -> CHANGE 497 : 499 @ 497 : 498 +> DELETE 5 @ 5 : 7 + +> DELETE 98 @ 98 : 99 + +> DELETE 1 @ 1 : 3 + +> DELETE 1 @ 1 : 3 + +> DELETE 1 @ 1 : 3 + +> DELETE 1 @ 1 : 3 + +> DELETE 1 @ 1 : 3 + +> DELETE 1 @ 1 : 4 + +> DELETE 1 @ 1 : 2 + +> DELETE 1 @ 1 : 2 + +> DELETE 1 @ 1 : 3 + +> CHANGE 365 : 367 @ 365 : 366 ~ EaglercraftUUID uuid = new EaglercraftUUID(this.leashNBTTag.getLong("UUIDMost"), ~ this.leashNBTTag.getLong("UUIDLeast")); diff --git a/patches/minecraft/net/minecraft/entity/EntityLivingBase.edit.java b/patches/minecraft/net/minecraft/entity/EntityLivingBase.edit.java index a8491ab4..87a11157 100644 --- a/patches/minecraft/net/minecraft/entity/EntityLivingBase.edit.java +++ b/patches/minecraft/net/minecraft/entity/EntityLivingBase.edit.java @@ -25,7 +25,11 @@ ~ private static final EaglercraftUUID sprintingSpeedBoostModifierUUID = EaglercraftUUID ~ .fromString("662A6B8D-DA3E-4C1C-8813-96EA6097278D"); -> CHANGE 264 : 265 @ 264 : 265 +> DELETE 120 @ 120 : 121 + +> DELETE 88 @ 88 : 89 + +> CHANGE 54 : 55 @ 54 : 55 ~ public EaglercraftRandom getRNG() { @@ -40,7 +44,27 @@ ~ for (int i = 0; i < inv.length; ++i) { ~ ItemStack itemstack1 = inv[i]; -> INSERT 1254 : 1277 @ 1254 +> DELETE 941 @ 941 : 942 + +> DELETE 1 @ 1 : 3 + +> DELETE 33 @ 33 : 34 + +> DELETE 62 @ 62 : 63 + +> DELETE 6 @ 6 : 7 + +> DELETE 1 @ 1 : 2 + +> DELETE 2 @ 2 : 4 + +> DELETE 13 @ 13 : 15 + +> DELETE 4 @ 4 : 6 + +> DELETE 3 @ 3 : 5 + +> INSERT 173 : 196 @ 173 + + protected void renderDynamicLightsEaglerAt(double entityX, double entityY, double entityZ, double renderX, @@ -59,7 +83,7 @@ + if (itm != null && itm.stackSize > 0) { + Item item = itm.getItem(); + if (item != null) { -+ float f2 = item.getHeldItemBrightnessEagler(); ++ float f2 = item.getHeldItemBrightnessEagler(itm); + f = Math.min(f + f2 * 0.5f, 1.0f) + f2 * 0.5f; + } + } diff --git a/patches/minecraft/net/minecraft/entity/ai/EntityAITasks.edit.java b/patches/minecraft/net/minecraft/entity/ai/EntityAITasks.edit.java index 9396d140..ff0dbb7c 100644 --- a/patches/minecraft/net/minecraft/entity/ai/EntityAITasks.edit.java +++ b/patches/minecraft/net/minecraft/entity/ai/EntityAITasks.edit.java @@ -5,19 +5,25 @@ # Version: 1.0 # Author: lax1dude -> DELETE 5 @ 5 : 6 - -> CHANGE 1 : 3 @ 1 : 3 +> CHANGE 5 : 7 @ 5 : 9 ~ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; ~ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -> CHANGE 84 : 86 @ 84 : 86 +> DELETE 5 @ 5 : 6 + +> DELETE 3 @ 3 : 7 + +> DELETE 24 @ 24 : 25 + +> CHANGE 43 : 45 @ 43 : 48 ~ for (int i = 0, l = this.executingTaskEntries.size(); i < l; ++i) { ~ this.executingTaskEntries.get(i).action.updateTask(); -> CHANGE 11 : 13 @ 11 : 12 +> DELETE 1 @ 1 : 3 + +> CHANGE 8 : 10 @ 8 : 9 ~ for (int i = 0, l = this.taskEntries.size(); i < l; ++i) { ~ EntityAITasks.EntityAITaskEntry entityaitasks$entityaitaskentry = this.taskEntries.get(i); diff --git a/patches/minecraft/net/minecraft/entity/ai/EntitySenses.edit.java b/patches/minecraft/net/minecraft/entity/ai/EntitySenses.edit.java new file mode 100644 index 00000000..4c2454bc --- /dev/null +++ b/patches/minecraft/net/minecraft/entity/ai/EntitySenses.edit.java @@ -0,0 +1,12 @@ + +# Eagler Context Redacted Diff +# Copyright (c) 2024 lax1dude. All rights reserved. + +# Version: 1.0 +# Author: lax1dude + +> DELETE 27 @ 27 : 28 + +> DELETE 1 @ 1 : 2 + +> EOF diff --git a/patches/minecraft/net/minecraft/entity/item/EntityItem.edit.java b/patches/minecraft/net/minecraft/entity/item/EntityItem.edit.java index 05f4831a..ddd7664b 100644 --- a/patches/minecraft/net/minecraft/entity/item/EntityItem.edit.java +++ b/patches/minecraft/net/minecraft/entity/item/EntityItem.edit.java @@ -43,7 +43,7 @@ + if (itm != null && itm.stackSize > 0) { + Item item = itm.getItem(); + if (item != null) { -+ float f2 = item.getHeldItemBrightnessEagler() * 0.75f; ++ float f2 = item.getHeldItemBrightnessEagler(itm) * 0.75f; + f = Math.min(f + f2 * 0.5f, 1.0f) + f2 * 0.5f; + } + } diff --git a/patches/minecraft/net/minecraft/entity/item/EntityItemFrame.edit.java b/patches/minecraft/net/minecraft/entity/item/EntityItemFrame.edit.java index d391f9b2..1f829a95 100644 --- a/patches/minecraft/net/minecraft/entity/item/EntityItemFrame.edit.java +++ b/patches/minecraft/net/minecraft/entity/item/EntityItemFrame.edit.java @@ -31,7 +31,7 @@ + if (itm != null && itm.stackSize > 0) { + Item item = itm.getItem(); + if (item != null) { -+ float f2 = item.getHeldItemBrightnessEagler() * 0.75f; ++ float f2 = item.getHeldItemBrightnessEagler(itm) * 0.75f; + f = Math.min(f + f2 * 0.5f, 1.0f) + f2 * 0.5f; + } + } diff --git a/patches/minecraft/net/minecraft/entity/item/EntityMinecart.edit.java b/patches/minecraft/net/minecraft/entity/item/EntityMinecart.edit.java index 99531860..cab34da0 100644 --- a/patches/minecraft/net/minecraft/entity/item/EntityMinecart.edit.java +++ b/patches/minecraft/net/minecraft/entity/item/EntityMinecart.edit.java @@ -25,14 +25,32 @@ ~ this.dataWatcher.addObject(20, Integer.valueOf(0)); ~ this.dataWatcher.addObject(21, Integer.valueOf(6)); -> CHANGE 198 : 202 @ 198 : 200 +> DELETE 101 @ 101 : 102 + +> DELETE 32 @ 32 : 34 + +> CHANGE 62 : 66 @ 62 : 64 ~ List lst = this.worldObj.getEntitiesWithinAABBExcludingEntity(this, ~ this.getEntityBoundingBox().expand(0.20000000298023224D, 0.0D, 0.20000000298023224D)); ~ for (int i = 0, m = lst.size(); i < m; ++i) { ~ Entity entity = lst.get(i); -> CHANGE 585 : 588 @ 585 : 588 +> INSERT 533 : 537 @ 533 + ++ public String getNameProfanityFilter() { ++ return getName(); ++ } ++ + +> INSERT 23 : 27 @ 23 + ++ public IChatComponent getDisplayNameProfanityFilter() { ++ return getDisplayName(); ++ } ++ + +> CHANGE 29 : 32 @ 29 : 32 ~ EntityMinecart.EnumMinecartType[] types = values(); ~ for (int i = 0; i < types.length; ++i) { diff --git a/patches/minecraft/net/minecraft/entity/passive/EntityHorse.edit.java b/patches/minecraft/net/minecraft/entity/passive/EntityHorse.edit.java index 4778f2d5..1bc3f89b 100644 --- a/patches/minecraft/net/minecraft/entity/passive/EntityHorse.edit.java +++ b/patches/minecraft/net/minecraft/entity/passive/EntityHorse.edit.java @@ -19,7 +19,22 @@ > DELETE 12 @ 12 : 13 -> CHANGE 381 : 385 @ 381 : 383 +> INSERT 99 : 107 @ 99 + ++ return getNameImpl(false); ++ } ++ ++ public String getNameProfanityFilter() { ++ return getNameImpl(true); ++ } ++ ++ private String getNameImpl(boolean filter) { + +> CHANGE 1 : 2 @ 1 : 2 + +~ return filter ? this.getCustomNameTagProfanityFilter() : this.getCustomNameTag(); + +> CHANGE 280 : 284 @ 280 : 282 ~ List lst = this.worldObj.getEntitiesInAABBexcluding(entityIn, ~ entityIn.getEntityBoundingBox().addCoord(distance, distance, distance), horseBreedingSelector); diff --git a/patches/minecraft/net/minecraft/entity/passive/EntityOcelot.edit.java b/patches/minecraft/net/minecraft/entity/passive/EntityOcelot.edit.java index c0a52cd2..76e70e8b 100644 --- a/patches/minecraft/net/minecraft/entity/passive/EntityOcelot.edit.java +++ b/patches/minecraft/net/minecraft/entity/passive/EntityOcelot.edit.java @@ -7,4 +7,21 @@ > DELETE 20 @ 20 : 23 +> CHANGE 215 : 216 @ 215 : 217 + +~ return getNameImpl(false); + +> INSERT 2 : 12 @ 2 + ++ public String getNameProfanityFilter() { ++ return getNameImpl(true); ++ } ++ ++ private String getNameImpl(boolean filter) { ++ return this.hasCustomName() ? (filter ? this.getCustomNameTagProfanityFilter() : this.getCustomNameTag()) ++ : (this.isTamed() ? StatCollector.translateToLocal("entity.Cat.name") ++ : (filter ? super.getNameProfanityFilter() : super.getName())); ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/entity/passive/EntityTameable.edit.java b/patches/minecraft/net/minecraft/entity/passive/EntityTameable.edit.java index e741155e..cd234599 100644 --- a/patches/minecraft/net/minecraft/entity/passive/EntityTameable.edit.java +++ b/patches/minecraft/net/minecraft/entity/passive/EntityTameable.edit.java @@ -5,8 +5,10 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 4 @ 2 : 3 +> CHANGE 2 : 6 @ 2 : 3 +~ import org.apache.commons.lang3.StringUtils; +~ ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; ~ import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; @@ -48,8 +50,19 @@ ~ s = nbttagcompound.getString("Owner"); ~ } -> CHANGE 83 : 84 @ 83 : 84 +> INSERT 82 : 86 @ 82 -~ EaglercraftUUID uuid = EaglercraftUUID.fromString(this.getOwnerId()); ++ String ownerName = this.getOwnerId(); ++ if (StringUtils.isEmpty(ownerName)) { ++ return null; ++ } + +> CHANGE 1 : 2 @ 1 : 2 + +~ EaglercraftUUID uuid = EaglercraftUUID.fromString(ownerName); + +> CHANGE 2 : 3 @ 2 : 3 + +~ return this.worldObj.getPlayerEntityByName(ownerName); > EOF diff --git a/patches/minecraft/net/minecraft/entity/passive/EntityVillager.edit.java b/patches/minecraft/net/minecraft/entity/passive/EntityVillager.edit.java index 5f1f0f9b..731fb6f6 100644 --- a/patches/minecraft/net/minecraft/entity/passive/EntityVillager.edit.java +++ b/patches/minecraft/net/minecraft/entity/passive/EntityVillager.edit.java @@ -257,7 +257,23 @@ ~ for (int k = 0, l = aentityvillager$itradelist2.size(); k < l; ++k) { ~ aentityvillager$itradelist2.get(k).modifyMerchantRecipeList(this.buyingList, this.rand); -> CHANGE 236 : 237 @ 236 : 237 +> CHANGE 9 : 18 @ 9 : 10 + +~ return getDisplayNameImpl(false); +~ } +~ +~ public IChatComponent getDisplayNameProfanityFilter() { +~ return getDisplayNameImpl(true); +~ } +~ +~ private IChatComponent getDisplayNameImpl(boolean filter) { +~ String s = filter ? this.getCustomNameTagProfanityFilter() : this.getCustomNameTag(); + +> CHANGE 53 : 54 @ 53 : 54 + +~ return filter ? super.getDisplayNameProfanityFilter() : super.getDisplayName(); + +> CHANGE 172 : 173 @ 172 : 173 ~ public void modifyMerchantRecipeList(MerchantRecipeList recipeList, EaglercraftRandom random) { diff --git a/patches/minecraft/net/minecraft/entity/player/EntityPlayer.edit.java b/patches/minecraft/net/minecraft/entity/player/EntityPlayer.edit.java index b7ffecdf..dade11d8 100644 --- a/patches/minecraft/net/minecraft/entity/player/EntityPlayer.edit.java +++ b/patches/minecraft/net/minecraft/entity/player/EntityPlayer.edit.java @@ -24,7 +24,19 @@ ~ public abstract class EntityPlayer extends EntityLivingBase implements ICommandSender { -> CHANGE 458 : 459 @ 458 : 459 +> INSERT 77 : 86 @ 77 + ++ public boolean getItemShouldUseOnTouchEagler() { ++ if (itemInUse != null) { ++ return itemInUse.getItem().shouldUseOnTouchEagler(itemInUse); ++ } else { ++ ItemStack st = getHeldItem(); ++ return st != null && st.getItem().shouldUseOnTouchEagler(st); ++ } ++ } ++ + +> CHANGE 381 : 382 @ 381 : 382 ~ Collection collection = this.getWorldScoreboard() diff --git a/patches/minecraft/net/minecraft/entity/player/EntityPlayerMP.edit.java b/patches/minecraft/net/minecraft/entity/player/EntityPlayerMP.edit.java index 7afbaf13..8963e4d2 100644 --- a/patches/minecraft/net/minecraft/entity/player/EntityPlayerMP.edit.java +++ b/patches/minecraft/net/minecraft/entity/player/EntityPlayerMP.edit.java @@ -14,8 +14,10 @@ > DELETE 51 @ 51 : 52 -> CHANGE 22 : 24 @ 22 : 24 +> CHANGE 22 : 26 @ 22 : 24 +~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; +~ import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; ~ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; ~ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; @@ -23,9 +25,14 @@ + -> INSERT 24 : 25 @ 24 +> CHANGE 18 : 19 @ 18 : 19 + +~ private long playerLastActiveTime = EagRuntime.steadyTimeMillis(); + +> INSERT 5 : 7 @ 5 + public byte[] updateCertificate = null; ++ public EaglercraftUUID clientBrandUUID = null; > CHANGE 87 : 88 @ 87 : 88 diff --git a/patches/minecraft/net/minecraft/item/Item.edit.java b/patches/minecraft/net/minecraft/item/Item.edit.java index d0f520c0..b0c1e92d 100644 --- a/patches/minecraft/net/minecraft/item/Item.edit.java +++ b/patches/minecraft/net/minecraft/item/Item.edit.java @@ -28,11 +28,15 @@ ~ protected static EaglercraftRandom itemRand = new EaglercraftRandom(); -> INSERT 884 : 888 @ 884 +> INSERT 884 : 892 @ 884 + -+ public float getHeldItemBrightnessEagler() { ++ public float getHeldItemBrightnessEagler(ItemStack itemStack) { + return 0.0f; + } ++ ++ public boolean shouldUseOnTouchEagler(ItemStack itemStack) { ++ return getItemUseAction(itemStack) != EnumAction.NONE; ++ } > EOF diff --git a/patches/minecraft/net/minecraft/item/ItemBlock.edit.java b/patches/minecraft/net/minecraft/item/ItemBlock.edit.java index 262e5986..34701ff2 100644 --- a/patches/minecraft/net/minecraft/item/ItemBlock.edit.java +++ b/patches/minecraft/net/minecraft/item/ItemBlock.edit.java @@ -10,7 +10,7 @@ > INSERT 120 : 124 @ 120 + -+ public float getHeldItemBrightnessEagler() { ++ public float getHeldItemBrightnessEagler(ItemStack itemStack) { + return this.block.getLightValue() * 0.06667f; + } diff --git a/patches/minecraft/net/minecraft/item/ItemEgg.edit.java b/patches/minecraft/net/minecraft/item/ItemEgg.edit.java index 41da09fc..e49d647d 100644 --- a/patches/minecraft/net/minecraft/item/ItemEgg.edit.java +++ b/patches/minecraft/net/minecraft/item/ItemEgg.edit.java @@ -7,4 +7,11 @@ > DELETE 5 @ 5 : 7 +> INSERT 22 : 26 @ 22 + ++ ++ public boolean shouldUseOnTouchEagler(ItemStack itemStack) { ++ return true; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/item/ItemEnderEye.edit.java b/patches/minecraft/net/minecraft/item/ItemEnderEye.edit.java index b723c560..97f5521b 100644 --- a/patches/minecraft/net/minecraft/item/ItemEnderEye.edit.java +++ b/patches/minecraft/net/minecraft/item/ItemEnderEye.edit.java @@ -7,4 +7,11 @@ > DELETE 8 @ 8 : 10 +> INSERT 134 : 138 @ 134 + ++ ++ public boolean shouldUseOnTouchEagler(ItemStack itemStack) { ++ return true; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/item/ItemEnderPearl.edit.java b/patches/minecraft/net/minecraft/item/ItemEnderPearl.edit.java index c8d222df..76ddab46 100644 --- a/patches/minecraft/net/minecraft/item/ItemEnderPearl.edit.java +++ b/patches/minecraft/net/minecraft/item/ItemEnderPearl.edit.java @@ -22,4 +22,11 @@ ~ --itemstack.stackSize; ~ } +> INSERT 9 : 13 @ 9 + ++ ++ public boolean shouldUseOnTouchEagler(ItemStack itemStack) { ++ return true; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/item/ItemExpBottle.edit.java b/patches/minecraft/net/minecraft/item/ItemExpBottle.edit.java index 41da09fc..85734f1c 100644 --- a/patches/minecraft/net/minecraft/item/ItemExpBottle.edit.java +++ b/patches/minecraft/net/minecraft/item/ItemExpBottle.edit.java @@ -7,4 +7,11 @@ > DELETE 5 @ 5 : 7 +> INSERT 25 : 29 @ 25 + ++ ++ public boolean shouldUseOnTouchEagler(ItemStack itemStack) { ++ return true; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/item/ItemFirework.edit.java b/patches/minecraft/net/minecraft/item/ItemFirework.edit.java index a2b2fd67..f9c9b4d7 100644 --- a/patches/minecraft/net/minecraft/item/ItemFirework.edit.java +++ b/patches/minecraft/net/minecraft/item/ItemFirework.edit.java @@ -11,4 +11,11 @@ + +> INSERT 45 : 49 @ 45 + ++ ++ public boolean shouldUseOnTouchEagler(ItemStack itemStack) { ++ return true; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/item/ItemSnowball.edit.java b/patches/minecraft/net/minecraft/item/ItemSnowball.edit.java index 41da09fc..e49d647d 100644 --- a/patches/minecraft/net/minecraft/item/ItemSnowball.edit.java +++ b/patches/minecraft/net/minecraft/item/ItemSnowball.edit.java @@ -7,4 +7,11 @@ > DELETE 5 @ 5 : 7 +> INSERT 22 : 26 @ 22 + ++ ++ public boolean shouldUseOnTouchEagler(ItemStack itemStack) { ++ return true; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/item/ItemStack.edit.java b/patches/minecraft/net/minecraft/item/ItemStack.edit.java index a616fd58..2133a4c1 100644 --- a/patches/minecraft/net/minecraft/item/ItemStack.edit.java +++ b/patches/minecraft/net/minecraft/item/ItemStack.edit.java @@ -9,10 +9,12 @@ > DELETE 3 @ 3 : 4 -> INSERT 1 : 9 @ 1 +> INSERT 1 : 11 @ 1 + import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; + import net.lax1dude.eaglercraft.v1_8.HString; ++ import net.lax1dude.eaglercraft.v1_8.profanity_filter.ProfanityFilter; ++ + import java.util.Set; + + import com.google.common.collect.HashMultimap; @@ -20,13 +22,61 @@ + import com.google.common.collect.Multimap; + -> DELETE 13 @ 13 : 17 +> INSERT 1 : 2 @ 1 -> CHANGE 185 : 186 @ 185 : 186 ++ import net.minecraft.client.Minecraft; + +> DELETE 12 @ 12 : 16 + +> INSERT 19 : 21 @ 19 + ++ private String profanityFilteredName; ++ private String profanityFilteredNameFiltered; + +> CHANGE 166 : 167 @ 166 : 167 ~ public boolean attemptDamageItem(int amount, EaglercraftRandom rand) { -> CHANGE 249 : 250 @ 249 : 250 +> INSERT 197 : 218 @ 197 + ++ public String getDisplayNameProfanityFilter() { ++ String s = this.getItem().getItemStackDisplayName(this); ++ if (this.stackTagCompound != null && this.stackTagCompound.hasKey("display", 10)) { ++ NBTTagCompound nbttagcompound = this.stackTagCompound.getCompoundTag("display"); ++ if (nbttagcompound.hasKey("Name", 8)) { ++ s = nbttagcompound.getString("Name"); ++ if (Minecraft.getMinecraft().isEnableProfanityFilter()) { ++ if (!s.equals(profanityFilteredName)) { ++ profanityFilteredName = s; ++ profanityFilteredNameFiltered = ProfanityFilter.getInstance().profanityFilterString(s); ++ } ++ if (profanityFilteredNameFiltered != null) { ++ s = profanityFilteredNameFiltered; ++ } ++ } ++ } ++ } ++ ++ return s; ++ } ++ + +> INSERT 36 : 44 @ 36 + ++ return getTooltipImpl(playerIn, advanced, false); ++ } ++ ++ public List getTooltipProfanityFilter(EntityPlayer playerIn, boolean advanced) { ++ return getTooltipImpl(playerIn, advanced, true); ++ } ++ ++ public List getTooltipImpl(EntityPlayer playerIn, boolean advanced, boolean profanityFilter) { + +> CHANGE 1 : 2 @ 1 : 2 + +~ String s = profanityFilter ? this.getDisplayNameProfanityFilter() : this.getDisplayName(); + +> CHANGE 14 : 15 @ 14 : 15 ~ s = s + HString.format("#%04d/%d%s", diff --git a/patches/minecraft/net/minecraft/network/NetHandlerPlayServer.edit.java b/patches/minecraft/net/minecraft/network/NetHandlerPlayServer.edit.java index be1699a0..63c6b730 100644 --- a/patches/minecraft/net/minecraft/network/NetHandlerPlayServer.edit.java +++ b/patches/minecraft/net/minecraft/network/NetHandlerPlayServer.edit.java @@ -5,56 +5,94 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 5 : 6 @ 5 : 9 +> CHANGE 5 : 10 @ 5 : 9 -~ import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageProtocol; +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.client.GameProtocolMessageController; +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketUpdateCertEAG; +~ > CHANGE 3 : 4 @ 3 : 4 ~ import java.util.List; -> INSERT 2 : 4 @ 2 - -+ -+ import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerMinecraftServer; - -> DELETE 25 @ 25 : 29 - -> INSERT 33 : 34 @ 33 - -+ import net.minecraft.network.play.server.S3FPacketCustomPayload; - -> DELETE 2 @ 2 : 3 - -> INSERT 16 : 19 @ 16 - -+ import net.lax1dude.eaglercraft.v1_8.sp.server.socket.IntegratedServerPlayerNetworkManager; -+ import net.lax1dude.eaglercraft.v1_8.sp.server.voice.IntegratedVoiceService; -+ - -> CHANGE 1 : 3 @ 1 : 3 - -~ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; -~ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; - > INSERT 2 : 3 @ 2 + -> CHANGE 1 : 2 @ 1 : 2 +> DELETE 25 @ 25 : 29 + +> DELETE 35 @ 35 : 36 + +> INSERT 16 : 18 @ 16 + ++ import net.lax1dude.eaglercraft.v1_8.sp.server.socket.IntegratedServerPlayerNetworkManager; ++ + +> DELETE 1 @ 1 : 3 + +> INSERT 1 : 5 @ 1 + ++ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ++ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; ++ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; ++ + +> INSERT 1 : 2 @ 1 + ++ + +> CHANGE 1 : 3 @ 1 : 3 ~ public final IntegratedServerPlayerNetworkManager netManager; +~ public final MinecraftServer serverController; -> INSERT 16 : 17 @ 16 +> INSERT 15 : 17 @ 15 + private boolean hasDisconnected = false; ++ private GameProtocolMessageController eaglerMessageController = null; > CHANGE 1 : 3 @ 1 : 2 ~ public NetHandlerPlayServer(MinecraftServer server, IntegratedServerPlayerNetworkManager networkManagerIn, ~ EntityPlayerMP playerIn) { -> CHANGE 35 : 36 @ 35 : 36 +> INSERT 7 : 28 @ 7 + ++ public GameProtocolMessageController getEaglerMessageController() { ++ return eaglerMessageController; ++ } ++ ++ public void setEaglerMessageController(GameProtocolMessageController eaglerMessageController) { ++ this.eaglerMessageController = eaglerMessageController; ++ } ++ ++ public GamePluginMessageProtocol getEaglerMessageProtocol() { ++ return eaglerMessageController != null ? eaglerMessageController.protocol : null; ++ } ++ ++ public void sendEaglerMessage(GameMessagePacket packet) { ++ try { ++ eaglerMessageController.sendPacket(packet); ++ } catch (IOException e) { ++ logger.error("Failed to send eaglercraft plugin message packet: " + packet); ++ logger.error(e); ++ } ++ } ++ + +> DELETE 3 @ 3 : 4 + +> DELETE 7 @ 7 : 8 + +> INSERT 14 : 17 @ 14 + ++ if (this.eaglerMessageController != null) { ++ this.eaglerMessageController.flush(); ++ } + +> CHANGE 2 : 3 @ 2 : 3 ~ public IntegratedServerPlayerNetworkManager getNetworkManager() { @@ -140,7 +178,11 @@ ~ } ~ tileentitysign.signText[i] = new ChatComponentText(s); -> DELETE 21 @ 21 : 22 +> CHANGE 17 : 18 @ 17 : 18 + +~ return EagRuntime.steadyTimeMillis(); + +> DELETE 3 @ 3 : 4 > CHANGE 5 : 10 @ 5 : 11 @@ -187,21 +229,8 @@ + s = net.minecraft.util.StringUtils.translateControlCodesAlternate(s); + } -> INSERT 5 : 33 @ 5 +> INSERT 5 : 28 @ 5 -+ } else if ("EAG|Skins-1.8".equals(c17packetcustompayload.getChannelName())) { -+ byte[] r = new byte[c17packetcustompayload.getBufferData().readableBytes()]; -+ c17packetcustompayload.getBufferData().readBytes(r); -+ ((EaglerMinecraftServer) serverController).getSkinService().processPacket(r, playerEntity); -+ } else if ("EAG|Capes-1.8".equals(c17packetcustompayload.getChannelName())) { -+ byte[] r = new byte[c17packetcustompayload.getBufferData().readableBytes()]; -+ c17packetcustompayload.getBufferData().readBytes(r); -+ ((EaglerMinecraftServer) serverController).getCapeService().processPacket(r, playerEntity); -+ } else if ("EAG|Voice-1.8".equals(c17packetcustompayload.getChannelName())) { -+ IntegratedVoiceService vcs = ((EaglerMinecraftServer) serverController).getVoiceService(); -+ if (vcs != null) { -+ vcs.processPacket(c17packetcustompayload.getBufferData(), playerEntity); -+ } + } else if ("EAG|MyUpdCert-1.8".equals(c17packetcustompayload.getChannelName())) { + if (playerEntity.updateCertificate == null) { + PacketBuffer pb = c17packetcustompayload.getBufferData(); @@ -212,11 +241,19 @@ + for (int i = 0, l = lst.size(); i < l; ++i) { + EntityPlayerMP player = lst.get(i); + if (player != playerEntity) { -+ player.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload("EAG|UpdateCert-1.8", -+ new PacketBuffer(Unpooled.buffer(cert, cert.length).writerIndex(cert.length)))); ++ player.playerNetServerHandler.sendEaglerMessage(new SPacketUpdateCertEAG(cert)); + } + } + } ++ } else { ++ try { ++ eaglerMessageController.handlePacket(c17packetcustompayload.getChannelName(), ++ c17packetcustompayload.getBufferData()); ++ } catch (IOException e) { ++ logger.error("Couldn't read \"{}\" packet as an eaglercraft plugin message!", ++ c17packetcustompayload.getChannelName()); ++ logger.error(e); ++ } > DELETE 1 @ 1 : 2 diff --git a/patches/minecraft/net/minecraft/network/login/client/C00PacketLoginStart.edit.java b/patches/minecraft/net/minecraft/network/login/client/C00PacketLoginStart.edit.java index 2bcf0dd6..f45ef6a7 100644 --- a/patches/minecraft/net/minecraft/network/login/client/C00PacketLoginStart.edit.java +++ b/patches/minecraft/net/minecraft/network/login/client/C00PacketLoginStart.edit.java @@ -13,32 +13,41 @@ ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; -> INSERT 6 : 8 @ 6 +> INSERT 6 : 10 @ 6 + private byte[] skin; + private byte[] cape; ++ private byte[] protocols; ++ private EaglercraftUUID brandUUID; -> CHANGE 4 : 5 @ 4 : 5 +> CHANGE 4 : 6 @ 4 : 5 -~ public C00PacketLoginStart(GameProfile profileIn, byte[] skin, byte[] cape) { +~ public C00PacketLoginStart(GameProfile profileIn, byte[] skin, byte[] cape, byte[] protocols, +~ EaglercraftUUID brandUUID) { -> INSERT 1 : 3 @ 1 +> INSERT 1 : 5 @ 1 + this.skin = skin; + this.cape = cape; ++ this.protocols = protocols; ++ this.brandUUID = brandUUID; -> CHANGE 3 : 6 @ 3 : 4 +> CHANGE 3 : 8 @ 3 : 4 ~ this.profile = new GameProfile((EaglercraftUUID) null, parPacketBuffer.readStringFromBuffer(16)); ~ this.skin = parPacketBuffer.readByteArray(); ~ this.cape = parPacketBuffer.readableBytes() > 0 ? parPacketBuffer.readByteArray() : null; +~ this.protocols = parPacketBuffer.readableBytes() > 0 ? parPacketBuffer.readByteArray() : null; +~ this.brandUUID = parPacketBuffer.readableBytes() > 0 ? parPacketBuffer.readUuid() : null; -> INSERT 4 : 6 @ 4 +> INSERT 4 : 8 @ 4 + parPacketBuffer.writeByteArray(this.skin); + parPacketBuffer.writeByteArray(this.cape); ++ parPacketBuffer.writeByteArray(this.protocols); ++ parPacketBuffer.writeUuid(brandUUID); -> INSERT 9 : 17 @ 9 +> INSERT 9 : 25 @ 9 + + public byte[] getSkin() { @@ -48,5 +57,13 @@ + public byte[] getCape() { + return this.cape; + } ++ ++ public byte[] getProtocols() { ++ return this.protocols; ++ } ++ ++ public EaglercraftUUID getBrandUUID() { ++ return this.brandUUID; ++ } > EOF diff --git a/patches/minecraft/net/minecraft/network/login/server/S02PacketLoginSuccess.edit.java b/patches/minecraft/net/minecraft/network/login/server/S02PacketLoginSuccess.edit.java index 1d6e01eb..4893bd47 100644 --- a/patches/minecraft/net/minecraft/network/login/server/S02PacketLoginSuccess.edit.java +++ b/patches/minecraft/net/minecraft/network/login/server/S02PacketLoginSuccess.edit.java @@ -13,12 +13,38 @@ ~ ~ import net.lax1dude.eaglercraft.v1_8.mojang.authlib.GameProfile; -> CHANGE 17 : 18 @ 17 : 18 +> INSERT 6 : 7 @ 6 ++ private int selectedProtocol = 3; + +> CHANGE 4 : 5 @ 4 : 5 + +~ public S02PacketLoginSuccess(GameProfile profileIn, int selectedProtocol) { + +> INSERT 1 : 2 @ 1 + ++ this.selectedProtocol = selectedProtocol; + +> CHANGE 5 : 7 @ 5 : 6 + +~ selectedProtocol = parPacketBuffer.readableBytes() > 0 ? parPacketBuffer.readShort() : 3; ~ EaglercraftUUID uuid = EaglercraftUUID.fromString(s); > CHANGE 4 : 5 @ 4 : 5 ~ EaglercraftUUID uuid = this.profile.getId(); +> INSERT 2 : 5 @ 2 + ++ if (selectedProtocol != 3) { ++ parPacketBuffer.writeShort(selectedProtocol); ++ } + +> INSERT 9 : 13 @ 9 + ++ ++ public int getSelectedProtocol() { ++ return selectedProtocol; ++ } + > EOF diff --git a/patches/minecraft/net/minecraft/pathfinding/PathNavigate.edit.java b/patches/minecraft/net/minecraft/pathfinding/PathNavigate.edit.java index eb2d4efd..f3e387b1 100644 --- a/patches/minecraft/net/minecraft/pathfinding/PathNavigate.edit.java +++ b/patches/minecraft/net/minecraft/pathfinding/PathNavigate.edit.java @@ -7,7 +7,15 @@ > DELETE 7 @ 7 : 9 -> CHANGE 134 : 135 @ 134 : 135 +> DELETE 45 @ 45 : 46 + +> DELETE 4 @ 4 : 5 + +> DELETE 19 @ 19 : 20 + +> DELETE 5 @ 5 : 6 + +> CHANGE 57 : 58 @ 57 : 58 ~ List list = this.worldObj.getCollidingBoundingBoxes(this.theEntity, diff --git a/patches/minecraft/net/minecraft/profiler/Profiler.edit.java b/patches/minecraft/net/minecraft/profiler/Profiler.edit.java deleted file mode 100644 index c8bfb9d9..00000000 --- a/patches/minecraft/net/minecraft/profiler/Profiler.edit.java +++ /dev/null @@ -1,21 +0,0 @@ - -# Eagler Context Redacted Diff -# Copyright (c) 2024 lax1dude. All rights reserved. - -# Version: 1.0 -# Author: lax1dude - -> DELETE 2 @ 2 : 4 - -> DELETE 4 @ 4 : 6 - -> INSERT 1 : 7 @ 1 - -+ import com.google.common.collect.Lists; -+ import com.google.common.collect.Maps; -+ -+ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; -+ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -+ - -> EOF diff --git a/patches/minecraft/net/minecraft/scoreboard/ScoreObjective.edit.java b/patches/minecraft/net/minecraft/scoreboard/ScoreObjective.edit.java index 0c659788..791e0c67 100644 --- a/patches/minecraft/net/minecraft/scoreboard/ScoreObjective.edit.java +++ b/patches/minecraft/net/minecraft/scoreboard/ScoreObjective.edit.java @@ -5,6 +5,27 @@ # Version: 1.0 # Author: lax1dude -> DELETE 2 @ 2 : 5 +> CHANGE 2 : 4 @ 2 : 4 + +~ import net.lax1dude.eaglercraft.v1_8.profanity_filter.ProfanityFilter; +~ import net.minecraft.client.Minecraft; + +> INSERT 7 : 8 @ 7 + ++ private String displayNameProfanityFilter; + +> INSERT 25 : 36 @ 25 + ++ public String getDisplayNameProfanityFilter() { ++ if (Minecraft.getMinecraft().isEnableProfanityFilter()) { ++ if (displayNameProfanityFilter == null) { ++ displayNameProfanityFilter = ProfanityFilter.getInstance().profanityFilterString(displayName); ++ } ++ return displayNameProfanityFilter; ++ } else { ++ return this.displayName; ++ } ++ } ++ > EOF diff --git a/patches/minecraft/net/minecraft/server/MinecraftServer.edit.java b/patches/minecraft/net/minecraft/server/MinecraftServer.edit.java index a5f3a732..dbf2e050 100644 --- a/patches/minecraft/net/minecraft/server/MinecraftServer.edit.java +++ b/patches/minecraft/net/minecraft/server/MinecraftServer.edit.java @@ -20,9 +20,10 @@ > DELETE 2 @ 2 : 4 -> CHANGE 1 : 6 @ 1 : 4 +> CHANGE 1 : 7 @ 1 : 4 ~ +~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; ~ import net.lax1dude.eaglercraft.v1_8.futures.FutureTask; @@ -30,12 +31,10 @@ > DELETE 8 @ 8 : 11 -> CHANGE 1 : 2 @ 1 : 3 +> CHANGE 1 : 2 @ 1 : 5 ~ import net.minecraft.network.play.server.S41PacketServerDifficulty; -> DELETE 1 @ 1 : 2 - > DELETE 7 @ 7 : 8 > CHANGE 10 : 11 @ 10 : 12 @@ -59,7 +58,7 @@ ~ protected final List playersOnline = Lists.newArrayList(); -> CHANGE 2 : 3 @ 2 : 5 +> CHANGE 1 : 2 @ 1 : 5 ~ private final EaglercraftRandom random = new EaglercraftRandom(); @@ -81,7 +80,7 @@ > CHANGE 1 : 2 @ 1 : 4 -~ protected final Queue> futureTaskQueue = new LinkedList(); +~ protected final Queue> futureTaskQueue = new LinkedList<>(); > CHANGE 1 : 4 @ 1 : 2 @@ -128,7 +127,21 @@ + } + -> DELETE 26 @ 26 : 29 +> CHANGE 12 : 14 @ 12 : 14 + +~ this.worldServers[j] = (WorldServer) (new DemoWorldServer(this, isavehandler, worldinfo, b0)) +~ .init(); + +> CHANGE 1 : 2 @ 1 : 3 + +~ this.worldServers[j] = (WorldServer) (new WorldServer(this, isavehandler, worldinfo, b0)).init(); + +> CHANGE 4 : 6 @ 4 : 6 + +~ this.worldServers[j] = (WorldServer) (new WorldServerMulti(this, isavehandler, b0, +~ this.worldServers[0])).init(); + +> DELETE 3 @ 3 : 6 > CHANGE 3 : 11 @ 3 : 5 @@ -206,7 +219,15 @@ > DELETE 15 @ 15 : 40 -> DELETE 17 @ 17 : 24 +> CHANGE 7 : 8 @ 7 : 8 + +~ long i = EagRuntime.nanoTime(); + +> DELETE 3 @ 3 : 5 + +> DELETE 2 @ 2 : 3 + +> DELETE 1 @ 1 : 8 > CHANGE 1 : 8 @ 1 : 4 @@ -222,21 +243,57 @@ ~ this.isSpawnChunksLoaded = loadSpawnChunks; -> DELETE 13 @ 13 : 16 +> DELETE 3 @ 3 : 4 -> DELETE 1 @ 1 : 5 +> DELETE 2 @ 2 : 3 -> CHANGE 58 : 59 @ 58 : 59 +> CHANGE 2 : 3 @ 2 : 16 + +~ this.tickTimeArray[this.tickCounter % 100] = EagRuntime.nanoTime() - i; + +> DELETE 3 @ 3 : 4 + +> DELETE 6 @ 6 : 8 + +> CHANGE 1 : 2 @ 1 : 2 + +~ long i = EagRuntime.nanoTime(); + +> DELETE 2 @ 2 : 3 + +> DELETE 1 @ 1 : 2 + +> DELETE 4 @ 4 : 5 + +> DELETE 2 @ 2 : 4 + +> DELETE 17 @ 17 : 19 + +> DELETE 1 @ 1 : 3 + +> CHANGE 2 : 3 @ 2 : 3 + +~ this.timeOfLastDimensionTick[j][this.tickCounter % 100] = EagRuntime.nanoTime() - i; + +> CHANGE 2 : 3 @ 2 : 5 ~ EaglerIntegratedServerWorker.tick(); -> DELETE 20 @ 20 : 24 +> DELETE 1 @ 1 : 2 + +> DELETE 4 @ 4 : 6 + +> DELETE 11 @ 11 : 15 > CHANGE 29 : 30 @ 29 : 30 ~ return "eagler"; -> CHANGE 28 : 29 @ 28 : 29 +> CHANGE 5 : 6 @ 5 : 8 + +~ return "N/A (disabled)"; + +> CHANGE 20 : 21 @ 20 : 21 ~ List list = this.commandManager.getTabCompletionOptions(sender, input, pos); @@ -290,7 +347,11 @@ > DELETE 28 @ 28 : 32 -> DELETE 20 @ 20 : 36 +> CHANGE 1 : 2 @ 1 : 2 + +~ return EagRuntime.steadyTimeMillis(); + +> DELETE 18 @ 18 : 34 > CHANGE 4 : 7 @ 4 : 6 diff --git a/patches/minecraft/net/minecraft/server/management/ServerConfigurationManager.edit.java b/patches/minecraft/net/minecraft/server/management/ServerConfigurationManager.edit.java index 412e71cd..95bed2d7 100644 --- a/patches/minecraft/net/minecraft/server/management/ServerConfigurationManager.edit.java +++ b/patches/minecraft/net/minecraft/server/management/ServerConfigurationManager.edit.java @@ -26,9 +26,14 @@ + import net.minecraft.util.ChatComponentText; -> CHANGE 12 : 17 @ 12 : 14 +> CHANGE 12 : 22 @ 12 : 14 +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageConstants; +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageProtocol; +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.client.GameProtocolMessageController; +~ import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketUpdateCertEAG; ~ import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerMinecraftServer; +~ import net.lax1dude.eaglercraft.v1_8.sp.server.WorldsDB; ~ import net.lax1dude.eaglercraft.v1_8.sp.server.socket.IntegratedServerPlayerNetworkManager; ~ import net.lax1dude.eaglercraft.v1_8.sp.server.voice.IntegratedVoiceService; ~ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; @@ -57,16 +62,25 @@ ~ this.maxPlayers = 100; -> CHANGE 2 : 4 @ 2 : 8 +> CHANGE 2 : 6 @ 2 : 8 -~ public void initializeConnectionToPlayer(IntegratedServerPlayerNetworkManager netManager, EntityPlayerMP playerIn) { +~ public void initializeConnectionToPlayer(IntegratedServerPlayerNetworkManager netManager, EntityPlayerMP playerIn, +~ int protocolVersion, EaglercraftUUID clientBrandUUID) { +~ playerIn.clientBrandUUID = clientBrandUUID; ~ GameProfile gameprofile1 = playerIn.getGameProfile(); > CHANGE 3 : 4 @ 3 : 7 ~ String s1 = "channel:" + netManager.playerChannel; -> DELETE 12 @ 12 : 14 +> INSERT 8 : 12 @ 8 + ++ nethandlerplayserver.setEaglerMessageController(new GameProtocolMessageController( ++ GamePluginMessageProtocol.getByVersion(protocolVersion), GamePluginMessageConstants.SERVER_TO_CLIENT, ++ GameProtocolMessageController.createServerHandler(protocolVersion, nethandlerplayserver), ++ (ch, msg) -> nethandlerplayserver.sendPacket(new S3FPacketCustomPayload(ch, msg)))); + +> DELETE 4 @ 4 : 6 > INSERT 1 : 4 @ 1 @@ -90,17 +104,13 @@ + playerIn.addChatMessage(shaderF4Msg); + } -> INSERT 23 : 35 @ 23 +> INSERT 23 : 31 @ 23 + if (EagRuntime.getConfiguration().allowUpdateSvc()) { + for (int i = 0, l = playerEntityList.size(); i < l; ++i) { + EntityPlayerMP playerItr = playerEntityList.get(i); + if (playerItr != playerIn && playerItr.updateCertificate != null) { -+ nethandlerplayserver -+ .sendPacket(new S3FPacketCustomPayload("EAG|UpdateCert-1.8", -+ new PacketBuffer(Unpooled -+ .buffer(playerItr.updateCertificate, playerItr.updateCertificate.length) -+ .writerIndex(playerItr.updateCertificate.length)))); ++ nethandlerplayserver.sendEaglerMessage(new SPacketUpdateCertEAG(playerItr.updateCertificate)); + } + } + } @@ -175,7 +185,20 @@ ~ for (int i = 0, l = arraylist.size(); i < l; ++i) { ~ arraylist.get(i).playerNetServerHandler.kickPlayerFromServer("You logged in from another location"); -> CHANGE 205 : 206 @ 205 : 206 +> INSERT 32 : 34 @ 32 + ++ entityplayermp.updateCertificate = playerIn.updateCertificate; ++ entityplayermp.clientBrandUUID = playerIn.clientBrandUUID; + +> DELETE 75 @ 75 : 76 + +> DELETE 35 @ 35 : 36 + +> DELETE 1 @ 1 : 2 + +> DELETE 8 @ 8 : 10 + +> CHANGE 49 : 50 @ 49 : 50 ~ for (int i = 0, l = this.playerEntityList.size(); i < l; ++i) { @@ -237,13 +260,11 @@ ~ String name = playerIn.getName(); ~ StatisticsFile statisticsfile = (StatisticsFile) this.playerStatFiles.get(name); -> CHANGE 1 : 2 @ 1 : 2 +> CHANGE 1 : 4 @ 1 : 11 -~ VFile2 file1 = new VFile2(this.mcServer.worldServerForDimension(0).getSaveHandler().getWorldDirectory(), - -> CHANGE 1 : 2 @ 1 : 9 - -~ VFile2 file2 = new VFile2(file1, name + ".json"); +~ VFile2 file1 = WorldsDB +~ .newVFile(this.mcServer.worldServerForDimension(0).getSaveHandler().getWorldDirectory(), "stats"); +~ VFile2 file2 = WorldsDB.newVFile(file1, name + ".json"); > CHANGE 2 : 3 @ 2 : 3 diff --git a/patches/minecraft/net/minecraft/server/network/NetHandlerLoginServer.edit.java b/patches/minecraft/net/minecraft/server/network/NetHandlerLoginServer.edit.java index fc78fa73..3570a719 100644 --- a/patches/minecraft/net/minecraft/server/network/NetHandlerLoginServer.edit.java +++ b/patches/minecraft/net/minecraft/server/network/NetHandlerLoginServer.edit.java @@ -5,11 +5,13 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 3 : 7 @ 3 : 15 +> CHANGE 3 : 9 @ 3 : 15 ~ import net.lax1dude.eaglercraft.v1_8.mojang.authlib.GameProfile; -~ import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; +~ import net.lax1dude.eaglercraft.v1_8.ClientUUIDLoadingCache; +~ import net.lax1dude.eaglercraft.v1_8.EaglerInputStream; ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +~ import net.lax1dude.eaglercraft.v1_8.EaglercraftVersion; ~ import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerMinecraftServer; > CHANGE 1 : 2 @ 1 : 2 @@ -22,11 +24,14 @@ > DELETE 2 @ 2 : 3 -> INSERT 2 : 5 @ 2 +> INSERT 2 : 8 @ 2 + import net.lax1dude.eaglercraft.v1_8.sp.server.socket.IntegratedServerPlayerNetworkManager; + import net.lax1dude.eaglercraft.v1_8.sp.server.voice.IntegratedVoiceService; + ++ import java.io.DataInputStream; ++ import java.io.IOException; ++ > CHANGE 1 : 3 @ 1 : 3 @@ -35,18 +40,18 @@ > DELETE 2 @ 2 : 3 +> DELETE 1 @ 1 : 3 + > CHANGE 1 : 2 @ 1 : 2 -~ private static final EaglercraftRandom RANDOM = new EaglercraftRandom(); - -> CHANGE 2 : 3 @ 2 : 3 - ~ public final IntegratedServerPlayerNetworkManager networkManager; -> INSERT 3 : 5 @ 3 +> INSERT 3 : 7 @ 3 + private byte[] loginSkinPacket; + private byte[] loginCapePacket; ++ private int selectedProtocol = 3; ++ private EaglercraftUUID clientBrandUUID; > DELETE 1 @ 1 : 2 @@ -55,35 +60,40 @@ ~ public NetHandlerLoginServer(MinecraftServer parMinecraftServer, ~ IntegratedServerPlayerNetworkManager parNetworkManager) { -> INSERT 15 : 25 @ 15 +> DELETE 2 @ 2 : 3 -+ ((EaglerMinecraftServer) field_181025_l.mcServer).getSkinService() -+ .processLoginPacket(this.loginSkinPacket, field_181025_l); -+ if (this.loginCapePacket != null) { -+ ((EaglerMinecraftServer) field_181025_l.mcServer).getCapeService() -+ .processLoginPacket(this.loginCapePacket, field_181025_l); -+ } -+ IntegratedVoiceService svc = ((EaglerMinecraftServer) field_181025_l.mcServer).getVoiceService(); -+ if (svc != null) { -+ svc.handlePlayerLoggedIn(entityplayermp); -+ } +> CHANGE 11 : 23 @ 11 : 12 + +~ this.field_181025_l, this.selectedProtocol, this.clientBrandUUID); +~ ((EaglerMinecraftServer) field_181025_l.mcServer).getSkinService() +~ .processLoginPacket(this.loginSkinPacket, field_181025_l, 3); // singleplayer always sends V3 +~ // skin in handshake +~ if (this.loginCapePacket != null) { +~ ((EaglerMinecraftServer) field_181025_l.mcServer).getCapeService() +~ .processLoginPacket(this.loginCapePacket, field_181025_l); +~ } +~ IntegratedVoiceService svc = ((EaglerMinecraftServer) field_181025_l.mcServer).getVoiceService(); +~ if (svc != null) { +~ svc.handlePlayerLoggedIn(entityplayermp); +~ } > CHANGE 23 : 24 @ 23 : 29 ~ String s = this.server.getConfigurationManager().allowUserToConnect(this.loginGameProfile); -> DELETE 4 @ 4 : 15 +> CHANGE 4 : 6 @ 4 : 16 -> INSERT 1 : 2 @ 1 +~ this.networkManager.sendPacket(new S02PacketLoginSuccess(this.loginGameProfile, this.selectedProtocol)); +~ this.networkManager.setConnectionState(EnumConnectionState.PLAY); -+ this.networkManager.setConnectionState(EnumConnectionState.PLAY); - -> CHANGE 6 : 18 @ 6 : 8 +> CHANGE 6 : 20 @ 6 : 8 ~ entityplayermp = this.server.getConfigurationManager().createPlayerForUser(this.loginGameProfile); -~ this.server.getConfigurationManager().initializeConnectionToPlayer(this.networkManager, entityplayermp); +~ this.server.getConfigurationManager().initializeConnectionToPlayer(this.networkManager, entityplayermp, +~ this.selectedProtocol, this.clientBrandUUID); ~ ((EaglerMinecraftServer) entityplayermp.mcServer).getSkinService() -~ .processLoginPacket(this.loginSkinPacket, entityplayermp); +~ .processLoginPacket(this.loginSkinPacket, entityplayermp, 3); // singleplayer always sends V3 +~ // skin in handshake ~ if (this.loginCapePacket != null) { ~ ((EaglerMinecraftServer) entityplayermp.mcServer).getCapeService() ~ .processLoginPacket(this.loginCapePacket, entityplayermp); @@ -98,11 +108,44 @@ ~ ? this.loginGameProfile.toString() + " (channel:" + this.networkManager.playerChannel + ")" ~ : ("channel:" + this.networkManager.playerChannel); -> CHANGE 5 : 9 @ 5 : 14 +> CHANGE 5 : 25 @ 5 : 10 + +~ if (c00packetloginstart.getProtocols() != null) { +~ try { +~ DataInputStream dis = new DataInputStream(new EaglerInputStream(c00packetloginstart.getProtocols())); +~ int maxSupported = -1; +~ int protocolCount = dis.readUnsignedShort(); +~ for (int i = 0; i < protocolCount; ++i) { +~ int p = dis.readUnsignedShort(); +~ if ((p == 3 || p == 4) && p > maxSupported) { +~ maxSupported = p; +~ } +~ } +~ if (maxSupported != -1) { +~ selectedProtocol = maxSupported; +~ } else { +~ this.closeConnection("Unknown protocol!"); +~ return; +~ } +~ } catch (IOException ex) { +~ selectedProtocol = 3; +~ } + +> CHANGE 1 : 2 @ 1 : 2 + +~ selectedProtocol = 3; + +> CHANGE 1 : 11 @ 1 : 2 ~ this.loginGameProfile = this.getOfflineProfile(c00packetloginstart.getProfile()); ~ this.loginSkinPacket = c00packetloginstart.getSkin(); ~ this.loginCapePacket = c00packetloginstart.getCape(); +~ this.clientBrandUUID = selectedProtocol <= 3 ? EaglercraftVersion.legacyClientUUIDInSharedWorld +~ : c00packetloginstart.getBrandUUID(); +~ if (ClientUUIDLoadingCache.PENDING_UUID.equals(clientBrandUUID) +~ || ClientUUIDLoadingCache.VANILLA_UUID.equals(clientBrandUUID)) { +~ this.clientBrandUUID = null; +~ } ~ this.currentLoginState = NetHandlerLoginServer.LoginState.READY_TO_ACCEPT; > DELETE 3 @ 3 : 15 diff --git a/patches/minecraft/net/minecraft/tileentity/TileEntitySign.edit.java b/patches/minecraft/net/minecraft/tileentity/TileEntitySign.edit.java index 0aa6b770..1605d22e 100644 --- a/patches/minecraft/net/minecraft/tileentity/TileEntitySign.edit.java +++ b/patches/minecraft/net/minecraft/tileentity/TileEntitySign.edit.java @@ -5,7 +5,10 @@ # Version: 1.0 # Author: lax1dude -> DELETE 2 @ 2 : 3 +> CHANGE 2 : 4 @ 2 : 3 + +~ import net.lax1dude.eaglercraft.v1_8.profanity_filter.ProfanityFilter; +~ import net.minecraft.client.Minecraft; > DELETE 10 @ 10 : 11 @@ -14,7 +17,33 @@ + import org.json.JSONException; + -> CHANGE 75 : 76 @ 75 : 76 +> INSERT 3 : 4 @ 3 + ++ protected IChatComponent[] signTextProfanityFilter = null; + +> INSERT 16 : 35 @ 16 + ++ public IChatComponent[] getSignTextProfanityFilter() { ++ if (Minecraft.getMinecraft().isEnableProfanityFilter()) { ++ if (signTextProfanityFilter == null) { ++ signTextProfanityFilter = new IChatComponent[4]; ++ ProfanityFilter filter = ProfanityFilter.getInstance(); ++ for (int i = 0; i < 4; ++i) { ++ signTextProfanityFilter[i] = filter.profanityFilterChatComponent(signText[i]); ++ } ++ } ++ return signTextProfanityFilter; ++ } else { ++ return signText; ++ } ++ } ++ ++ public void clearProfanityFilterCache() { ++ signTextProfanityFilter = null; ++ } ++ + +> CHANGE 56 : 57 @ 56 : 57 ~ } catch (JSONException var8) { diff --git a/patches/minecraft/net/minecraft/util/ChatComponentStyle.edit.java b/patches/minecraft/net/minecraft/util/ChatComponentStyle.edit.java index 9b30fde2..41e7323a 100644 --- a/patches/minecraft/net/minecraft/util/ChatComponentStyle.edit.java +++ b/patches/minecraft/net/minecraft/util/ChatComponentStyle.edit.java @@ -23,4 +23,11 @@ ~ for (int i = 0, l = this.siblings.size(); i < l; ++i) { ~ this.siblings.get(i).getChatStyle().setParentStyle(this.style); +> INSERT 6 : 10 @ 6 + ++ public ChatStyle getChatStyleIfPresent() { ++ return this.style; ++ } ++ + > EOF diff --git a/patches/minecraft/net/minecraft/util/ChatStyle.edit.java b/patches/minecraft/net/minecraft/util/ChatStyle.edit.java index 457178d8..d4a9a956 100644 --- a/patches/minecraft/net/minecraft/util/ChatStyle.edit.java +++ b/patches/minecraft/net/minecraft/util/ChatStyle.edit.java @@ -80,7 +80,7 @@ + } -> CHANGE 1 : 12 @ 1 : 13 +> CHANGE 1 : 13 @ 1 : 12 ~ if (jsonobject.has("hoverEvent")) { ~ JSONObject jsonobject2 = jsonobject.getJSONObject("hoverEvent"); @@ -88,13 +88,22 @@ ~ String jsonprimitive2 = jsonobject2.getString("action"); ~ HoverEvent.Action hoverevent$action = jsonprimitive2 == null ? null ~ : HoverEvent.Action.getValueByCanonicalName(jsonprimitive2); -~ IChatComponent ichatcomponent = JSONTypeProvider.deserializeNoCast(jsonobject2.get("value"), -~ IChatComponent.class); -~ if (hoverevent$action != null && ichatcomponent != null -~ && hoverevent$action.shouldAllowInChat()) { -~ chatstyle.chatHoverEvent = new HoverEvent(hoverevent$action, ichatcomponent); +~ Object val = jsonobject2.opt("value"); +~ if (val == null) { +~ val = jsonobject2.opt("contents"); +~ if (val == null) { +~ throw new JSONException( +~ "JSONObject[\"value\"] or JSONObject[\"contents\"] could not be found"); -> DELETE 2 @ 2 : 4 +> INSERT 2 : 7 @ 2 + ++ IChatComponent ichatcomponent = JSONTypeProvider.deserializeNoCast(val, IChatComponent.class); ++ if (hoverevent$action != null && ichatcomponent != null ++ && hoverevent$action.shouldAllowInChat()) { ++ chatstyle.chatHoverEvent = new HoverEvent(hoverevent$action, ichatcomponent); ++ } + +> DELETE 1 @ 1 : 3 > CHANGE 1 : 3 @ 1 : 3 diff --git a/patches/minecraft/net/minecraft/util/MouseHelper.edit.java b/patches/minecraft/net/minecraft/util/MouseHelper.edit.java index 6a2c4b6d..38ec9493 100644 --- a/patches/minecraft/net/minecraft/util/MouseHelper.edit.java +++ b/patches/minecraft/net/minecraft/util/MouseHelper.edit.java @@ -5,9 +5,15 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 2 : 4 @ 2 : 4 +> CHANGE 2 : 5 @ 2 : 4 ~ import net.lax1dude.eaglercraft.v1_8.Display; ~ import net.lax1dude.eaglercraft.v1_8.Mouse; +~ import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; + +> CHANGE 17 : 19 @ 17 : 19 + +~ this.deltaX = PointerInputAbstraction.getVCursorDX(); +~ this.deltaY = PointerInputAbstraction.getVCursorDY(); > EOF diff --git a/patches/minecraft/net/minecraft/util/MovementInputFromOptions.edit.java b/patches/minecraft/net/minecraft/util/MovementInputFromOptions.edit.java index a8dd6648..20acaad8 100644 --- a/patches/minecraft/net/minecraft/util/MovementInputFromOptions.edit.java +++ b/patches/minecraft/net/minecraft/util/MovementInputFromOptions.edit.java @@ -5,6 +5,38 @@ # Version: 1.0 # Author: lax1dude -> DELETE 3 @ 3 : 4 +> INSERT 2 : 4 @ 2 + ++ import net.lax1dude.eaglercraft.v1_8.touch_gui.EnumTouchControl; ++ import net.lax1dude.eaglercraft.v1_8.touch_gui.TouchControls; + +> DELETE 1 @ 1 : 2 + +> CHANGE 11 : 14 @ 11 : 12 + +~ if (this.gameSettings.keyBindForward.isKeyDown() || TouchControls.isPressed(EnumTouchControl.DPAD_UP) +~ || TouchControls.isPressed(EnumTouchControl.DPAD_UP_LEFT) +~ || TouchControls.isPressed(EnumTouchControl.DPAD_UP_RIGHT)) { + +> CHANGE 3 : 4 @ 3 : 4 + +~ if (this.gameSettings.keyBindBack.isKeyDown() || TouchControls.isPressed(EnumTouchControl.DPAD_DOWN)) { + +> CHANGE 3 : 5 @ 3 : 4 + +~ if (this.gameSettings.keyBindLeft.isKeyDown() || TouchControls.isPressed(EnumTouchControl.DPAD_LEFT) +~ || TouchControls.isPressed(EnumTouchControl.DPAD_UP_LEFT)) { + +> CHANGE 3 : 5 @ 3 : 4 + +~ if (this.gameSettings.keyBindRight.isKeyDown() || TouchControls.isPressed(EnumTouchControl.DPAD_RIGHT) +~ || TouchControls.isPressed(EnumTouchControl.DPAD_UP_RIGHT)) { + +> CHANGE 3 : 7 @ 3 : 5 + +~ this.jump = this.gameSettings.keyBindJump.isKeyDown() || TouchControls.isPressed(EnumTouchControl.JUMP) +~ || TouchControls.isPressed(EnumTouchControl.FLY_UP); +~ this.sneak = this.gameSettings.keyBindSneak.isKeyDown() || TouchControls.getSneakToggled() +~ || TouchControls.isPressed(EnumTouchControl.FLY_DOWN); > EOF diff --git a/patches/minecraft/net/minecraft/util/Session.edit.java b/patches/minecraft/net/minecraft/util/Session.edit.java index f799854b..6dc89810 100644 --- a/patches/minecraft/net/minecraft/util/Session.edit.java +++ b/patches/minecraft/net/minecraft/util/Session.edit.java @@ -19,7 +19,7 @@ > CHANGE 1 : 2 @ 1 : 7 -~ private static final EaglercraftUUID offlineUUID; +~ private static final EaglercraftUUID outOfGameUUID; > CHANGE 1 : 3 @ 1 : 3 @@ -39,7 +39,7 @@ > CHANGE 2 : 4 @ 2 : 4 ~ public void reset() { -~ update(EaglerProfile.getName(), offlineUUID); +~ update(EaglerProfile.getName(), outOfGameUUID); > CHANGE 2 : 4 @ 2 : 9 @@ -51,7 +51,7 @@ ~ static { ~ byte[] bytes = new byte[16]; ~ (new EaglercraftRandom()).nextBytes(bytes); -~ offlineUUID = new EaglercraftUUID(bytes); +~ outOfGameUUID = new EaglercraftUUID(bytes); > DELETE 2 @ 2 : 23 diff --git a/patches/minecraft/net/minecraft/util/StringTranslate.edit.java b/patches/minecraft/net/minecraft/util/StringTranslate.edit.java index 7ff5bb50..b9f1116d 100644 --- a/patches/minecraft/net/minecraft/util/StringTranslate.edit.java +++ b/patches/minecraft/net/minecraft/util/StringTranslate.edit.java @@ -41,7 +41,7 @@ > CHANGE 1 : 25 @ 1 : 9 ~ public static void initClient() { -~ try (InputStream inputstream = EagRuntime.getResourceStream("/assets/minecraft/lang/en_US.lang")) { +~ try (InputStream inputstream = EagRuntime.getRequiredResourceStream("/assets/minecraft/lang/en_US.lang")) { ~ initServer(IOUtils.readLines(inputstream, StandardCharsets.UTF_8)); ~ fallbackInstance = new StringTranslate(); ~ fallbackInstance.replaceWith(instance.languageList); @@ -69,13 +69,17 @@ > INSERT 2 : 3 @ 2 -+ instance.lastUpdateTimeInMilliseconds = System.currentTimeMillis(); ++ instance.lastUpdateTimeInMilliseconds = EagRuntime.steadyTimeMillis(); > CHANGE 6 : 7 @ 6 : 7 ~ public static void replaceWith(Map parMap) { -> CHANGE 5 : 6 @ 5 : 6 +> CHANGE 2 : 3 @ 2 : 3 + +~ instance.lastUpdateTimeInMilliseconds = EagRuntime.steadyTimeMillis(); + +> CHANGE 2 : 3 @ 2 : 3 ~ public String translateKey(String key) { diff --git a/patches/minecraft/net/minecraft/util/StringUtils.edit.java b/patches/minecraft/net/minecraft/util/StringUtils.edit.java index b0b57824..3f2fc2d0 100644 --- a/patches/minecraft/net/minecraft/util/StringUtils.edit.java +++ b/patches/minecraft/net/minecraft/util/StringUtils.edit.java @@ -11,7 +11,7 @@ + private static final Pattern patternControlCodeAlternate = Pattern.compile("(?i)&([0-9A-FK-OR])"); + + public static String translateControlCodesAlternate(String parString1) { -+ return patternControlCodeAlternate.matcher(parString1).replaceAll("\u00A7$1"); ++ return patternControlCodeAlternate.matcher(parString1).replaceAll(new String(new char[] { 0xA7, '$', '1' })); + } > EOF diff --git a/patches/minecraft/net/minecraft/util/Timer.edit.java b/patches/minecraft/net/minecraft/util/Timer.edit.java index f2d94130..e5c2d1bc 100644 --- a/patches/minecraft/net/minecraft/util/Timer.edit.java +++ b/patches/minecraft/net/minecraft/util/Timer.edit.java @@ -5,8 +5,20 @@ # Version: 1.0 # Author: lax1dude -> DELETE 3 @ 3 : 4 +> INSERT 2 : 3 @ 2 -> DELETE 52 @ 52 : 53 ++ import net.lax1dude.eaglercraft.v1_8.EagRuntime; + +> DELETE 1 @ 1 : 2 + +> CHANGE 16 : 17 @ 16 : 17 + +~ this.lastSyncHRClock = EagRuntime.nanoTime() / 1000000L; + +> CHANGE 5 : 6 @ 5 : 6 + +~ long k = EagRuntime.nanoTime() / 1000000L; + +> DELETE 29 @ 29 : 30 > EOF diff --git a/patches/minecraft/net/minecraft/world/GameRules.edit.java b/patches/minecraft/net/minecraft/world/GameRules.edit.java index a0dab682..138dc999 100644 --- a/patches/minecraft/net/minecraft/world/GameRules.edit.java +++ b/patches/minecraft/net/minecraft/world/GameRules.edit.java @@ -9,7 +9,7 @@ + -> INSERT 21 : 27 @ 21 +> INSERT 21 : 28 @ 21 + this.addGameRule("loadSpawnChunks", "false", GameRules.ValueType.BOOLEAN_VALUE); + this.addGameRule("bedSpawnPoint", "true", GameRules.ValueType.BOOLEAN_VALUE); @@ -17,5 +17,6 @@ + this.addGameRule("clickToSit", "true", GameRules.ValueType.BOOLEAN_VALUE); + this.addGameRule("colorCodes", "true", GameRules.ValueType.BOOLEAN_VALUE); + this.addGameRule("doSignEditing", "true", GameRules.ValueType.BOOLEAN_VALUE); ++ this.addGameRule("doWeatherCycle", "true", GameRules.ValueType.BOOLEAN_VALUE); > EOF diff --git a/patches/minecraft/net/minecraft/world/World.edit.java b/patches/minecraft/net/minecraft/world/World.edit.java index b59977a1..5e84e694 100644 --- a/patches/minecraft/net/minecraft/world/World.edit.java +++ b/patches/minecraft/net/minecraft/world/World.edit.java @@ -9,10 +9,9 @@ + -> CHANGE 5 : 8 @ 5 : 6 +> CHANGE 5 : 7 @ 5 : 6 ~ -~ import net.lax1dude.eaglercraft.v1_8.EagRuntime; ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; > CHANGE 1 : 4 @ 1 : 2 @@ -25,7 +24,9 @@ + -> DELETE 32 @ 32 : 46 +> DELETE 16 @ 16 : 17 + +> DELETE 15 @ 15 : 29 > CHANGE 24 : 25 @ 24 : 25 @@ -35,17 +36,21 @@ ~ public final EaglercraftRandom rand = new EaglercraftRandom(); -> CHANGE 9 : 10 @ 9 : 10 +> DELETE 8 @ 8 : 9 -~ private final Calendar theCalendar = EagRuntime.getLocaleCalendar(); - -> DELETE 1 @ 1 : 2 +> DELETE 2 @ 2 : 3 > INSERT 7 : 8 @ 7 + public final boolean isRemote; -> DELETE 11 @ 11 : 12 +> CHANGE 1 : 2 @ 1 : 3 + +~ protected World(ISaveHandler saveHandlerIn, WorldInfo info, WorldProvider providerIn, boolean client) { + +> DELETE 5 @ 5 : 6 + +> DELETE 2 @ 2 : 3 > INSERT 1 : 2 @ 1 @@ -56,7 +61,11 @@ ~ return CrashReportCategory ~ .getCoordinateInfo(new net.minecraft.util.BlockPos(pos.getX(), pos.getY(), pos.getZ())); -> CHANGE 257 : 258 @ 257 : 258 +> DELETE 123 @ 123 : 124 + +> DELETE 1 @ 1 : 2 + +> CHANGE 131 : 132 @ 131 : 132 ~ return HString.format("ID #%d (%s // %s)", @@ -73,7 +82,33 @@ ~ } ~ } else if (j < lightValue) { -> CHANGE 1224 : 1225 @ 1224 : 1225 +> DELETE 579 @ 579 : 582 + +> DELETE 23 @ 23 : 24 + +> DELETE 16 @ 16 : 17 + +> DELETE 12 @ 12 : 13 + +> DELETE 11 @ 11 : 13 + +> DELETE 11 @ 11 : 12 + +> DELETE 2 @ 2 : 3 + +> DELETE 36 @ 36 : 37 + +> DELETE 20 @ 20 : 22 + +> DELETE 48 @ 48 : 49 + +> DELETE 38 @ 38 : 39 + +> CHANGE 400 : 401 @ 400 : 401 + +~ if (!this.provider.getHasNoSky() && this.getGameRules().getBoolean("doWeatherCycle")) { + +> CHANGE 12 : 13 @ 12 : 13 ~ this.worldInfo.setThunderTime((this.rand.nextInt(12000) / 2) + 3600); @@ -89,7 +124,19 @@ ~ this.worldInfo.setRainTime((this.rand.nextInt(168000) + 12000) * 2); -> CHANGE 91 : 92 @ 91 : 92 +> DELETE 23 @ 23 : 24 + +> DELETE 14 @ 14 : 15 + +> DELETE 4 @ 4 : 5 + +> DELETE 9 @ 9 : 10 + +> DELETE 5 @ 5 : 6 + +> DELETE 23 @ 23 : 24 + +> CHANGE 7 : 8 @ 7 : 8 ~ public void forceBlockUpdateTick(Block blockType, BlockPos pos, EaglercraftRandom random) { @@ -99,13 +146,19 @@ ~ for (int m = 0; m < facings.length; ++m) { ~ EnumFacing enumfacing = facings[m]; -> CHANGE 50 : 53 @ 50 : 51 +> DELETE 22 @ 22 : 23 + +> CHANGE 27 : 30 @ 27 : 28 ~ EnumFacing[] facings = EnumFacing._VALUES; ~ for (int m = 0; m < facings.length; ++m) { ~ EnumFacing enumfacing = facings[m]; -> CHANGE 117 : 120 @ 117 : 119 +> DELETE 20 @ 20 : 23 + +> DELETE 50 @ 50 : 51 + +> CHANGE 43 : 46 @ 43 : 45 ~ for (int i = 0, l = this.loadedEntityList.size(); i < l; ++i) { ~ Entity entity = this.loadedEntityList.get(i); diff --git a/patches/minecraft/net/minecraft/world/WorldServer.edit.java b/patches/minecraft/net/minecraft/world/WorldServer.edit.java index 57e9e43b..2429e2b4 100644 --- a/patches/minecraft/net/minecraft/world/WorldServer.edit.java +++ b/patches/minecraft/net/minecraft/world/WorldServer.edit.java @@ -16,7 +16,9 @@ ~ import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; ~ import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerMinecraftServer; -> DELETE 42 @ 42 : 53 +> DELETE 25 @ 25 : 26 + +> DELETE 16 @ 16 : 27 > CHANGE 12 : 14 @ 12 : 14 @@ -27,7 +29,28 @@ ~ private final Map entitiesByUuid = Maps.newHashMap(); -> CHANGE 143 : 145 @ 143 : 144 +> CHANGE 23 : 25 @ 23 : 26 + +~ public WorldServer(MinecraftServer server, ISaveHandler saveHandlerIn, WorldInfo info, int dimensionId) { +~ super(saveHandlerIn, info, WorldProvider.getProviderForDimension(dimensionId), false); + +> DELETE 64 @ 64 : 65 + +> DELETE 6 @ 6 : 7 + +> DELETE 11 @ 11 : 12 + +> DELETE 1 @ 1 : 2 + +> DELETE 1 @ 1 : 2 + +> DELETE 1 @ 1 : 2 + +> DELETE 2 @ 2 : 3 + +> DELETE 1 @ 1 : 2 + +> CHANGE 22 : 24 @ 22 : 23 ~ for (int k = 0, l = this.playerEntities.size(); k < l; ++k) { ~ EntityPlayer entityplayer = this.playerEntities.get(k); @@ -43,7 +66,17 @@ ~ for (int k = 0, l = this.playerEntities.size(); k < l; ++k) { ~ EntityPlayer entityplayer = this.playerEntities.get(k); -> CHANGE 88 : 91 @ 88 : 89 +> DELETE 48 @ 48 : 49 + +> DELETE 2 @ 2 : 3 + +> DELETE 1 @ 1 : 2 + +> DELETE 11 @ 11 : 12 + +> DELETE 19 @ 19 : 20 + +> CHANGE 2 : 5 @ 2 : 3 ~ ExtendedBlockStorage[] vigg = chunk.getBlockStorageArray(); ~ for (int m = 0; m < vigg.length; ++m) { @@ -53,15 +86,25 @@ + ++EaglerMinecraftServer.counterTileUpdate; -> INSERT 47 : 48 @ 47 +> DELETE 5 @ 5 : 7 + +> DELETE 1 @ 1 : 2 + +> INSERT 38 : 39 @ 38 + ++EaglerMinecraftServer.counterTileUpdate; -> INSERT 95 : 96 @ 95 +> DELETE 65 @ 65 : 67 + +> DELETE 12 @ 12 : 14 + +> INSERT 14 : 15 @ 14 + ++EaglerMinecraftServer.counterTileUpdate; -> CHANGE 160 : 161 @ 160 : 161 +> DELETE 15 @ 15 : 16 + +> CHANGE 144 : 145 @ 144 : 145 ~ EaglercraftRandom random = new EaglercraftRandom(this.getSeed()); diff --git a/patches/minecraft/net/minecraft/world/WorldServerMulti.edit.java b/patches/minecraft/net/minecraft/world/WorldServerMulti.edit.java index 3e397c6f..f8851dec 100644 --- a/patches/minecraft/net/minecraft/world/WorldServerMulti.edit.java +++ b/patches/minecraft/net/minecraft/world/WorldServerMulti.edit.java @@ -5,6 +5,13 @@ # Version: 1.0 # Author: lax1dude -> DELETE 5 @ 5 : 8 +> DELETE 2 @ 2 : 3 + +> DELETE 2 @ 2 : 5 + +> CHANGE 8 : 10 @ 8 : 11 + +~ public WorldServerMulti(MinecraftServer server, ISaveHandler saveHandlerIn, int dimensionId, WorldServer delegate) { +~ super(server, saveHandlerIn, new DerivedWorldInfo(delegate.getWorldInfo()), dimensionId); > EOF diff --git a/patches/minecraft/net/minecraft/world/border/WorldBorder.edit.java b/patches/minecraft/net/minecraft/world/border/WorldBorder.edit.java index 2f9f3400..e1038cac 100644 --- a/patches/minecraft/net/minecraft/world/border/WorldBorder.edit.java +++ b/patches/minecraft/net/minecraft/world/border/WorldBorder.edit.java @@ -7,11 +7,12 @@ > DELETE 2 @ 2 : 3 -> INSERT 1 : 4 @ 1 +> INSERT 1 : 5 @ 1 + + import com.google.common.collect.Lists; + ++ import net.lax1dude.eaglercraft.v1_8.EagRuntime; > DELETE 4 @ 4 : 6 @@ -21,13 +22,29 @@ ~ for (int i = 0, l = lst.size(); i < l; ++i) { ~ lst.get(i).onCenterChanged(this, x, z); -> CHANGE 32 : 35 @ 32 : 34 +> CHANGE 6 : 7 @ 6 : 7 + +~ double d0 = (double) ((float) (EagRuntime.steadyTimeMillis() - this.startTime) + +> CHANGE 12 : 13 @ 12 : 13 + +~ return this.getStatus() != EnumBorderStatus.STATIONARY ? this.endTime - EagRuntime.steadyTimeMillis() : 0L; + +> CHANGE 9 : 10 @ 9 : 10 + +~ this.endTime = EagRuntime.steadyTimeMillis(); + +> CHANGE 2 : 5 @ 2 : 4 ~ List lst = this.getListeners(); ~ for (int i = 0, l = lst.size(); i < l; ++i) { ~ lst.get(i).onSizeChanged(this, newSize); -> CHANGE 10 : 13 @ 10 : 12 +> CHANGE 7 : 8 @ 7 : 8 + +~ this.startTime = EagRuntime.steadyTimeMillis(); + +> CHANGE 2 : 5 @ 2 : 4 ~ List lst = this.getListeners(); ~ for (int i = 0, l = lst.size(); i < l; ++i) { diff --git a/patches/minecraft/net/minecraft/world/chunk/Chunk.edit.java b/patches/minecraft/net/minecraft/world/chunk/Chunk.edit.java index 40157c10..ba65f3b3 100644 --- a/patches/minecraft/net/minecraft/world/chunk/Chunk.edit.java +++ b/patches/minecraft/net/minecraft/world/chunk/Chunk.edit.java @@ -45,7 +45,9 @@ + ++EaglerMinecraftServer.counterLightUpdate; + } -> CHANGE 20 : 23 @ 20 : 21 +> DELETE 9 @ 9 : 10 + +> CHANGE 10 : 13 @ 10 : 11 ~ EnumFacing[] facings = EnumFacing.Plane.HORIZONTAL.facingsArray; ~ for (int m = 0; m < facings.length; ++m) { @@ -56,7 +58,11 @@ ~ for (int m = 0; m < facings.length; ++m) { ~ EnumFacing enumfacing1 = facings[m]; -> CHANGE 110 : 113 @ 110 : 111 +> DELETE 5 @ 5 : 6 + +> DELETE 8 @ 8 : 10 + +> CHANGE 94 : 97 @ 94 : 95 ~ EnumFacing[] facings = EnumFacing.Plane.HORIZONTAL.facingsArray; ~ for (int m = 0; m < facings.length; ++m) { diff --git a/patches/minecraft/net/minecraft/world/demo/DemoWorldServer.edit.java b/patches/minecraft/net/minecraft/world/demo/DemoWorldServer.edit.java index cf86a389..407f74b1 100644 --- a/patches/minecraft/net/minecraft/world/demo/DemoWorldServer.edit.java +++ b/patches/minecraft/net/minecraft/world/demo/DemoWorldServer.edit.java @@ -5,7 +5,9 @@ # Version: 1.0 # Author: lax1dude -> INSERT 10 : 12 @ 10 +> DELETE 2 @ 2 : 3 + +> INSERT 7 : 9 @ 7 + import net.lax1dude.eaglercraft.v1_8.EaglercraftVersion; + @@ -14,4 +16,9 @@ ~ private static final long demoWorldSeed = EaglercraftVersion.demoWorldSeed; +> CHANGE 3 : 5 @ 3 : 6 + +~ public DemoWorldServer(MinecraftServer server, ISaveHandler saveHandlerIn, WorldInfo worldInfoIn, int dimensionId) { +~ super(server, saveHandlerIn, worldInfoIn, dimensionId); + > EOF diff --git a/patches/minecraft/net/minecraft/world/storage/MapStorage.edit.java b/patches/minecraft/net/minecraft/world/storage/MapStorage.edit.java index 94639279..cb10e24e 100644 --- a/patches/minecraft/net/minecraft/world/storage/MapStorage.edit.java +++ b/patches/minecraft/net/minecraft/world/storage/MapStorage.edit.java @@ -28,7 +28,7 @@ + WorldSavedData createInstance(String mapFileName); + } + -+ public static final Map, MapStorageProvider> storageProviders = new HashMap(); ++ public static final Map, MapStorageProvider> storageProviders = new HashMap<>(); + + static { + storageProviders.put(MapData.class, MapData::new); diff --git a/patches/minecraft/net/minecraft/world/storage/SaveFormatOld.edit.java b/patches/minecraft/net/minecraft/world/storage/SaveFormatOld.edit.java index 20217c76..72bfbca5 100644 --- a/patches/minecraft/net/minecraft/world/storage/SaveFormatOld.edit.java +++ b/patches/minecraft/net/minecraft/world/storage/SaveFormatOld.edit.java @@ -13,9 +13,10 @@ > DELETE 2 @ 2 : 3 -> INSERT 2 : 3 @ 2 +> INSERT 2 : 4 @ 2 + import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerIntegratedServerWorker; ++ import net.lax1dude.eaglercraft.v1_8.sp.server.WorldsDB; > CHANGE 1 : 4 @ 1 : 8 @@ -41,11 +42,11 @@ > CHANGE 10 : 11 @ 10 : 11 -~ VFile2 file1 = new VFile2(this.savesDirectory, saveName); +~ VFile2 file1 = WorldsDB.newVFile(this.savesDirectory, saveName); > CHANGE 3 : 4 @ 3 : 4 -~ VFile2 file2 = new VFile2(file1, "level.dat"); +~ VFile2 file2 = WorldsDB.newVFile(file1, "level.dat"); > CHANGE 2 : 6 @ 2 : 3 @@ -56,7 +57,7 @@ > CHANGE 7 : 8 @ 7 : 8 -~ file2 = new VFile2(file1, "level.dat_old"); +~ file2 = WorldsDB.newVFile(file1, "level.dat_old"); > CHANGE 2 : 6 @ 2 : 3 @@ -68,8 +69,8 @@ > CHANGE 11 : 15 @ 11 : 15 ~ public boolean renameWorld(String dirName, String newName) { -~ VFile2 file1 = new VFile2(this.savesDirectory, dirName); -~ VFile2 file2 = new VFile2(file1, "level.dat"); +~ VFile2 file1 = WorldsDB.newVFile(this.savesDirectory, dirName); +~ VFile2 file2 = WorldsDB.newVFile(file1, "level.dat"); ~ { > CHANGE 2 : 6 @ 2 : 3 @@ -99,7 +100,7 @@ > CHANGE 3 : 5 @ 3 : 8 -~ VFile2 file1 = new VFile2(this.savesDirectory, parString1); +~ VFile2 file1 = WorldsDB.newVFile(this.savesDirectory, parString1); ~ logger.info("Deleting level " + parString1); > CHANGE 1 : 6 @ 1 : 6 @@ -148,7 +149,7 @@ > CHANGE 15 : 17 @ 15 : 17 -~ return (new VFile2(this.savesDirectory, parString1, "level.dat")).exists() -~ || (new VFile2(this.savesDirectory, parString1, "level.dat_old")).exists(); +~ return (WorldsDB.newVFile(this.savesDirectory, parString1, "level.dat")).exists() +~ || (WorldsDB.newVFile(this.savesDirectory, parString1, "level.dat_old")).exists(); > EOF diff --git a/patches/minecraft/net/minecraft/world/storage/SaveHandler.edit.java b/patches/minecraft/net/minecraft/world/storage/SaveHandler.edit.java index 9f2e41fc..26191aff 100644 --- a/patches/minecraft/net/minecraft/world/storage/SaveHandler.edit.java +++ b/patches/minecraft/net/minecraft/world/storage/SaveHandler.edit.java @@ -12,11 +12,12 @@ ~ import java.util.List; ~ -> CHANGE 7 : 10 @ 7 : 12 +> CHANGE 7 : 11 @ 7 : 12 ~ import net.lax1dude.eaglercraft.v1_8.internal.vfs2.VFile2; ~ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; ~ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +~ import net.lax1dude.eaglercraft.v1_8.sp.server.WorldsDB; > CHANGE 3 : 6 @ 3 : 6 @@ -27,9 +28,9 @@ > CHANGE 3 : 7 @ 3 : 9 ~ public SaveHandler(VFile2 savesDirectory, String directoryName) { -~ this.worldDirectory = new VFile2(savesDirectory, directoryName); -~ this.playersDirectory = new VFile2(this.worldDirectory, "player"); -~ this.mapDataDir = new VFile2(this.worldDirectory, "data"); +~ this.worldDirectory = WorldsDB.newVFile(savesDirectory, directoryName); +~ this.playersDirectory = WorldsDB.newVFile(this.worldDirectory, "player"); +~ this.mapDataDir = WorldsDB.newVFile(this.worldDirectory, "data"); > DELETE 1 @ 1 : 4 @@ -47,7 +48,7 @@ > CHANGE 3 : 4 @ 3 : 4 -~ VFile2 file1 = new VFile2(this.worldDirectory, "level.dat"); +~ VFile2 file1 = WorldsDB.newVFile(this.worldDirectory, "level.dat"); > CHANGE 1 : 3 @ 1 : 3 @@ -61,7 +62,7 @@ > CHANGE 3 : 4 @ 3 : 4 -~ file1 = new VFile2(this.worldDirectory, "level.dat_old"); +~ file1 = WorldsDB.newVFile(this.worldDirectory, "level.dat_old"); > CHANGE 1 : 3 @ 1 : 3 @@ -75,9 +76,9 @@ > CHANGE 12 : 18 @ 12 : 16 -~ VFile2 file1 = new VFile2(this.worldDirectory, "level.dat_new"); -~ VFile2 file2 = new VFile2(this.worldDirectory, "level.dat_old"); -~ VFile2 file3 = new VFile2(this.worldDirectory, "level.dat"); +~ VFile2 file1 = WorldsDB.newVFile(this.worldDirectory, "level.dat_new"); +~ VFile2 file2 = WorldsDB.newVFile(this.worldDirectory, "level.dat_old"); +~ VFile2 file3 = WorldsDB.newVFile(this.worldDirectory, "level.dat"); ~ try (OutputStream os = file1.getOutputStream()) { ~ CompressedStreamTools.writeCompressed(nbttagcompound2, os); ~ } @@ -89,9 +90,9 @@ > CHANGE 10 : 16 @ 10 : 14 -~ VFile2 file1 = new VFile2(this.worldDirectory, "level.dat_new"); -~ VFile2 file2 = new VFile2(this.worldDirectory, "level.dat_old"); -~ VFile2 file3 = new VFile2(this.worldDirectory, "level.dat"); +~ VFile2 file1 = WorldsDB.newVFile(this.worldDirectory, "level.dat_new"); +~ VFile2 file2 = WorldsDB.newVFile(this.worldDirectory, "level.dat_old"); +~ VFile2 file3 = WorldsDB.newVFile(this.worldDirectory, "level.dat"); ~ try (OutputStream os = file1.getOutputStream()) { ~ CompressedStreamTools.writeCompressed(nbttagcompound1, os); ~ } @@ -104,8 +105,8 @@ > CHANGE 8 : 14 @ 8 : 11 ~ String s = player.getName().toLowerCase(); -~ VFile2 file1 = new VFile2(this.playersDirectory, s + ".dat.tmp"); -~ VFile2 file2 = new VFile2(this.playersDirectory, s + ".dat"); +~ VFile2 file1 = WorldsDB.newVFile(this.playersDirectory, s + ".dat.tmp"); +~ VFile2 file2 = WorldsDB.newVFile(this.playersDirectory, s + ".dat"); ~ try (OutputStream os = file1.getOutputStream()) { ~ CompressedStreamTools.writeCompressed(nbttagcompound, os); ~ } @@ -117,7 +118,7 @@ > CHANGE 8 : 13 @ 8 : 11 -~ VFile2 file1 = new VFile2(this.playersDirectory, player.getName().toLowerCase() + ".dat"); +~ VFile2 file1 = WorldsDB.newVFile(this.playersDirectory, player.getName().toLowerCase() + ".dat"); ~ if (file1.exists()) { ~ try (InputStream is = file1.getInputStream()) { ~ nbttagcompound = CompressedStreamTools.readCompressed(is); @@ -146,6 +147,6 @@ > CHANGE 5 : 7 @ 5 : 7 ~ public VFile2 getMapFileFromName(String mapName) { -~ return new VFile2(this.mapDataDir, mapName + ".dat"); +~ return WorldsDB.newVFile(this.mapDataDir, mapName + ".dat"); > EOF diff --git a/patches/minecraft/net/minecraft/world/storage/WorldInfo.edit.java b/patches/minecraft/net/minecraft/world/storage/WorldInfo.edit.java index 78992be2..c03cd0fb 100644 --- a/patches/minecraft/net/minecraft/world/storage/WorldInfo.edit.java +++ b/patches/minecraft/net/minecraft/world/storage/WorldInfo.edit.java @@ -5,7 +5,9 @@ # Version: 1.0 # Author: lax1dude -> INSERT 12 : 14 @ 12 +> DELETE 5 @ 5 : 6 + +> INSERT 6 : 8 @ 6 + import net.lax1dude.eaglercraft.v1_8.HString; + @@ -20,7 +22,11 @@ + this.eaglerVersion = nbt.getInteger("eaglerVersionSerial"); -> INSERT 102 : 103 @ 102 +> CHANGE 82 : 83 @ 82 : 83 + +~ nbt.setLong("LastPlayed", System.currentTimeMillis()); + +> INSERT 19 : 20 @ 19 + nbt.setInteger("eaglerVersionSerial", this.eaglerVersion); diff --git a/patches/resources/assets/minecraft/lang/en_US.edit.lang b/patches/resources/assets/minecraft/lang/en_US.edit.lang index 9b7099f0..26b05afe 100644 --- a/patches/resources/assets/minecraft/lang/en_US.edit.lang +++ b/patches/resources/assets/minecraft/lang/en_US.edit.lang @@ -5,24 +5,19 @@ # Version: 1.0 # Author: lax1dude -> CHANGE 17 : 21 @ 17 : 24 +> CHANGE 17 : 25 @ 17 : 24 -~ eaglercraft.recording.unsupported=Recording Unsupported! -~ eaglercraft.recording.stop=Stop Recording -~ eaglercraft.recording.start=Record Screen... -~ eaglercraft.soundCategory.voice=Recording Voice +~ eaglercraft.resourcePack.prompt.title=What do you want to do with '%s'? +~ eaglercraft.resourcePack.prompt.text=Tip: Hold Shift to skip this screen when selecting a resource pack! +~ eaglercraft.resourcePack.prompt.delete=Delete this resource pack +~ eaglercraft.resourcePack.prompt.add=Select this resource pack +~ eaglercraft.resourcePack.load.refreshing=Refreshing Resources... +~ eaglercraft.resourcePack.load.pleaseWait=Please Wait. +~ eaglercraft.resourcePack.load.loading=Loading resource pack... +~ eaglercraft.resourcePack.load.deleting=Deleting resource pack... -> INSERT 1 : 239 @ 1 +> INSERT 1 : 231 @ 1 -+ eaglercraft.resourcePack.prompt.title=What do you want to do with '%s'? -+ eaglercraft.resourcePack.prompt.text=Tip: Hold Shift to skip this screen when selecting a resource pack! -+ eaglercraft.resourcePack.prompt.delete=Delete this resource pack -+ eaglercraft.resourcePack.prompt.add=Select this resource pack -+ eaglercraft.resourcePack.load.refreshing=Refreshing Resources... -+ eaglercraft.resourcePack.load.pleaseWait=Please Wait. -+ eaglercraft.resourcePack.load.loading=Loading resource pack... -+ eaglercraft.resourcePack.load.deleting=Deleting resource pack... -+ + eaglercraft.gui.exitKey=Use '%s' to close this screen! + eaglercraft.gui.exitKeyRetarded=Use Backtick (`) to close this screen! + eaglercraft.gui.continue=Continue @@ -240,6 +235,7 @@ + + eaglercraft.shaders.gui.unsupported.title=Shaders are unavailable on this device! + eaglercraft.shaders.gui.unsupported.reason.hdrFramebuffer=Reason: No HDR render target support ++ eaglercraft.shaders.gui.unsupported.reason.oldOpenGLVersion=Reason: OpenGL ES 3.0 (WebGL 2.0) is not supported! + + eaglercraft.shaders.debugMenuTip=Press %s+4 to access the shader debug menu + @@ -247,13 +243,13 @@ + eaglercraft.command.skull.usage=/eagskull + eaglercraft.command.skull.nopermission=Cheats are not enabled! + eaglercraft.command.skull.feedback=Created new skull: "%s" -+ eaglercraft.command.skull.error.invalid.png=Invalid PNG file! ++ eaglercraft.command.skull.error.invalid.format=Invalid or unsupported image file! + eaglercraft.command.skull.error.invalid.skin=Image with size %dx%d is not a valid minecraft skin! + + eaglercraft.command.clientStub=This command is client side! + -> INSERT 163 : 409 @ 163 +> INSERT 163 : 545 @ 163 + eaglercraft.singleplayer.busy.killTask=Cancel Task + eaglercraft.singleplayer.busy.cancelWarning=Are you sure? @@ -262,13 +258,14 @@ + eaglercraft.singleplayer.crashed.title=Recieved a crash report from integrated server! + eaglercraft.singleplayer.crashed.checkConsole=Check the console for more details + eaglercraft.singleplayer.crashed.continue=Continue ++ eaglercraft.singleplayer.crashed.singleThreadCont=Single Thread Mode + + eaglercraft.singleplayer.failed.title=Singleplayer Task Failed! + eaglercraft.singleplayer.failed.killed=The worker process was killed + eaglercraft.singleplayer.failed.notStarted=The worker process could not start + -+ eaglercraft.singleplayer.failed.singleThreadWarning.1=Worker Thread Startup Failure! -+ eaglercraft.singleplayer.failed.singleThreadWarning.2=The integrated server will run in single-threaded mode ++ eaglercraft.singleplayer.failed.singleThreadWarning.1=Worker Thread Issue Detected ++ eaglercraft.singleplayer.failed.singleThreadWarning.2=The integrated server is running in single-threaded mode + + eaglercraft.singleplayer.busy.listingworlds=Loading worlds + eaglercraft.singleplayer.failed.listingworlds=Could not fetch worlds list! @@ -342,11 +339,21 @@ + eaglercraft.singleplayer.backup.clearPlayerData.warning1=Are you sure you want to delete all player data? + eaglercraft.singleplayer.backup.clearPlayerData.warning2=All players in '%s' will lose their inventories (besides %s) + ++ eaglercraft.singleplayer.ramdiskdetected.title=Worker Issues Detected ++ eaglercraft.singleplayer.ramdiskdetected.text0=Running in "RAMDisk mode", worlds cannot be saved! ++ eaglercraft.singleplayer.ramdiskdetected.text1=Single thread mode may solve this issue ++ eaglercraft.singleplayer.ramdiskdetected.continue=Continue ++ eaglercraft.singleplayer.ramdiskdetected.singleThreadCont=Single Thread Mode ++ + eaglercraft.selectWorld.backup=Backup + + eaglercraft.selectWorld.duplicate=Duplicate World: + eaglercraft.selectWorld.duplicateButton=Duplicate + ++ eaglercraft.selectWorld.ramdiskWarning=WARNING: Running in "RAMDisk mode", worlds will NOT be saved to local storage! ++ ++ eaglercraft.singleplayer.tpscounter.singleThreadMode=Single Thread Mode ++ + eaglercraft.networkSettings.title=Shared World Relay Servers + eaglercraft.networkSettings.add=Add Relay + eaglercraft.networkSettings.delete=Delete Relay @@ -447,6 +454,27 @@ + eaglercraft.updateList.note.0=Note: Updates are digitally signed, EaglercraftX will block any + eaglercraft.updateList.note.1=updates that were not created by lax1dude or ayunami2000 + ++ eaglercraft.updateSuccess.title=Update Successful ++ eaglercraft.updateSuccess.installToBootMenu=Install to Boot Menu ++ eaglercraft.updateSuccess.downloadOffline=Download Offline ++ ++ eaglercraft.updateSuccess.downloading=Downloading Offline... ++ eaglercraft.updateSuccess.installing=Installing Client... ++ ++ eaglercraft.updateFailed.title=Update Failed ++ ++ eaglercraft.installFailed.title=Installation Failed ++ ++ eaglercraft.updateInstall.title=Install to Boot Menu ++ eaglercraft.updateInstall.setDefault=Make Default ++ eaglercraft.updateInstall.setCountdown=Enable Countdown ++ eaglercraft.updateInstall.install=Install ++ ++ eaglercraft.options.pressDeleteText=Press DEL to enter boot menu ++ ++ eaglercraft.enterBootMenu.title=Enter Boot Menu ++ eaglercraft.enterBootMenu.text0=Refresh the page to enter the boot menu ++ + eaglercraft.voice.title=Voice Channel + eaglercraft.voice.titleNoVoice=Voice is disabled on this server + eaglercraft.voice.titleVoiceUnavailable=Voice is unavailable @@ -477,14 +505,15 @@ + eaglercraft.voice.volumeMicrophoneLabel=Microphone: + + eaglercraft.voice.unsupportedWarning1=Voice Warning -+ eaglercraft.voice.unsupportedWarning2=Your network's firewall may not support ++ eaglercraft.voice.unsupportedWarning2=Your network configuration may not support + eaglercraft.voice.unsupportedWarning3=eaglercraft's voice chat. -+ eaglercraft.voice.unsupportedWarning4=If your game doesn't work it's your issue -+ eaglercraft.voice.unsupportedWarning5=to solve, not ayunami2000's or lax1dude's. -+ eaglercraft.voice.unsupportedWarning6=Don't ask them to 'fix' it for you because -+ eaglercraft.voice.unsupportedWarning7=they won't help you fix a problem that only -+ eaglercraft.voice.unsupportedWarning8=you or your network's administrator has the -+ eaglercraft.voice.unsupportedWarning9=ability to correctly resolve. ++ eaglercraft.voice.unsupportedWarning4=Voice chat is based on WebRTC and is ++ eaglercraft.voice.unsupportedWarning5=normally meant to be peer-to-peer, if your ++ eaglercraft.voice.unsupportedWarning6=firewall blocks peer-to-peer connections ++ eaglercraft.voice.unsupportedWarning7=a TURN server will be required. ++ eaglercraft.voice.unsupportedWarning8=The default OpenRelayProject TURN servers ++ eaglercraft.voice.unsupportedWarning9=are no longer working in 2024! ++ + eaglercraft.voice.unsupportedWarning10=Continue + eaglercraft.voice.unsupportedWarning11=Cancel + @@ -494,16 +523,126 @@ + eaglercraft.voice.ipGrabWarning4=also using voice on the server. + eaglercraft.voice.ipGrabWarning5=This issue will not be fixed, it is an + eaglercraft.voice.ipGrabWarning6=internal browser issue, not a mistake in the -+ eaglercraft.voice.ipGrabWarning7=game. Fortunately, this can only be done if -+ eaglercraft.voice.ipGrabWarning8=the other player uses a hacked web browser -+ eaglercraft.voice.ipGrabWarning9=or has Wireshark to capture the voice -+ eaglercraft.voice.ipGrabWarning10=packets, as there exists no real javascript -+ eaglercraft.voice.ipGrabWarning11=method to log IPs using a normal skidded -+ eaglercraft.voice.ipGrabWarning12=eaglercraft hacked client. ++ eaglercraft.voice.ipGrabWarning7=game. Sorry about that. ++ ++ eaglercraft.revokeSessionToken.button=Revoke Session Token ++ eaglercraft.revokeSessionToken.title=Select session to revoke: ++ eaglercraft.revokeSessionToken.inspect=Inspect ++ eaglercraft.revokeSessionToken.revoke=Revoke ++ eaglercraft.revokeSessionToken.note.0=Use this tool when you believe someone has stolen your cookies ++ eaglercraft.revokeSessionToken.note.1=Eagler will request the server invalidate all session tokens ++ ++ eaglercraft.inspectSessionToken.title=Cookie Details ++ eaglercraft.inspectSessionToken.details.server=Server Address: ++ eaglercraft.inspectSessionToken.details.expires=Expires At: ++ eaglercraft.inspectSessionToken.details.length=Byte Length: ++ ++ eaglercraft.errorNoSessions.title=No Sessions Active! ++ eaglercraft.errorNoSessions.desc=There are no revokable sessions ++ ++ eaglercraft.revokeSendingScreen.title=Revoking Session ++ eaglercraft.revokeSendingScreen.message.opening=Connecting to %s... ++ eaglercraft.revokeSendingScreen.message.sending=Sending Request... ++ ++ eaglercraft.revokeSuccess.title=Revoke Success ++ eaglercraft.revokeSuccess.desc=The session was revoked sucessfully! ++ ++ eaglercraft.revokeFailure.title=Revoke Failed ++ eaglercraft.revokeFailure.desc.notSupported=The server does not support this feature! ++ eaglercraft.revokeFailure.desc.notAllowed=The server does not allow revoking this token! ++ eaglercraft.revokeFailure.desc.notFound=The session was not found on the server! ++ eaglercraft.revokeFailure.desc.serverError=Internal server error! ++ eaglercraft.revokeFailure.desc.clientError=Error handling server's response! ++ eaglercraft.revokeFailure.desc.genericCode=Error code received! (Check Console) ++ eaglercraft.revokeFailure.desc.connectionError=Failed to connect to the server! ++ eaglercraft.revokeFailure.desc.cancelled=Connection closed ++ ++ eaglercraft.recieveServerInfo.title=Retrieving Server Info ++ eaglercraft.recieveServerInfo.checkingCache=Checking Cache ++ eaglercraft.recieveServerInfo.contactingServer=Contacting Server ++ eaglercraft.recieveServerInfo.recievingData=Recieving Data ++ eaglercraft.recieveServerInfo.decompressing=Decompressing + Verifying ++ ++ eaglercraft.serverInfoFailure.title=Retrieval Failed ++ eaglercraft.serverInfoFailure.desc=Failed to retrieve server info! ++ ++ eaglercraft.webviewNotSupported.title=WebView Error ++ eaglercraft.webviewNotSupported.desc=WebView is not supported on this platform! ++ ++ eaglercraft.webviewInvalidURL.title=WebView Error ++ eaglercraft.webviewInvalidURL.desc=Server provided an invalid URL! ++ ++ eaglercraft.fallbackWebViewScreen.text0=View in your browser at: ++ eaglercraft.fallbackWebViewScreen.startingUp=Starting Up... ++ eaglercraft.fallbackWebViewScreen.pleaseWait=Please Wait... ++ eaglercraft.fallbackWebViewScreen.exited=(exited) ++ eaglercraft.fallbackWebViewScreen.openButton=Open ++ eaglercraft.fallbackWebViewScreen.exitButton=Exit ++ ++ eaglercraft.webviewPhishingWaring.title=WARNING!!! ++ eaglercraft.webviewPhishingWaring.text0=If you see a login page, think before you enter a password ++ eaglercraft.webviewPhishingWaring.text1=Passwords can be stolen by the owner of this server or website ++ eaglercraft.webviewPhishingWaring.text2=Do not log in to accounts you don't want hackers to steal ++ eaglercraft.webviewPhishingWaring.dontShowAgain=Do not show this message again ++ eaglercraft.webviewPhishingWaring.continue=Continue ++ ++ eaglercraft.notifications.title=Notifications ++ eaglercraft.notifications.priority=Priority: %s ++ eaglercraft.notifications.priority.low=All ++ eaglercraft.notifications.priority.normal=Info ++ eaglercraft.notifications.priority.higher=Warning ++ eaglercraft.notifications.priority.highest=Severe ++ eaglercraft.notifications.clearAll=Clear All ++ ++ eaglercraft.options.touchControlOpacity=Touch Ctrls Opacity ++ ++ eaglercraft.options.profanityFilterButton=Profanity Filter ++ ++ eaglercraft.profanityFilterWarning.title=Content Warning ++ eaglercraft.profanityFilterWarning.text0=If you are streaming this game on Twitch, or ++ eaglercraft.profanityFilterWarning.text1=are under the wise old age of 14, please enable ++ eaglercraft.profanityFilterWarning.text2=the profanity filter before playing Multiplayer ++ eaglercraft.profanityFilterWarning.text4=(Disable in the 'Options' -> 'Chat Settings' menu) ++ ++ eaglercraft.options.screenRecording.unsupported=Recording Unsupported! ++ eaglercraft.options.screenRecording.button=Record Screen... ++ ++ eaglercraft.options.screenRecording.title=Screen Recording ++ eaglercraft.options.screenRecording.codec=Output Format: %s ++ eaglercraft.options.screenRecording.codecButton=Change... ++ eaglercraft.options.screenRecording.start=Start Recording ++ eaglercraft.options.screenRecording.stop=Stop Recording ++ eaglercraft.options.screenRecording.status=Status: %s ++ eaglercraft.options.screenRecording.status.0=Not Recording ++ eaglercraft.options.screenRecording.status.1=Recording! ++ eaglercraft.options.screenRecording.audioBitrate=Audio Bitrate ++ eaglercraft.options.screenRecording.videoBitrate=Video Bitrate ++ eaglercraft.options.screenRecording.videoResolution=Video Resolution ++ eaglercraft.options.screenRecording.microphoneVolume=Microphone Volume ++ eaglercraft.options.screenRecording.gameVolume=Game Volume ++ eaglercraft.options.screenRecording.videoFPS=Video Frame Rate ++ eaglercraft.options.screenRecording.onVSync=VSync ++ eaglercraft.options.screenRecording.failed=Failed to begin recording! ++ ++ eaglercraft.options.recordingCodec.title=Select Codec ++ eaglercraft.options.recordingCodec.showAdvancedCodecs=Show Advanced: %s ++ ++ eaglercraft.options.recordingNote.title=Recording Note ++ eaglercraft.options.recordingNote.text0=If the recorded video does not play, ++ eaglercraft.options.recordingNote.text1=try opening the file in your browser ++ ++ eaglercraft.touch.interact.entity=Interact + -> CHANGE 22 : 23 @ 22 : 23 +> INSERT 18 : 19 @ 18 ++ eaglercraft.addServer.hideAddr=Hide Addr + +> CHANGE 4 : 8 @ 4 : 5 + +~ eaglercraft.addServer.enableCookies=Cookies +~ eaglercraft.addServer.enableCookies.enabled=Enabled +~ eaglercraft.addServer.enableCookies.disabled=Disabled ~ lanServer.title=Shared World > CHANGE 1 : 2 @ 1 : 2 diff --git a/sources/lwjgl/java/fi/iki/elonen/NanoHTTPD.java b/sources/lwjgl/java/fi/iki/elonen/NanoHTTPD.java new file mode 100644 index 00000000..3cc7199a --- /dev/null +++ b/sources/lwjgl/java/fi/iki/elonen/NanoHTTPD.java @@ -0,0 +1,2333 @@ +package fi.iki.elonen; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.security.KeyStore; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.GZIPOutputStream; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import fi.iki.elonen.NanoHTTPD.Response.IStatus; +import fi.iki.elonen.NanoHTTPD.Response.Status; + +/** + * A simple, tiny, nicely embeddable HTTP server in Java + *

+ *

+ * NanoHTTPD + *

+ * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, + * 2010 by Konstantinos Togias + *

+ *

+ *

+ * Features + limitations: + *

    + *

    + *

  • Only one Java file
  • + *
  • Java 5 compatible
  • + *
  • Released as open source, Modified BSD licence
  • + *
  • No fixed config files, logging, authorization etc. (Implement yourself if + * you need them.)
  • + *
  • Supports parameter parsing of GET and POST methods (+ rudimentary PUT + * support in 1.25)
  • + *
  • Supports both dynamic content and file serving
  • + *
  • Supports file upload (since version 1.2, 2010)
  • + *
  • Supports partial content (streaming)
  • + *
  • Supports ETags
  • + *
  • Never caches anything
  • + *
  • Doesn't limit bandwidth, request time or simultaneous connections
  • + *
  • Default code serves files and shows all HTTP parameters and headers
  • + *
  • File server supports directory listing, index.html and index.htm
  • + *
  • File server supports partial content (streaming)
  • + *
  • File server supports ETags
  • + *
  • File server does the 301 redirection trick for directories without + * '/'
  • + *
  • File server supports simple skipping for files (continue download)
  • + *
  • File server serves also very long files without memory overhead
  • + *
  • Contains a built-in list of most common MIME types
  • + *
  • All header names are converted to lower case so they don't vary between + * browsers/clients
  • + *

    + *

+ *

+ *

+ * How to use: + *

    + *

    + *

  • Subclass and implement serve() and embed to your own program
  • + *

    + *

+ *

+ * See the separate "LICENSE.md" file for the distribution license (Modified BSD + * licence) + */ +public abstract class NanoHTTPD { + + /** + * Pluggable strategy for asynchronously executing requests. + */ + public interface AsyncRunner { + + void closeAll(); + + void closed(ClientHandler clientHandler); + + void exec(ClientHandler code); + } + + /** + * The runnable that will be used for every new client connection. + */ + public class ClientHandler implements Runnable { + + private final InputStream inputStream; + + private final Socket acceptSocket; + + public ClientHandler(InputStream inputStream, Socket acceptSocket) { + this.inputStream = inputStream; + this.acceptSocket = acceptSocket; + } + + public void close() { + safeClose(this.inputStream); + safeClose(this.acceptSocket); + } + + @Override + public void run() { + OutputStream outputStream = null; + try { + outputStream = this.acceptSocket.getOutputStream(); + TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create(); + HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, + this.acceptSocket.getInetAddress()); + while (!this.acceptSocket.isClosed()) { + session.execute(); + } + } catch (Exception e) { + // When the socket is closed by the client, + // we throw our own SocketException + // to break the "keep alive" loop above. If + // the exception was anything other + // than the expected SocketException OR a + // SocketTimeoutException, print the + // stacktrace + if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) + && !(e instanceof SocketTimeoutException)) { + NanoHTTPD.LOG.log(Level.SEVERE, + "Communication with the client broken, or an bug in the handler code", e); + } + } finally { + safeClose(outputStream); + safeClose(this.inputStream); + safeClose(this.acceptSocket); + NanoHTTPD.this.asyncRunner.closed(this); + } + } + } + + public static class Cookie { + + public static String getHTTPTime(int days) { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + calendar.add(Calendar.DAY_OF_MONTH, days); + return dateFormat.format(calendar.getTime()); + } + + private final String n, v, e; + + public Cookie(String name, String value) { + this(name, value, 30); + } + + public Cookie(String name, String value, int numDays) { + this.n = name; + this.v = value; + this.e = getHTTPTime(numDays); + } + + public Cookie(String name, String value, String expires) { + this.n = name; + this.v = value; + this.e = expires; + } + + public String getHTTPHeader() { + String fmt = "%s=%s; expires=%s"; + return String.format(fmt, this.n, this.v, this.e); + } + } + + /** + * Provides rudimentary support for cookies. Doesn't support 'path', 'secure' + * nor 'httpOnly'. Feel free to improve it and/or add unsupported features. + * + * @author LordFokas + */ + public class CookieHandler implements Iterable { + + private final HashMap cookies = new HashMap(); + + private final ArrayList queue = new ArrayList(); + + public CookieHandler(Map httpHeaders) { + String raw = httpHeaders.get("cookie"); + if (raw != null) { + String[] tokens = raw.split(";"); + for (String token : tokens) { + String[] data = token.trim().split("="); + if (data.length == 2) { + this.cookies.put(data[0], data[1]); + } + } + } + } + + /** + * Set a cookie with an expiration date from a month ago, effectively deleting + * it on the client side. + * + * @param name The cookie name. + */ + public void delete(String name) { + set(name, "-delete-", -30); + } + + @Override + public Iterator iterator() { + return this.cookies.keySet().iterator(); + } + + /** + * Read a cookie from the HTTP Headers. + * + * @param name The cookie's name. + * @return The cookie's value if it exists, null otherwise. + */ + public String read(String name) { + return this.cookies.get(name); + } + + public void set(Cookie cookie) { + this.queue.add(cookie); + } + + /** + * Sets a cookie. + * + * @param name The cookie's name. + * @param value The cookie's value. + * @param expires How many days until the cookie expires. + */ + public void set(String name, String value, int expires) { + this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); + } + + /** + * Internally used by the webserver to add all queued cookies into the + * Response's HTTP Headers. + * + * @param response The Response object to which headers the queued cookies will + * be added. + */ + public void unloadQueue(Response response) { + for (Cookie cookie : this.queue) { + response.addHeader("Set-Cookie", cookie.getHTTPHeader()); + } + } + } + + /** + * Default threading strategy for NanoHTTPD. + *

+ *

+ * By default, the server spawns a new Thread for every incoming request. These + * are set to daemon status, and named according to the request number. + * The name is useful when profiling the application. + *

+ */ + public static class DefaultAsyncRunner implements AsyncRunner { + + private long requestCount; + + private final List running = Collections + .synchronizedList(new ArrayList()); + + /** + * @return a list with currently running clients. + */ + public List getRunning() { + return running; + } + + @Override + public void closeAll() { + // copy of the list for concurrency + for (ClientHandler clientHandler : new ArrayList(this.running)) { + clientHandler.close(); + } + } + + @Override + public void closed(ClientHandler clientHandler) { + this.running.remove(clientHandler); + } + + @Override + public void exec(ClientHandler clientHandler) { + ++this.requestCount; + Thread t = new Thread(clientHandler); + t.setDaemon(true); + t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")"); + this.running.add(clientHandler); + t.start(); + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + *

+ *

+ * By default, files are created by File.createTempFile() in the + * directory specified. + *

+ */ + public static class DefaultTempFile implements TempFile { + + private final File file; + + private final OutputStream fstream; + + public DefaultTempFile(File tempdir) throws IOException { + this.file = File.createTempFile("NanoHTTPD-", "", tempdir); + this.fstream = new FileOutputStream(this.file); + } + + @Override + public void delete() throws Exception { + safeClose(this.fstream); + if (!this.file.delete()) { + throw new Exception("could not delete temporary file: " + this.file.getAbsolutePath()); + } + } + + @Override + public String getName() { + return this.file.getAbsolutePath(); + } + + @Override + public OutputStream open() throws Exception { + return this.fstream; + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + *

+ *

+ * This class stores its files in the standard location (that is, wherever + * java.io.tmpdir points to). Files are added to an internal list, + * and deleted when no longer needed (that is, when clear() is + * invoked at the end of processing a request). + *

+ */ + public static class DefaultTempFileManager implements TempFileManager { + + private final File tmpdir; + + private final List tempFiles; + + public DefaultTempFileManager() { + this.tmpdir = new File(System.getProperty("java.io.tmpdir")); + if (!tmpdir.exists()) { + tmpdir.mkdirs(); + } + this.tempFiles = new ArrayList(); + } + + @Override + public void clear() { + for (TempFile file : this.tempFiles) { + try { + file.delete(); + } catch (Exception ignored) { + NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored); + } + } + this.tempFiles.clear(); + } + + @Override + public TempFile createTempFile(String filename_hint) throws Exception { + DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir); + this.tempFiles.add(tempFile); + return tempFile; + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + */ + private class DefaultTempFileManagerFactory implements TempFileManagerFactory { + + @Override + public TempFileManager create() { + return new DefaultTempFileManager(); + } + } + + /** + * Creates a normal ServerSocket for TCP connections + */ + public static class DefaultServerSocketFactory implements ServerSocketFactory { + + @Override + public ServerSocket create() throws IOException { + return new ServerSocket(); + } + + } + + /** + * Creates a new SSLServerSocket + */ + public static class SecureServerSocketFactory implements ServerSocketFactory { + + private SSLServerSocketFactory sslServerSocketFactory; + + private String[] sslProtocols; + + public SecureServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { + this.sslServerSocketFactory = sslServerSocketFactory; + this.sslProtocols = sslProtocols; + } + + @Override + public ServerSocket create() throws IOException { + SSLServerSocket ss = null; + ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); + if (this.sslProtocols != null) { + ss.setEnabledProtocols(this.sslProtocols); + } else { + ss.setEnabledProtocols(ss.getSupportedProtocols()); + } + ss.setUseClientMode(false); + ss.setWantClientAuth(false); + ss.setNeedClientAuth(false); + return ss; + } + + } + + private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)"; + + private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, + Pattern.CASE_INSENSITIVE); + + private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)"; + + private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE); + + private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]"; + + private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern + .compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX); + + protected static class ContentType { + + private static final String ASCII_ENCODING = "US-ASCII"; + + private static final String MULTIPART_FORM_DATA_HEADER = "multipart/form-data"; + + private static final String CONTENT_REGEX = "[ |\t]*([^/^ ^;^,]+/[^ ^;^,]+)"; + + private static final Pattern MIME_PATTERN = Pattern.compile(CONTENT_REGEX, Pattern.CASE_INSENSITIVE); + + private static final String CHARSET_REGEX = "[ |\t]*(charset)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;^,]*)['|\"]?"; + + private static final Pattern CHARSET_PATTERN = Pattern.compile(CHARSET_REGEX, Pattern.CASE_INSENSITIVE); + + private static final String BOUNDARY_REGEX = "[ |\t]*(boundary)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;^,]*)['|\"]?"; + + private static final Pattern BOUNDARY_PATTERN = Pattern.compile(BOUNDARY_REGEX, Pattern.CASE_INSENSITIVE); + + private final String contentTypeHeader; + + private final String contentType; + + private final String encoding; + + private final String boundary; + + public ContentType(String contentTypeHeader) { + this.contentTypeHeader = contentTypeHeader; + if (contentTypeHeader != null) { + contentType = getDetailFromContentHeader(contentTypeHeader, MIME_PATTERN, "", 1); + encoding = getDetailFromContentHeader(contentTypeHeader, CHARSET_PATTERN, null, 2); + } else { + contentType = ""; + encoding = "UTF-8"; + } + if (MULTIPART_FORM_DATA_HEADER.equalsIgnoreCase(contentType)) { + boundary = getDetailFromContentHeader(contentTypeHeader, BOUNDARY_PATTERN, null, 2); + } else { + boundary = null; + } + } + + private String getDetailFromContentHeader(String contentTypeHeader, Pattern pattern, String defaultValue, + int group) { + Matcher matcher = pattern.matcher(contentTypeHeader); + return matcher.find() ? matcher.group(group) : defaultValue; + } + + public String getContentTypeHeader() { + return contentTypeHeader; + } + + public String getContentType() { + return contentType; + } + + public String getEncoding() { + return encoding == null ? ASCII_ENCODING : encoding; + } + + public String getBoundary() { + return boundary; + } + + public boolean isMultipart() { + return MULTIPART_FORM_DATA_HEADER.equalsIgnoreCase(contentType); + } + + public ContentType tryUTF8() { + if (encoding == null) { + return new ContentType(this.contentTypeHeader + "; charset=UTF-8"); + } + return this; + } + } + + protected class HTTPSession implements IHTTPSession { + + private static final int REQUEST_BUFFER_LEN = 512; + + private static final int MEMORY_STORE_LIMIT = 1024; + + public static final int BUFSIZE = 8192; + + public static final int MAX_HEADER_SIZE = 1024; + + private final TempFileManager tempFileManager; + + private final OutputStream outputStream; + + private final BufferedInputStream inputStream; + + private int splitbyte; + + private int rlen; + + private String uri; + + private Method method; + + private Map> parms; + + private Map headers; + + private CookieHandler cookies; + + private String queryParameterString; + + private String remoteIp; + + private String remoteHostname; + + private String protocolVersion; + + public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { + this.tempFileManager = tempFileManager; + this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); + this.outputStream = outputStream; + } + + public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, + InetAddress inetAddress) { + this.tempFileManager = tempFileManager; + this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); + this.outputStream = outputStream; + this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" + : inetAddress.getHostAddress().toString(); + this.remoteHostname = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "localhost" + : inetAddress.getHostName().toString(); + this.headers = new HashMap(); + } + + /** + * Decodes the sent headers and loads the data into Key/value pairs + */ + private void decodeHeader(BufferedReader in, Map pre, Map> parms, + Map headers) throws ResponseException { + try { + // Read the request line + String inLine = in.readLine(); + if (inLine == null) { + return; + } + + StringTokenizer st = new StringTokenizer(inLine); + if (!st.hasMoreTokens()) { + throw new ResponseException(Response.Status.BAD_REQUEST, + "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); + } + + pre.put("method", st.nextToken()); + + if (!st.hasMoreTokens()) { + throw new ResponseException(Response.Status.BAD_REQUEST, + "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); + } + + String uri = st.nextToken(); + + // Decode parameters from the URI + int qmi = uri.indexOf('?'); + if (qmi >= 0) { + decodeParms(uri.substring(qmi + 1), parms); + uri = decodePercent(uri.substring(0, qmi)); + } else { + uri = decodePercent(uri); + } + + // If there's another token, its protocol version, + // followed by HTTP headers. + // NOTE: this now forces header names lower case since they are + // case insensitive and vary by client. + if (st.hasMoreTokens()) { + protocolVersion = st.nextToken(); + } else { + protocolVersion = "HTTP/1.1"; + NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1."); + } + String line = in.readLine(); + while (line != null && !line.trim().isEmpty()) { + int p = line.indexOf(':'); + if (p >= 0) { + headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim()); + } + line = in.readLine(); + } + + pre.put("uri", uri); + } catch (IOException ioe) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, + "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); + } + } + + /** + * Decodes the Multipart Body data and put it into Key/Value pairs. + */ + private void decodeMultipartFormData(ContentType contentType, ByteBuffer fbuf, Map> parms, + Map files) throws ResponseException { + int pcount = 0; + try { + int[] boundaryIdxs = getBoundaryPositions(fbuf, contentType.getBoundary().getBytes()); + if (boundaryIdxs.length < 2) { + throw new ResponseException(Response.Status.BAD_REQUEST, + "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings."); + } + + byte[] partHeaderBuff = new byte[MAX_HEADER_SIZE]; + for (int boundaryIdx = 0; boundaryIdx < boundaryIdxs.length - 1; boundaryIdx++) { + fbuf.position(boundaryIdxs[boundaryIdx]); + int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE; + fbuf.get(partHeaderBuff, 0, len); + BufferedReader in = new BufferedReader( + new InputStreamReader(new ByteArrayInputStream(partHeaderBuff, 0, len), + Charset.forName(contentType.getEncoding())), + len); + + int headerLines = 0; + // First line is boundary string + String mpline = in.readLine(); + headerLines++; + if (mpline == null || !mpline.contains(contentType.getBoundary())) { + throw new ResponseException(Response.Status.BAD_REQUEST, + "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary."); + } + + String partName = null, fileName = null, partContentType = null; + // Parse the reset of the header lines + mpline = in.readLine(); + headerLines++; + while (mpline != null && mpline.trim().length() > 0) { + Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline); + if (matcher.matches()) { + String attributeString = matcher.group(2); + matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString); + while (matcher.find()) { + String key = matcher.group(1); + if ("name".equalsIgnoreCase(key)) { + partName = matcher.group(2); + } else if ("filename".equalsIgnoreCase(key)) { + fileName = matcher.group(2); + // add these two line to support multiple + // files uploaded using the same field Id + if (!fileName.isEmpty()) { + if (pcount > 0) + partName = partName + String.valueOf(pcount++); + else + pcount++; + } + } + } + } + matcher = CONTENT_TYPE_PATTERN.matcher(mpline); + if (matcher.matches()) { + partContentType = matcher.group(2).trim(); + } + mpline = in.readLine(); + headerLines++; + } + int partHeaderLength = 0; + while (headerLines-- > 0) { + partHeaderLength = scipOverNewLine(partHeaderBuff, partHeaderLength); + } + // Read the part data + if (partHeaderLength >= len - 4) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, + "Multipart header size exceeds MAX_HEADER_SIZE."); + } + int partDataStart = boundaryIdxs[boundaryIdx] + partHeaderLength; + int partDataEnd = boundaryIdxs[boundaryIdx + 1] - 4; + + fbuf.position(partDataStart); + + List values = parms.get(partName); + if (values == null) { + values = new ArrayList(); + parms.put(partName, values); + } + + if (partContentType == null) { + // Read the part into a string + byte[] data_bytes = new byte[partDataEnd - partDataStart]; + fbuf.get(data_bytes); + + values.add(new String(data_bytes, contentType.getEncoding())); + } else { + // Read it into a file + String path = saveTmpFile(fbuf, partDataStart, partDataEnd - partDataStart, fileName); + if (!files.containsKey(partName)) { + files.put(partName, path); + } else { + int count = 2; + while (files.containsKey(partName + count)) { + count++; + } + files.put(partName + count, path); + } + values.add(fileName); + } + } + } catch (ResponseException re) { + throw re; + } catch (Exception e) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString()); + } + } + + private int scipOverNewLine(byte[] partHeaderBuff, int index) { + while (partHeaderBuff[index] != '\n') { + index++; + } + return ++index; + } + + /** + * Decodes parameters in percent-encoded URI-format ( e.g. + * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given Map. + */ + private void decodeParms(String parms, Map> p) { + if (parms == null) { + this.queryParameterString = ""; + return; + } + + this.queryParameterString = parms; + StringTokenizer st = new StringTokenizer(parms, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + String key = null; + String value = null; + + if (sep >= 0) { + key = decodePercent(e.substring(0, sep)).trim(); + value = decodePercent(e.substring(sep + 1)); + } else { + key = decodePercent(e).trim(); + value = ""; + } + + List values = p.get(key); + if (values == null) { + values = new ArrayList(); + p.put(key, values); + } + + values.add(value); + } + } + + @Override + public void execute() throws IOException { + Response r = null; + try { + // Read the first 8192 bytes. + // The full header should fit in here. + // Apache's default header limit is 8KB. + // Do NOT assume that a single read will get the entire header + // at once! + byte[] buf = new byte[HTTPSession.BUFSIZE]; + this.splitbyte = 0; + this.rlen = 0; + + int read = -1; + this.inputStream.mark(HTTPSession.BUFSIZE); + try { + read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE); + } catch (SSLException e) { + throw e; + } catch (IOException e) { + safeClose(this.inputStream); + safeClose(this.outputStream); + throw new SocketException("NanoHttpd Shutdown"); + } + if (read == -1) { + // socket was been closed + safeClose(this.inputStream); + safeClose(this.outputStream); + throw new SocketException("NanoHttpd Shutdown"); + } + while (read > 0) { + this.rlen += read; + this.splitbyte = findHeaderEnd(buf, this.rlen); + if (this.splitbyte > 0) { + break; + } + read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen); + } + + if (this.splitbyte < this.rlen) { + this.inputStream.reset(); + this.inputStream.skip(this.splitbyte); + } + + this.parms = new HashMap>(); + if (null == this.headers) { + this.headers = new HashMap(); + } else { + this.headers.clear(); + } + + // Create a BufferedReader for parsing the header. + BufferedReader hin = new BufferedReader( + new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen))); + + // Decode the header into parms and header java properties + Map pre = new HashMap(); + decodeHeader(hin, pre, this.parms, this.headers); + + if (null != this.remoteIp) { + this.headers.put("remote-addr", this.remoteIp); + this.headers.put("http-client-ip", this.remoteIp); + } + + this.method = Method.lookup(pre.get("method")); + if (this.method == null) { + throw new ResponseException(Response.Status.BAD_REQUEST, + "BAD REQUEST: Syntax error. HTTP verb " + pre.get("method") + " unhandled."); + } + + this.uri = pre.get("uri"); + + this.cookies = new CookieHandler(this.headers); + + String connection = this.headers.get("connection"); + boolean keepAlive = "HTTP/1.1".equals(protocolVersion) + && (connection == null || !connection.matches("(?i).*close.*")); + + // Ok, now do the serve() + + // TODO: long body_size = getBodySize(); + // TODO: long pos_before_serve = this.inputStream.totalRead() + // (requires implementation for totalRead()) + r = serve(this); + // TODO: this.inputStream.skip(body_size - + // (this.inputStream.totalRead() - pos_before_serve)) + + if (r == null) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, + "SERVER INTERNAL ERROR: Serve() returned a null response."); + } else { + String acceptEncoding = this.headers.get("accept-encoding"); + this.cookies.unloadQueue(r); + r.setRequestMethod(this.method); + r.setGzipEncoding( + useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip")); + r.setKeepAlive(keepAlive); + r.send(this.outputStream); + } + if (!keepAlive || r.isCloseConnection()) { + throw new SocketException("NanoHttpd Shutdown"); + } + } catch (SocketException e) { + // throw it out to close socket object (finalAccept) + throw e; + } catch (SocketTimeoutException ste) { + // treat socket timeouts the same way we treat socket exceptions + // i.e. close the stream & finalAccept object by throwing the + // exception up the call stack. + throw ste; + } catch (SSLException ssle) { + Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, + "SSL PROTOCOL FAILURE: " + ssle.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); + } catch (IOException ioe) { + Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, + "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); + } catch (ResponseException re) { + Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); + } finally { + safeClose(r); + this.tempFileManager.clear(); + } + } + + /** + * Find byte index separating header from body. It must be the last byte of the + * first two sequential new lines. + */ + private int findHeaderEnd(final byte[] buf, int rlen) { + int splitbyte = 0; + while (splitbyte + 1 < rlen) { + + // RFC2616 + if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && splitbyte + 3 < rlen + && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { + return splitbyte + 4; + } + + // tolerance + if (buf[splitbyte] == '\n' && buf[splitbyte + 1] == '\n') { + return splitbyte + 2; + } + splitbyte++; + } + return 0; + } + + /** + * Find the byte positions where multipart boundaries start. This reads a large + * block at a time and uses a temporary buffer to optimize (memory mapped) file + * access. + */ + private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { + int[] res = new int[0]; + if (b.remaining() < boundary.length) { + return res; + } + + int search_window_pos = 0; + byte[] search_window = new byte[4 * 1024 + boundary.length]; + + int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length; + b.get(search_window, 0, first_fill); + int new_bytes = first_fill - boundary.length; + + do { + // Search the search_window + for (int j = 0; j < new_bytes; j++) { + for (int i = 0; i < boundary.length; i++) { + if (search_window[j + i] != boundary[i]) + break; + if (i == boundary.length - 1) { + // Match found, add it to results + int[] new_res = new int[res.length + 1]; + System.arraycopy(res, 0, new_res, 0, res.length); + new_res[res.length] = search_window_pos + j; + res = new_res; + } + } + } + search_window_pos += new_bytes; + + // Copy the end of the buffer to the start + System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, + boundary.length); + + // Refill search_window + new_bytes = search_window.length - boundary.length; + new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes; + b.get(search_window, boundary.length, new_bytes); + } while (new_bytes > 0); + return res; + } + + @Override + public CookieHandler getCookies() { + return this.cookies; + } + + @Override + public final Map getHeaders() { + return this.headers; + } + + @Override + public final InputStream getInputStream() { + return this.inputStream; + } + + @Override + public final Method getMethod() { + return this.method; + } + + /** + * @deprecated use {@link #getParameters()} instead. + */ + @Override + @Deprecated + public final Map getParms() { + Map result = new HashMap(); + for (String key : this.parms.keySet()) { + result.put(key, this.parms.get(key).get(0)); + } + + return result; + } + + @Override + public final Map> getParameters() { + return this.parms; + } + + @Override + public String getQueryParameterString() { + return this.queryParameterString; + } + + private RandomAccessFile getTmpBucket() { + try { + TempFile tempFile = this.tempFileManager.createTempFile(null); + return new RandomAccessFile(tempFile.getName(), "rw"); + } catch (Exception e) { + throw new Error(e); // we won't recover, so throw an error + } + } + + @Override + public final String getUri() { + return this.uri; + } + + /** + * Deduce body length in bytes. Either from "content-length" header or read + * bytes. + */ + public long getBodySize() { + if (this.headers.containsKey("content-length")) { + return Long.parseLong(this.headers.get("content-length")); + } else if (this.splitbyte < this.rlen) { + return this.rlen - this.splitbyte; + } + return 0; + } + + @Override + public void parseBody(Map files) throws IOException, ResponseException { + RandomAccessFile randomAccessFile = null; + try { + long size = getBodySize(); + ByteArrayOutputStream baos = null; + DataOutput requestDataOutput = null; + + // Store the request in memory or a file, depending on size + if (size < MEMORY_STORE_LIMIT) { + baos = new ByteArrayOutputStream(); + requestDataOutput = new DataOutputStream(baos); + } else { + randomAccessFile = getTmpBucket(); + requestDataOutput = randomAccessFile; + } + + // Read all the body and write it to request_data_output + byte[] buf = new byte[REQUEST_BUFFER_LEN]; + while (this.rlen >= 0 && size > 0) { + this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN)); + size -= this.rlen; + if (this.rlen > 0) { + requestDataOutput.write(buf, 0, this.rlen); + } + } + + ByteBuffer fbuf = null; + if (baos != null) { + fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size()); + } else { + fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, + randomAccessFile.length()); + randomAccessFile.seek(0); + } + + // If the method is POST, there may be parameters + // in data section, too, read it: + if (Method.POST.equals(this.method)) { + ContentType contentType = new ContentType(this.headers.get("content-type")); + if (contentType.isMultipart()) { + String boundary = contentType.getBoundary(); + if (boundary == null) { + throw new ResponseException(Response.Status.BAD_REQUEST, + "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); + } + decodeMultipartFormData(contentType, fbuf, this.parms, files); + } else { + byte[] postBytes = new byte[fbuf.remaining()]; + fbuf.get(postBytes); + String postLine = new String(postBytes, contentType.getEncoding()).trim(); + // Handle application/x-www-form-urlencoded + if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType.getContentType())) { + decodeParms(postLine, this.parms); + } else if (postLine.length() != 0) { + // Special case for raw POST data => create a + // special files entry "postData" with raw content + // data + files.put("postData", postLine); + } + } + } else if (Method.PUT.equals(this.method)) { + files.put("content", saveTmpFile(fbuf, 0, fbuf.limit(), null)); + } + } finally { + safeClose(randomAccessFile); + } + } + + /** + * Retrieves the content of a sent file and saves it to a temporary file. The + * full path to the saved file is returned. + */ + private String saveTmpFile(ByteBuffer b, int offset, int len, String filename_hint) { + String path = ""; + if (len > 0) { + FileOutputStream fileOutputStream = null; + try { + TempFile tempFile = this.tempFileManager.createTempFile(filename_hint); + ByteBuffer src = b.duplicate(); + fileOutputStream = new FileOutputStream(tempFile.getName()); + FileChannel dest = fileOutputStream.getChannel(); + src.position(offset).limit(offset + len); + dest.write(src.slice()); + path = tempFile.getName(); + } catch (Exception e) { // Catch exception if any + throw new Error(e); // we won't recover, so throw an error + } finally { + safeClose(fileOutputStream); + } + } + return path; + } + + @Override + public String getRemoteIpAddress() { + return this.remoteIp; + } + + @Override + public String getRemoteHostName() { + return this.remoteHostname; + } + } + + /** + * Handles one session, i.e. parses the HTTP request and returns the response. + */ + public interface IHTTPSession { + + void execute() throws IOException; + + CookieHandler getCookies(); + + Map getHeaders(); + + InputStream getInputStream(); + + Method getMethod(); + + /** + * This method will only return the first value for a given parameter. You will + * want to use getParameters if you expect multiple values for a given key. + * + * @deprecated use {@link #getParameters()} instead. + */ + @Deprecated + Map getParms(); + + Map> getParameters(); + + String getQueryParameterString(); + + /** + * @return the path part of the URL. + */ + String getUri(); + + /** + * Adds the files in the request body to the files map. + * + * @param files map to modify + */ + void parseBody(Map files) throws IOException, ResponseException; + + /** + * Get the remote ip address of the requester. + * + * @return the IP address. + */ + String getRemoteIpAddress(); + + /** + * Get the remote hostname of the requester. + * + * @return the hostname. + */ + String getRemoteHostName(); + } + + /** + * HTTP Request methods, with the ability to decode a String back + * to its enum value. + */ + public enum Method { + GET, PUT, POST, DELETE, HEAD, OPTIONS, TRACE, CONNECT, PATCH, PROPFIND, PROPPATCH, MKCOL, MOVE, COPY, LOCK, + UNLOCK; + + static Method lookup(String method) { + if (method == null) + return null; + + try { + return valueOf(method); + } catch (IllegalArgumentException e) { + // TODO: Log it? + return null; + } + } + } + + /** + * HTTP response. Return one of these from serve(). + */ + public static class Response implements Closeable { + + public interface IStatus { + + String getDescription(); + + int getRequestStatus(); + } + + /** + * Some HTTP response status codes + */ + public enum Status implements IStatus { + SWITCH_PROTOCOL(101, "Switching Protocols"), + + OK(200, "OK"), CREATED(201, "Created"), ACCEPTED(202, "Accepted"), NO_CONTENT(204, "No Content"), + PARTIAL_CONTENT(206, "Partial Content"), MULTI_STATUS(207, "Multi-Status"), + + REDIRECT(301, "Moved Permanently"), + /** + * Many user agents mishandle 302 in ways that violate the RFC1945 spec (i.e., + * redirect a POST to a GET). 303 and 307 were added in RFC2616 to address this. + * You should prefer 303 and 307 unless the calling user agent does not support + * 303 and 307 functionality + */ + @Deprecated + FOUND(302, "Found"), REDIRECT_SEE_OTHER(303, "See Other"), NOT_MODIFIED(304, "Not Modified"), + TEMPORARY_REDIRECT(307, "Temporary Redirect"), + + BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401, "Unauthorized"), FORBIDDEN(403, "Forbidden"), + NOT_FOUND(404, "Not Found"), METHOD_NOT_ALLOWED(405, "Method Not Allowed"), + NOT_ACCEPTABLE(406, "Not Acceptable"), REQUEST_TIMEOUT(408, "Request Timeout"), CONFLICT(409, "Conflict"), + GONE(410, "Gone"), LENGTH_REQUIRED(411, "Length Required"), PRECONDITION_FAILED(412, "Precondition Failed"), + PAYLOAD_TOO_LARGE(413, "Payload Too Large"), UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"), + RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), + EXPECTATION_FAILED(417, "Expectation Failed"), TOO_MANY_REQUESTS(429, "Too Many Requests"), + + INTERNAL_ERROR(500, "Internal Server Error"), NOT_IMPLEMENTED(501, "Not Implemented"), + SERVICE_UNAVAILABLE(503, "Service Unavailable"), + UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported"); + + private final int requestStatus; + + private final String description; + + Status(int requestStatus, String description) { + this.requestStatus = requestStatus; + this.description = description; + } + + public static Status lookup(int requestStatus) { + for (Status status : Status.values()) { + if (status.getRequestStatus() == requestStatus) { + return status; + } + } + return null; + } + + @Override + public String getDescription() { + return "" + this.requestStatus + " " + this.description; + } + + @Override + public int getRequestStatus() { + return this.requestStatus; + } + + } + + /** + * Output stream that will automatically send every write to the wrapped + * OutputStream according to chunked transfer: + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 + */ + private static class ChunkedOutputStream extends FilterOutputStream { + + public ChunkedOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + byte[] data = { (byte) b }; + write(data, 0, 1); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (len == 0) + return; + out.write(String.format("%x\r\n", len).getBytes()); + out.write(b, off, len); + out.write("\r\n".getBytes()); + } + + public void finish() throws IOException { + out.write("0\r\n\r\n".getBytes()); + } + + } + + /** + * HTTP status code after processing, e.g. "200 OK", Status.OK + */ + private IStatus status; + + /** + * MIME type of content, e.g. "text/html" + */ + private String mimeType; + + /** + * Data of the response, may be null. + */ + private InputStream data; + + private long contentLength; + + /** + * Headers for the HTTP response. Use addHeader() to add lines. the lowercase + * map is automatically kept up to date. + */ + @SuppressWarnings("serial") + private final Map header = new HashMap() { + + public String put(String key, String value) { + lowerCaseHeader.put(key == null ? key : key.toLowerCase(), value); + return super.put(key, value); + }; + }; + + /** + * copy of the header map with all the keys lowercase for faster searching. + */ + private final Map lowerCaseHeader = new HashMap(); + + /** + * The request method that spawned this response. + */ + private Method requestMethod; + + /** + * Use chunkedTransfer + */ + private boolean chunkedTransfer; + + private boolean encodeAsGzip; + + private boolean keepAlive; + + /** + * Creates a fixed length response if totalBytes>=0, otherwise chunked. + */ + protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) { + this.status = status; + this.mimeType = mimeType; + if (data == null) { + this.data = new ByteArrayInputStream(new byte[0]); + this.contentLength = 0L; + } else { + this.data = data; + this.contentLength = totalBytes; + } + this.chunkedTransfer = this.contentLength < 0; + keepAlive = true; + } + + @Override + public void close() throws IOException { + if (this.data != null) { + this.data.close(); + } + } + + /** + * Adds given line to the header. + */ + public void addHeader(String name, String value) { + this.header.put(name, value); + } + + /** + * Indicate to close the connection after the Response has been sent. + * + * @param close {@code true} to hint connection closing, {@code false} to let + * connection be closed by client. + */ + public void closeConnection(boolean close) { + if (close) + this.header.put("connection", "close"); + else + this.header.remove("connection"); + } + + /** + * @return {@code true} if connection is to be closed after this Response has + * been sent. + */ + public boolean isCloseConnection() { + return "close".equals(getHeader("connection")); + } + + public InputStream getData() { + return this.data; + } + + public String getHeader(String name) { + return this.lowerCaseHeader.get(name.toLowerCase()); + } + + public String getMimeType() { + return this.mimeType; + } + + public Method getRequestMethod() { + return this.requestMethod; + } + + public IStatus getStatus() { + return this.status; + } + + public void setGzipEncoding(boolean encodeAsGzip) { + this.encodeAsGzip = encodeAsGzip; + } + + public void setKeepAlive(boolean useKeepAlive) { + this.keepAlive = useKeepAlive; + } + + /** + * Sends given response to the socket. + */ + protected void send(OutputStream outputStream) { + SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); + gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); + + try { + if (this.status == null) { + throw new Error("sendResponse(): Status can't be null."); + } + PrintWriter pw = new PrintWriter( + new BufferedWriter( + new OutputStreamWriter(outputStream, new ContentType(this.mimeType).getEncoding())), + false); + pw.append("HTTP/1.1 ").append(this.status.getDescription()).append(" \r\n"); + if (this.mimeType != null) { + printHeader(pw, "Content-Type", this.mimeType); + } + if (getHeader("date") == null) { + printHeader(pw, "Date", gmtFrmt.format(new Date())); + } + for (Entry entry : this.header.entrySet()) { + printHeader(pw, entry.getKey(), entry.getValue()); + } + if (getHeader("connection") == null) { + printHeader(pw, "Connection", (this.keepAlive ? "keep-alive" : "close")); + } + if (getHeader("content-length") != null) { + encodeAsGzip = false; + } + if (encodeAsGzip) { + printHeader(pw, "Content-Encoding", "gzip"); + setChunkedTransfer(true); + } + long pending = this.data != null ? this.contentLength : 0; + if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + printHeader(pw, "Transfer-Encoding", "chunked"); + } else if (!encodeAsGzip) { + pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, pending); + } + pw.append("\r\n"); + pw.flush(); + sendBodyWithCorrectTransferAndEncoding(outputStream, pending); + outputStream.flush(); + safeClose(this.data); + } catch (IOException ioe) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); + } + } + + @SuppressWarnings("static-method") + protected void printHeader(PrintWriter pw, String key, String value) { + pw.append(key).append(": ").append(value).append("\r\n"); + } + + protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, long defaultSize) { + String contentLengthString = getHeader("content-length"); + long size = defaultSize; + if (contentLengthString != null) { + try { + size = Long.parseLong(contentLengthString); + } catch (NumberFormatException ex) { + LOG.severe("content-length was no number " + contentLengthString); + } + } + pw.print("Content-Length: " + size + "\r\n"); + return size; + } + + private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) + throws IOException { + if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); + sendBodyWithCorrectEncoding(chunkedOutputStream, -1); + chunkedOutputStream.finish(); + } else { + sendBodyWithCorrectEncoding(outputStream, pending); + } + } + + private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { + if (encodeAsGzip) { + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); + sendBody(gzipOutputStream, -1); + gzipOutputStream.finish(); + } else { + sendBody(outputStream, pending); + } + } + + /** + * Sends the body to the specified OutputStream. The pending parameter limits + * the maximum amounts of bytes sent unless it is -1, in which case everything + * is sent. + * + * @param outputStream the OutputStream to send data to + * @param pending -1 to send everything, otherwise sets a max limit to the + * number of bytes sent + * @throws IOException if something goes wrong while sending the data. + */ + private void sendBody(OutputStream outputStream, long pending) throws IOException { + long BUFFER_SIZE = 16 * 1024; + byte[] buff = new byte[(int) BUFFER_SIZE]; + boolean sendEverything = pending == -1; + while (pending > 0 || sendEverything) { + long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE); + int read = this.data.read(buff, 0, (int) bytesToRead); + if (read <= 0) { + break; + } + outputStream.write(buff, 0, read); + if (!sendEverything) { + pending -= read; + } + } + } + + public void setChunkedTransfer(boolean chunkedTransfer) { + this.chunkedTransfer = chunkedTransfer; + } + + public void setData(InputStream data) { + this.data = data; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public void setRequestMethod(Method requestMethod) { + this.requestMethod = requestMethod; + } + + public void setStatus(IStatus status) { + this.status = status; + } + } + + public static final class ResponseException extends Exception { + + private static final long serialVersionUID = 6569838532917408380L; + + private final Response.Status status; + + public ResponseException(Response.Status status, String message) { + super(message); + this.status = status; + } + + public ResponseException(Response.Status status, String message, Exception e) { + super(message, e); + this.status = status; + } + + public Response.Status getStatus() { + return this.status; + } + } + + /** + * The runnable that will be used for the main listening thread. + */ + public class ServerRunnable implements Runnable { + + private final int timeout; + + private IOException bindException; + + private boolean hasBinded = false; + + public ServerRunnable(int timeout) { + this.timeout = timeout; + } + + @Override + public void run() { + try { + myServerSocket.bind( + hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); + hasBinded = true; + } catch (IOException e) { + this.bindException = e; + return; + } + do { + try { + final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); + if (this.timeout > 0) { + finalAccept.setSoTimeout(this.timeout); + } + final InputStream inputStream = finalAccept.getInputStream(); + NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream)); + } catch (IOException e) { + NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e); + } + } while (!NanoHTTPD.this.myServerSocket.isClosed()); + } + } + + /** + * A temp file. + *

+ *

+ * Temp files are responsible for managing the actual temporary storage and + * cleaning themselves up when no longer needed. + *

+ */ + public interface TempFile { + + public void delete() throws Exception; + + public String getName(); + + public OutputStream open() throws Exception; + } + + /** + * Temp file manager. + *

+ *

+ * Temp file managers are created 1-to-1 with incoming requests, to create and + * cleanup temporary files created as a result of handling the request. + *

+ */ + public interface TempFileManager { + + void clear(); + + public TempFile createTempFile(String filename_hint) throws Exception; + } + + /** + * Factory to create temp file managers. + */ + public interface TempFileManagerFactory { + + public TempFileManager create(); + } + + /** + * Factory to create ServerSocketFactories. + */ + public interface ServerSocketFactory { + + public ServerSocket create() throws IOException; + + } + + /** + * Maximum time to wait on Socket.getInputStream().read() (in milliseconds) This + * is required as the Keep-Alive HTTP connections would otherwise block the + * socket reading thread forever (or as long the browser is open). + */ + public static final int SOCKET_READ_TIMEOUT = 5000; + + /** + * Common MIME type for dynamic content: plain text + */ + public static final String MIME_PLAINTEXT = "text/plain"; + + /** + * Common MIME type for dynamic content: html + */ + public static final String MIME_HTML = "text/html"; + + /** + * Pseudo-Parameter to use to store the actual query string in the parameters + * map for later re-processing. + */ + private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; + + /** + * logger to log to. + */ + private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); + + /** + * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE + */ + protected static Map MIME_TYPES; + + public static Map mimeTypes() { + if (MIME_TYPES == null) { + MIME_TYPES = new HashMap(); + loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties"); + loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties"); + if (MIME_TYPES.isEmpty()) { + LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties"); + } + } + return MIME_TYPES; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void loadMimeTypes(Map result, String resourceName) { + try { + Enumeration resources = NanoHTTPD.class.getClassLoader().getResources(resourceName); + while (resources.hasMoreElements()) { + URL url = (URL) resources.nextElement(); + Properties properties = new Properties(); + InputStream stream = null; + try { + stream = url.openStream(); + properties.load(stream); + } catch (IOException e) { + LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e); + } finally { + safeClose(stream); + } + result.putAll((Map) properties); + } + } catch (IOException e) { + LOG.log(Level.INFO, "no mime types available at " + resourceName); + } + }; + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an array of + * loaded KeyManagers. These objects must properly loaded/initialized by the + * caller. + */ + public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) + throws IOException { + SSLServerSocketFactory res = null; + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(loadedKeyStore); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); + res = ctx.getServerSocketFactory(); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + return res; + } + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a loaded + * KeyManagerFactory. These objects must properly loaded/initialized by the + * caller. + */ + public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, + KeyManagerFactory loadedKeyFactory) throws IOException { + try { + return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers()); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your + * certificate and passphrase + */ + public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) + throws IOException { + try { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); + + if (keystoreStream == null) { + throw new IOException("Unable to load keystore from classpath: " + keyAndTrustStoreClasspathPath); + } + + keystore.load(keystoreStream, passphrase); + KeyManagerFactory keyManagerFactory = KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keystore, passphrase); + return makeSSLSocketFactory(keystore, keyManagerFactory); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + /** + * Get MIME type from file name extension, if possible + * + * @param uri the string representing a file + * @return the connected mime/type + */ + public static String getMimeTypeForFile(String uri) { + int dot = uri.lastIndexOf('.'); + String mime = null; + if (dot >= 0) { + mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase()); + } + return mime == null ? "application/octet-stream" : mime; + } + + private static final void safeClose(Object closeable) { + try { + if (closeable != null) { + if (closeable instanceof Closeable) { + ((Closeable) closeable).close(); + } else if (closeable instanceof Socket) { + ((Socket) closeable).close(); + } else if (closeable instanceof ServerSocket) { + ((ServerSocket) closeable).close(); + } else { + throw new IllegalArgumentException("Unknown object to close"); + } + } + } catch (IOException e) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e); + } + } + + private final String hostname; + + private final int myPort; + + private volatile ServerSocket myServerSocket; + + private ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory(); + + private Thread myThread; + + /** + * Pluggable strategy for asynchronously executing requests. + */ + protected AsyncRunner asyncRunner; + + /** + * Pluggable strategy for creating and cleaning up temporary files. + */ + private TempFileManagerFactory tempFileManagerFactory; + + /** + * Constructs an HTTP server on given port. + */ + public NanoHTTPD(int port) { + this(null, port); + } + + // ------------------------------------------------------------------------------- + // // + // + // Threading Strategy. + // + // ------------------------------------------------------------------------------- + // // + + /** + * Constructs an HTTP server on given hostname and port. + */ + public NanoHTTPD(String hostname, int port) { + this.hostname = hostname; + this.myPort = port; + setTempFileManagerFactory(new DefaultTempFileManagerFactory()); + setAsyncRunner(new DefaultAsyncRunner()); + } + + /** + * Forcibly closes all connections that are open. + */ + public synchronized void closeAllConnections() { + stop(); + } + + /** + * create a instance of the client handler, subclasses can return a subclass of + * the ClientHandler. + * + * @param finalAccept the socket the cleint is connected to + * @param inputStream the input stream + * @return the client handler + */ + protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) { + return new ClientHandler(inputStream, finalAccept); + } + + /** + * Instantiate the server runnable, can be overwritten by subclasses to provide + * a subclass of the ServerRunnable. + * + * @param timeout the socet timeout to use. + * @return the server runnable. + */ + protected ServerRunnable createServerRunnable(final int timeout) { + return new ServerRunnable(timeout); + } + + /** + * Decode parameters from a URL, handing the case where a single parameter name + * might have been supplied several times, by return lists of values. In general + * these lists will contain a single element. + * + * @param parms original NanoHTTPD parameters values, as passed to the + * serve() method. + * @return a map of String (parameter name) to + * List<String> (a list of the values supplied). + */ + protected static Map> decodeParameters(Map parms) { + return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); + } + + // ------------------------------------------------------------------------------- + // // + + /** + * Decode parameters from a URL, handing the case where a single parameter name + * might have been supplied several times, by return lists of values. In general + * these lists will contain a single element. + * + * @param queryString a query string pulled from the URL. + * @return a map of String (parameter name) to + * List<String> (a list of the values supplied). + */ + protected static Map> decodeParameters(String queryString) { + Map> parms = new HashMap>(); + if (queryString != null) { + StringTokenizer st = new StringTokenizer(queryString, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); + if (!parms.containsKey(propertyName)) { + parms.put(propertyName, new ArrayList()); + } + String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; + if (propertyValue != null) { + parms.get(propertyName).add(propertyValue); + } + } + } + return parms; + } + + /** + * Decode percent encoded String values. + * + * @param str the percent encoded String + * @return expanded form of the input, for example "foo%20bar" becomes "foo bar" + */ + protected static String decodePercent(String str) { + String decoded = null; + try { + decoded = URLDecoder.decode(str, "UTF8"); + } catch (UnsupportedEncodingException ignored) { + NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); + } + return decoded; + } + + /** + * @return true if the gzip compression should be used if the client accespts + * it. Default this option is on for text content and off for + * everything. Override this for custom semantics. + */ + @SuppressWarnings("static-method") + protected boolean useGzipWhenAccepted(Response r) { + return r.getMimeType() != null + && (r.getMimeType().toLowerCase().contains("text/") || r.getMimeType().toLowerCase().contains("/json")); + } + + public final int getListeningPort() { + return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); + } + + public final boolean isAlive() { + return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive(); + } + + public ServerSocketFactory getServerSocketFactory() { + return serverSocketFactory; + } + + public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) { + this.serverSocketFactory = serverSocketFactory; + } + + public String getHostname() { + return hostname; + } + + public TempFileManagerFactory getTempFileManagerFactory() { + return tempFileManagerFactory; + } + + /** + * Call before start() to serve over HTTPS instead of HTTP + */ + public void makeSecure(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { + this.serverSocketFactory = new SecureServerSocketFactory(sslServerSocketFactory, sslProtocols); + } + + /** + * Create a response with unknown length (using HTTP 1.1 chunking). + */ + public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { + return new Response(status, mimeType, data, -1); + } + + /** + * Create a response with known length. + */ + public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { + return new Response(status, mimeType, data, totalBytes); + } + + /** + * Create a text response with known length. + */ + public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { + ContentType contentType = new ContentType(mimeType); + if (txt == null) { + return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0); + } else { + byte[] bytes; + try { + CharsetEncoder newEncoder = Charset.forName(contentType.getEncoding()).newEncoder(); + if (!newEncoder.canEncode(txt)) { + contentType = contentType.tryUTF8(); + } + bytes = txt.getBytes(contentType.getEncoding()); + } catch (UnsupportedEncodingException e) { + NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e); + bytes = new byte[0]; + } + return newFixedLengthResponse(status, contentType.getContentTypeHeader(), new ByteArrayInputStream(bytes), + bytes.length); + } + } + + /** + * Create a text response with known length. + */ + public static Response newFixedLengthResponse(String msg) { + return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); + } + + /** + * Override this to customize the server. + *

+ *

+ * (By default, this returns a 404 "Not Found" plain text error response.) + * + * @param session The HTTP session + * @return HTTP response, see class Response for details + */ + public Response serve(IHTTPSession session) { + Map files = new HashMap(); + Method method = session.getMethod(); + if (Method.PUT.equals(method) || Method.POST.equals(method)) { + try { + session.parseBody(files); + } catch (IOException ioe) { + return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, + "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } catch (ResponseException re) { + return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); + } + } + + Map parms = session.getParms(); + parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); + return serve(session.getUri(), method, session.getHeaders(), parms, files); + } + + /** + * Override this to customize the server. + *

+ *

+ * (By default, this returns a 404 "Not Found" plain text error response.) + * + * @param uri Percent-decoded URI without parameters, for example + * "/index.cgi" + * @param method "GET", "POST" etc. + * @param parms Parsed, percent decoded parameters from URI and, in case of + * POST, data. + * @param headers Header entries, percent decoded + * @return HTTP response, see class Response for details + */ + @Deprecated + public Response serve(String uri, Method method, Map headers, Map parms, + Map files) { + return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); + } + + /** + * Pluggable strategy for asynchronously executing requests. + * + * @param asyncRunner new strategy for handling threads. + */ + public void setAsyncRunner(AsyncRunner asyncRunner) { + this.asyncRunner = asyncRunner; + } + + /** + * Pluggable strategy for creating and cleaning up temporary files. + * + * @param tempFileManagerFactory new strategy for handling temp files. + */ + public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { + this.tempFileManagerFactory = tempFileManagerFactory; + } + + /** + * Start the server. + * + * @throws IOException if the socket is in use. + */ + public void start() throws IOException { + start(NanoHTTPD.SOCKET_READ_TIMEOUT); + } + + /** + * Starts the server (in setDaemon(true) mode). + */ + public void start(final int timeout) throws IOException { + start(timeout, true); + } + + /** + * Start the server. + * + * @param timeout timeout to use for socket connections. + * @param daemon start the thread daemon or not. + * @throws IOException if the socket is in use. + */ + public void start(final int timeout, boolean daemon) throws IOException { + this.myServerSocket = this.getServerSocketFactory().create(); + this.myServerSocket.setReuseAddress(true); + + ServerRunnable serverRunnable = createServerRunnable(timeout); + this.myThread = new Thread(serverRunnable); + this.myThread.setDaemon(daemon); + this.myThread.setName("NanoHttpd Main Listener"); + this.myThread.start(); + while (!serverRunnable.hasBinded && serverRunnable.bindException == null) { + try { + Thread.sleep(10L); + } catch (Throwable e) { + // on android this may not be allowed, that's why we + // catch throwable the wait should be very short because we are + // just waiting for the bind of the socket + } + } + if (serverRunnable.bindException != null) { + throw serverRunnable.bindException; + } + } + + /** + * Stop the server. + */ + public void stop() { + try { + safeClose(this.myServerSocket); + this.asyncRunner.closeAll(); + if (this.myThread != null) { + this.myThread.join(); + } + } catch (Exception e) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e); + } + } + + public final boolean wasStarted() { + return this.myServerSocket != null && this.myThread != null; + } +} diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/OpenGLObjects.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/OpenGLObjects.java index f1cfad66..693cbbe6 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/OpenGLObjects.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/OpenGLObjects.java @@ -25,6 +25,10 @@ class OpenGLObjects { this.ptr = ptr; } + public int hashCode() { + return ptr; + } + @Override public void free() { PlatformOpenGL._wglDeleteBuffers(this); @@ -40,6 +44,10 @@ class OpenGLObjects { this.ptr = ptr; } + public int hashCode() { + return ptr; + } + @Override public void free() { PlatformOpenGL._wglDeleteVertexArrays(this); @@ -55,6 +63,10 @@ class OpenGLObjects { this.ptr = ptr; } + public int hashCode() { + return ptr; + } + @Override public void free() { PlatformOpenGL._wglDeleteTextures(this); @@ -70,6 +82,10 @@ class OpenGLObjects { this.ptr = ptr; } + public int hashCode() { + return ptr; + } + @Override public void free() { PlatformOpenGL._wglDeleteProgram(this); @@ -85,6 +101,10 @@ class OpenGLObjects { this.ptr = ptr; } + public int hashCode() { + return ptr; + } + @Override public void free() { } @@ -99,6 +119,10 @@ class OpenGLObjects { this.ptr = ptr; } + public int hashCode() { + return ptr; + } + @Override public void free() { PlatformOpenGL._wglDeleteShader(this); @@ -114,6 +138,10 @@ class OpenGLObjects { this.ptr = ptr; } + public int hashCode() { + return ptr; + } + @Override public void free() { PlatformOpenGL._wglDeleteFramebuffer(this); @@ -129,6 +157,10 @@ class OpenGLObjects { this.ptr = ptr; } + public int hashCode() { + return ptr; + } + @Override public void free() { PlatformOpenGL._wglDeleteRenderbuffer(this); @@ -144,6 +176,10 @@ class OpenGLObjects { this.ptr = ptr; } + public int hashCode() { + return ptr; + } + @Override public void free() { PlatformOpenGL._wglDeleteQueries(this); diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformApplication.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformApplication.java index 463efe63..d4985bf2 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformApplication.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformApplication.java @@ -7,6 +7,7 @@ import java.awt.Desktop; import java.awt.EventQueue; import java.awt.HeadlessException; import java.awt.Toolkit; +import java.awt.image.BufferedImage; import java.awt.Dialog.ModalExclusionType; import java.awt.Dialog.ModalityType; import java.io.File; @@ -14,16 +15,23 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import javax.imageio.ImageIO; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.filechooser.FileFilter; import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; import net.lax1dude.eaglercraft.v1_8.internal.lwjgl.MainMenuCreditsDialog; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums; /** * Copyright (c) 2022-2023 lax1dude, ayunami2000. All Rights Reserved. @@ -49,10 +57,23 @@ public class PlatformApplication { } public static void openLink(String url) { + URI safeURL; try { - Desktop.getDesktop().browse(new URI(url)); + safeURL = new URI(url); + String proto = safeURL.getScheme(); + if(!proto.equalsIgnoreCase("http") && !proto.equalsIgnoreCase("https")) { + throw new IllegalArgumentException("Suspicious protocol: " + proto); + } + }catch(URISyntaxException | IllegalArgumentException ex) { + PlatformRuntime.logger.error("Refusing to open invalid URL: {}", url); + PlatformRuntime.logger.error(ex); + return; + } + try { + Desktop.getDesktop().browse(safeURL); } catch (Throwable var5) { - EagRuntime.debugPrintStackTrace(var5); + PlatformRuntime.logger.error("Failed to browse to URL: {}", safeURL.toString()); + PlatformRuntime.logger.error(var5); } } @@ -98,9 +119,40 @@ public class PlatformApplication { return null; } } + + private static final DateFormat dateFormatSS = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss"); + private static final File screeshotsDir = new File("screenshots"); public static String saveScreenshot() { - return "nothing"; + if(!screeshotsDir.isDirectory() && !screeshotsDir.mkdirs()) { + PlatformRuntime.logger.error("Failed to create screenshots directory: {}", screeshotsDir.getAbsolutePath()); + return "nothing"; + } + String name = "screenshot_" + dateFormatSS.format(new Date()).toString() + ".png"; + int w = PlatformInput.getWindowWidth(); + int h = PlatformInput.getWindowHeight(); + ByteBuffer screenshotBuffer = PlatformRuntime.allocateByteBuffer(w * h * 4); + PlatformOpenGL._wglReadPixels(0, 0, w, h, RealOpenGLEnums.GL_RGBA, RealOpenGLEnums.GL_UNSIGNED_BYTE, screenshotBuffer); + BufferedImage bufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + int i; + for(int y = 0; y < h; ++y) { + for(int x = 0; x < w; ++x) { + i = (x + (h - y - 1) * w) << 2; + bufferedImage.setRGB(x, y, + ((screenshotBuffer.get(i) & 0xFF) << 16) | ((screenshotBuffer.get(i + 1) & 0xFF) << 8) + | (screenshotBuffer.get(i + 2) & 0xFF) | 0xFF000000); + } + } + PlatformRuntime.freeByteBuffer(screenshotBuffer); + File screenshotFile = new File(screeshotsDir, name); + try { + ImageIO.write(bufferedImage, "PNG", screenshotFile); + }catch(IOException ex) { + PlatformRuntime.logger.error("Failed to write screenshot: {}", screenshotFile.getAbsolutePath()); + return "nothing"; + } + PlatformRuntime.logger.info("Saved screenshot to: {}", screenshotFile.getAbsolutePath()); + return name; } public static void showPopup(String msg) { @@ -127,6 +179,7 @@ public class PlatformApplication { public static void displayFileChooser(final String mime, final String ext) { if(!fileChooserOpen) { fileChooserOpen = true; + clearFileChooserResult(); EventQueue.invokeLater(new Runnable() { @Override public void run() { diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformAssets.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformAssets.java index 615e3e95..54eb330d 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformAssets.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformAssets.java @@ -43,7 +43,11 @@ public class PlatformAssets { } } - public static final byte[] getResourceBytes(String path) { + public static boolean getResourceExists(String path) { + return (new File("resources", path)).isFile(); + } + + public static byte[] getResourceBytes(String path) { File loadFile = new File("resources", path); byte[] ret = new byte[(int) loadFile.length()]; try(FileInputStream is = new FileInputStream(loadFile)) { @@ -56,8 +60,12 @@ public class PlatformAssets { return null; } } - - public static final ImageData loadImageFile(InputStream data) { + + public static ImageData loadImageFile(InputStream data) { + return loadImageFile(data, "image/png"); + } + + public static ImageData loadImageFile(InputStream data, String mime) { try { BufferedImage img = ImageIO.read(data); if(img == null) { @@ -81,9 +89,13 @@ public class PlatformAssets { return null; } } - - public static final ImageData loadImageFile(byte[] data) { - return loadImageFile(new EaglerInputStream(data)); + + public static ImageData loadImageFile(byte[] data) { + return loadImageFile(new EaglerInputStream(data), "image/png"); } - + + public static ImageData loadImageFile(byte[] data, String mime) { + return loadImageFile(new EaglerInputStream(data), mime); + } + } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformAudio.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformAudio.java index 7c74b708..0656d64f 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformAudio.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformAudio.java @@ -46,7 +46,7 @@ public class PlatformAudio { protected PaulscodeAudioHandle(String sourceName) { this.sourceName = sourceName; - this.stall = System.currentTimeMillis(); + this.stall = PlatformRuntime.steadyTimeMillis(); } @Override @@ -64,7 +64,7 @@ public class PlatformAudio { @Override public void restart() { - this.stall = System.currentTimeMillis(); + this.stall = PlatformRuntime.steadyTimeMillis(); sndSystem.rewind(sourceName); sndSystem.play(sourceName); } @@ -91,7 +91,7 @@ public class PlatformAudio { @Override public boolean shouldFree() { - return !sndSystem.playing(sourceName) && System.currentTimeMillis() - this.stall > 250l; //TODO: I hate this hack + return !sndSystem.playing(sourceName) && PlatformRuntime.steadyTimeMillis() - this.stall > 250l; //TODO: I hate this hack } } @@ -125,7 +125,7 @@ public class PlatformAudio { private static SoundSystem sndSystem = null; static void platformInitialize() { - logger.info("Eaglercraft still uses Paul Lamb's SoundSystem but with LWJGL3"); + logger.info("Eaglercraft uses Paul Lamb's SoundSystem (with LWJGL3)"); logger.info(" \"Author: Paul Lamb, www.paulscode.com\""); try { SoundSystemConfig.addLibrary(LibraryLWJGLOpenAL.class); diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformFilesystem.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformFilesystem.java index e8ae9daa..c8562c58 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformFilesystem.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformFilesystem.java @@ -2,7 +2,6 @@ package net.lax1dude.eaglercraft.v1_8.internal; import java.io.File; -import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; import net.lax1dude.eaglercraft.v1_8.internal.lwjgl.DebugFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.lwjgl.JDBCFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.lwjgl.JDBCFilesystemConverter; @@ -28,104 +27,50 @@ public class PlatformFilesystem { public static final Logger logger = LogManager.getLogger("PlatformFilesystem"); + @Deprecated public static final File debugFilesystemRoot = (new File("filesystem/sp")).getAbsoluteFile(); - private static IFilesystemProvider provider = null; + public static final File filesystemsRoot = (new File("filesystem")).getAbsoluteFile(); - public static String jdbcUri = null; - public static String jdbcDriver = null; + private static final boolean isLegacyFolder = checkLegacy(); - public static void initialize() { - if(provider == null) { - if(jdbcUri != null && jdbcDriver != null) { - provider = JDBCFilesystem.initialize(jdbcUri, jdbcDriver); + private static boolean checkLegacy() { + if(!debugFilesystemRoot.isDirectory()) return false; + String[] str = debugFilesystemRoot.list(); + return str != null && str.length > 0; + } + + public static IEaglerFilesystem initializePersist(String dbName) { + String jdbcUri = System.getProperty("eagler.jdbc." + dbName + ".uri"); + String jdbcDriver = System.getProperty("eagler.jdbc." + dbName + ".driver"); + if(jdbcUri != null && jdbcDriver != null) { + try { + IEaglerFilesystem provider = JDBCFilesystem.initialize(dbName, jdbcUri, jdbcDriver); if(((JDBCFilesystem)provider).isNewFilesystem() && debugFilesystemRoot.isDirectory() && debugFilesystemRoot.list().length > 0) { JDBCFilesystemConverter.convertFilesystem("Converting filesystem, please wait...", debugFilesystemRoot, provider, true); } + return provider; + }catch(Throwable t) { + logger.error("Could not open jdbc-based filesystem: {}", dbName); + logger.error(t); + return null; + } + }else { + File f; + if(isLegacyFolder && (dbName.equals("worlds") || dbName.equals("resourcePacks"))) { + f = debugFilesystemRoot; + logger.info("Note: filesystem \"{}\" will be stored in the legacy \"sp\" folder because it exists and is not empty", dbName); }else { - provider = DebugFilesystem.initialize(debugFilesystemRoot); + f = new File(filesystemsRoot, dbName); + } + try { + return DebugFilesystem.initialize(dbName, f); + }catch(Throwable t) { + logger.error("Could not open folder-based filesystem: {}", dbName); + logger.error(t); + return null; } } } - public static void setUseJDBC(String uri) { - jdbcUri = uri; - } - - public static void setJDBCDriverClass(String driver) { - jdbcDriver = driver; - } - - public static interface IFilesystemProvider { - - boolean eaglerDelete(String pathName); - - ByteBuffer eaglerRead(String pathName); - - void eaglerWrite(String pathName, ByteBuffer data); - - boolean eaglerExists(String pathName); - - boolean eaglerMove(String pathNameOld, String pathNameNew); - - int eaglerCopy(String pathNameOld, String pathNameNew); - - int eaglerSize(String pathName); - - void eaglerIterate(String pathName, VFSFilenameIterator itr, boolean recursive); - - } - - private static void throwNotInitialized() { - throw new UnsupportedOperationException("Filesystem has not been initialized!"); - } - - public static boolean eaglerDelete(String pathName) { - if(provider == null) throwNotInitialized(); - return provider.eaglerDelete(pathName); - } - - public static ByteBuffer eaglerRead(String pathName) { - if(provider == null) throwNotInitialized(); - return provider.eaglerRead(pathName); - } - - public static void eaglerWrite(String pathName, ByteBuffer data) { - if(provider == null) throwNotInitialized(); - provider.eaglerWrite(pathName, data); - } - - public static boolean eaglerExists(String pathName) { - if(provider == null) throwNotInitialized(); - return provider.eaglerExists(pathName); - } - - public static boolean eaglerMove(String pathNameOld, String pathNameNew) { - if(provider == null) throwNotInitialized(); - return provider.eaglerMove(pathNameOld, pathNameNew); - } - - public static int eaglerCopy(String pathNameOld, String pathNameNew) { - if(provider == null) throwNotInitialized(); - return provider.eaglerCopy(pathNameOld, pathNameNew); - } - - public static int eaglerSize(String pathName) { - if(provider == null) throwNotInitialized(); - return provider.eaglerSize(pathName); - } - - public static void eaglerIterate(String pathName, VFSFilenameIterator itr, boolean recursive) { - if(provider == null) throwNotInitialized(); - provider.eaglerIterate(pathName, itr, recursive); - } - - public static void platformShutdown() { - if(provider != null) { - if(provider instanceof JDBCFilesystem) { - ((JDBCFilesystem)provider).shutdown(); - } - provider = null; - } - } } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformInput.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformInput.java index d8648711..d8d15cd5 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformInput.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformInput.java @@ -2,15 +2,20 @@ package net.lax1dude.eaglercraft.v1_8.internal; import static org.lwjgl.glfw.GLFW.*; +import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Objects; +import java.util.Set; import org.lwjgl.PointerBuffer; +import org.lwjgl.glfw.GLFWGamepadState; import org.lwjgl.glfw.GLFWVidMode; import org.lwjgl.system.MemoryStack; /** - * Copyright (c) 2022-2023 lax1dude, ayunami2000. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -34,6 +39,7 @@ public class PlatformInput { private static boolean windowFocused = true; private static boolean windowResized = true; + private static boolean windowResized2 = true; private static boolean windowCursorEntered = true; private static boolean windowMouseGrabbed = false; @@ -46,7 +52,7 @@ public class PlatformInput { private static int windowWidth = 640; private static int windowHeight = 480; - private static final List keyboardEventList = new LinkedList(); + private static final List keyboardEventList = new LinkedList<>(); private static KeyboardEvent currentKeyboardEvent = null; private static final char[] keyboardReleaseEventChars = new char[256]; @@ -56,7 +62,7 @@ public class PlatformInput { public static boolean lockKeys = false; - private static final List keyboardCharList = new LinkedList(); + private static final List keyboardCharList = new LinkedList<>(); private static boolean vsync = true; private static boolean glfwVSyncState = false; @@ -75,8 +81,8 @@ public class PlatformInput { } } - - private static final List mouseEventList = new LinkedList(); + + private static final List mouseEventList = new LinkedList<>(); private static MouseEvent currentMouseEvent = null; private static class MouseEvent { @@ -97,6 +103,44 @@ public class PlatformInput { } + private static final List gamepadList = new ArrayList<>(); + private static int selectedGamepad = -1; + private static String selectedGamepadName = null; + private static String selectedGamepadUUID = null; + private static final boolean[] gamepadButtonStates = new boolean[24]; + private static final float[] gamepadAxisStates = new float[4]; + + private static class Gamepad { + + protected final int gamepadId; + protected final String gamepadName; + protected final String gamepadUUID; + + protected Gamepad(int gamepadId, String gamepadName, String gamepadUUID) { + this.gamepadId = gamepadId; + this.gamepadName = gamepadName; + this.gamepadUUID = gamepadUUID; + } + + @Override + public int hashCode() { + return Objects.hash(gamepadId, gamepadName, gamepadUUID); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Gamepad other = (Gamepad) obj; + return gamepadId == other.gamepadId && Objects.equals(gamepadName, other.gamepadName) + && Objects.equals(gamepadUUID, other.gamepadUUID); + } + } + static void initHooks(long glfwWindow) { win = glfwWindow; @@ -121,13 +165,20 @@ public class PlatformInput { windowWidth = v1[0]; windowHeight = v2[0]; + windowResized = true; + windowResized2 = true; glfwSetFramebufferSizeCallback(glfwWindow, (window, width, height) -> { - windowWidth = width; - windowHeight = height; - windowResized = true; + if(windowWidth != width || windowHeight != height) { + windowWidth = width; + windowHeight = height; + windowResized = true; + windowResized2 = true; + } }); + windowFocused = true; + glfwSetWindowFocusCallback(glfwWindow, (window, focused) -> { windowFocused = focused; }); @@ -199,6 +250,15 @@ public class PlatformInput { if(!fullscreen && startupFullscreen) { toggleFullscreen(); } + + gamepadEnumerateDevices(); + + glfwSetJoystickCallback((jid, event) -> { + if(event == GLFW_DISCONNECTED && jid == selectedGamepad) { + selectedGamepad = -1; + } + gamepadEnumerateDevices(); + }); } public static int getWindowWidth() { @@ -209,6 +269,22 @@ public class PlatformInput { return windowHeight; } + public static int getVisualViewportX() { + return 0; + } + + public static int getVisualViewportY() { + return 0; + } + + public static int getVisualViewportW() { + return windowWidth; + } + + public static int getVisualViewportH() { + return windowHeight; + } + public static boolean getWindowFocused() { return windowFocused; } @@ -240,6 +316,12 @@ public class PlatformInput { return b; } + public static boolean wasVisualViewportResized() { + boolean b = windowResized2; + windowResized2 = false; + return b; + } + public static boolean keyboardNext() { if(keyboardEventList.size() > 0) { currentKeyboardEvent = keyboardEventList.remove(0); @@ -274,6 +356,33 @@ public class PlatformInput { } } + public static void keyboardFireEvent(EnumFireKeyboardEvent eventType, int eagKey, char keyChar) { + switch(eventType) { + case KEY_DOWN: + keyboardCharList.add(keyChar); + keyboardEventList.add(new KeyboardEvent(eagKey, true, false)); + break; + case KEY_UP: + if(eagKey >= 0 && eagKey < keyboardReleaseEventChars.length) { + keyboardReleaseEventChars[eagKey] = keyChar; + } + keyboardEventList.add(new KeyboardEvent(eagKey, false, false)); + break; + case KEY_REPEAT: + keyboardCharList.add(keyChar); + keyboardEventList.add(new KeyboardEvent(eagKey, true, true)); + break; + default: + throw new UnsupportedOperationException(); + } + if(keyboardEventList.size() > 64) { + keyboardEventList.remove(0); + } + if(keyboardCharList.size() > 64) { + keyboardCharList.remove(0); + } + } + public static boolean keyboardGetEventKeyState() { return currentKeyboardEvent.pressed; } @@ -315,6 +424,44 @@ public class PlatformInput { } } + public static void mouseFireMoveEvent(EnumFireMouseEvent eventType, int posX, int posY) { + if(eventType == EnumFireMouseEvent.MOUSE_MOVE) { + mouseEventList.add(new MouseEvent(-1, false, posX, posY, 0.0f)); + if(mouseEventList.size() > 64) { + mouseEventList.remove(0); + } + }else { + throw new UnsupportedOperationException(); + } + } + + public static void mouseFireButtonEvent(EnumFireMouseEvent eventType, int posX, int posY, int button) { + switch(eventType) { + case MOUSE_DOWN: + mouseEventList.add(new MouseEvent(button, true, posX, posY, 0.0f)); + break; + case MOUSE_UP: + mouseEventList.add(new MouseEvent(button, false, posX, posY, 0.0f)); + break; + default: + throw new UnsupportedOperationException(); + } + if(mouseEventList.size() > 64) { + mouseEventList.remove(0); + } + } + + public static void mouseFireWheelEvent(EnumFireMouseEvent eventType, int posX, int posY, float wheel) { + if(eventType == EnumFireMouseEvent.MOUSE_WHEEL) { + mouseEventList.add(new MouseEvent(-1, false, posX, posY, wheel)); + if(mouseEventList.size() > 64) { + mouseEventList.remove(0); + } + }else { + throw new UnsupportedOperationException(); + } + } + public static boolean mouseGetEventButtonState() { return currentMouseEvent.pressed; } @@ -366,6 +513,10 @@ public class PlatformInput { } } + public static boolean mouseGrabSupported() { + return true; + } + public static boolean isPointerLocked() { return windowMouseGrabbed; } @@ -404,6 +555,10 @@ public class PlatformInput { functionKeyModifier = KeyboardConstants.getGLFWKeyFromEagler(key); } + public static boolean supportsFullscreen() { + return true; + } + private static boolean fullscreen = false; private static boolean startupFullscreen = false; private static int[] lastPos = new int[4]; @@ -483,4 +638,247 @@ public class PlatformInput { break; } } + + public static boolean touchNext() { + return false; + } + + public static EnumTouchEvent touchGetEventType() { + return null; + } + + public static int touchGetEventTouchPointCount() { + return 0; + } + + public static int touchGetEventTouchX(int pointId) { + return 0; + } + + public static int touchGetEventTouchY(int pointId) { + return 0; + } + + public static float touchGetEventTouchRadiusX(int pointId) { + return 0.0f; + } + + public static float touchGetEventTouchRadiusY(int pointId) { + return 0.0f; + } + + public static float touchGetEventTouchRadiusMixed(int pointId) { + return touchGetEventTouchRadiusX(pointId) * 0.5f + touchGetEventTouchRadiusY(pointId) * 0.5f; + } + + public static float touchGetEventTouchForce(int pointId) { + return 0.0f; + } + + public static int touchGetEventTouchPointUID(int pointId) { + return 0; + } + + public static int touchPointCount() { + return 0; + } + + public static int touchPointX(int pointId) { + return 0; + } + + public static int touchPointY(int pointId) { + return 0; + } + + public static float touchRadiusX(int pointId) { + return 0.0f; + } + + public static float touchRadiusY(int pointId) { + return 0.0f; + } + + public static float touchRadiusMixed(int pointId) { + return touchRadiusX(pointId) * 0.5f + touchRadiusY(pointId) * 0.5f; + } + + public static float touchForce(int pointId) { + return 0.0f; + } + + public static int touchPointUID(int pointId) { + return 0; + } + + public static void touchSetOpenKeyboardZone(int x, int y, int w, int h) { + + } + + public static void touchCloseDeviceKeyboard() { + + } + + public static boolean touchIsDeviceKeyboardOpenMAYBE() { + return false; + } + + public static String touchGetPastedString() { + return null; + } + + private static void gamepadEnumerateDevices() { + if(selectedGamepad != -1 && !glfwJoystickIsGamepad(selectedGamepad)) { + selectedGamepad = -1; + } + List oldList = null; + if(!gamepadList.isEmpty()) { + oldList = new ArrayList<>(gamepadList); + gamepadList.clear(); + } + for(int i = GLFW_JOYSTICK_1; i <= GLFW_JOYSTICK_16; ++i) { + if(glfwJoystickIsGamepad(i)) { + gamepadList.add(new Gamepad(i, gamepadMakeName(i), glfwGetJoystickGUID(i))); + } + } + vigg: if(selectedGamepad != -1) { + for(int i = 0, l = gamepadList.size(); i < l; ++i) { + Gamepad gp = gamepadList.get(i); + if(gp.gamepadId == selectedGamepad && gp.gamepadUUID.equals(selectedGamepadUUID)) { + break vigg; + } + } + selectedGamepad = -1; + } + if(oldList == null) { + if(!gamepadList.isEmpty()) { + for(int i = 0, l = gamepadList.size(); i < l; ++i) { + PlatformRuntime.logger.info("Found controller: {}", gamepadList.get(i).gamepadName); + } + } + }else { + if(gamepadList.isEmpty()) { + for(int i = 0, l = oldList.size(); i < l; ++i) { + PlatformRuntime.logger.info("Lost controller: {}", oldList.get(i).gamepadName); + } + }else { + Set oldGamepadUUIDs = new HashSet<>(); + for(int i = 0, l = oldList.size(); i < l; ++i) { + oldGamepadUUIDs.add(oldList.get(i).gamepadUUID); + } + Set newGamepadUUIDs = new HashSet<>(); + for(int i = 0, l = gamepadList.size(); i < l; ++i) { + newGamepadUUIDs.add(gamepadList.get(i).gamepadUUID); + } + for(int i = 0, l = oldList.size(); i < l; ++i) { + Gamepad g = oldList.get(i); + if(!newGamepadUUIDs.contains(g.gamepadUUID)) { + PlatformRuntime.logger.info("Lost controller: {}", g.gamepadName); + } + } + for(int i = 0, l = gamepadList.size(); i < l; ++i) { + Gamepad g = gamepadList.get(i); + if(!oldGamepadUUIDs.contains(g.gamepadUUID)) { + PlatformRuntime.logger.info("Found controller: {}", g.gamepadName); + } + } + } + } + + } + + private static String gamepadMakeName(int glfwId) { + String s = glfwGetGamepadName(glfwId); + if(s.endsWith(" (GLFW)")) { + s = s.substring(0, s.length() - 7); + } + return glfwGetJoystickName(glfwId) + " (" + s + ")"; + } + + public static int gamepadGetValidDeviceCount() { + return gamepadList.size(); + } + + public static String gamepadGetDeviceName(int deviceId) { + if(deviceId >= 0 && deviceId < gamepadList.size()) { + return gamepadList.get(deviceId).gamepadName; + }else { + return "Unknown"; + } + } + + public static void gamepadSetSelectedDevice(int deviceId) { + gamepadReset(); + if(deviceId >= 0 && deviceId < gamepadList.size()) { + selectedGamepad = gamepadList.get(deviceId).gamepadId; + if(!glfwJoystickIsGamepad(selectedGamepad)) { + selectedGamepad = -1; + } + }else { + selectedGamepad = -1; + } + } + + private static void gamepadReset() { + for(int i = 0; i < gamepadButtonStates.length; ++i) { + gamepadButtonStates[i] = false; + } + for(int i = 0; i < gamepadAxisStates.length; ++i) { + gamepadAxisStates[i] = 0.0f; + } + } + + public static void gamepadUpdate() { + gamepadReset(); + if(selectedGamepad != -1) { + if(!glfwJoystickIsGamepad(selectedGamepad)) { + selectedGamepad = -1; + return; + } + try(MemoryStack ms = MemoryStack.stackPush()) { + GLFWGamepadState state = GLFWGamepadState.calloc(ms); + glfwGetGamepadState(selectedGamepad, state); + java.nio.FloatBuffer axes = state.axes(); + axes.get(gamepadAxisStates); + java.nio.ByteBuffer buttons = state.buttons(); + for(int i = 0, l = buttons.remaining(); i < l && i < gamepadButtonStates.length; ++i) { + boolean v = buttons.get() != (byte)0; + int j = GamepadConstants.getEaglerButtonFromGLFW(i); + if(j != -1) { + gamepadButtonStates[j] = v; + } + } + gamepadButtonStates[GamepadConstants.GAMEPAD_LEFT_TRIGGER] = axes.get() > 0.4f; + gamepadButtonStates[GamepadConstants.GAMEPAD_RIGHT_TRIGGER] = axes.get() > 0.4f; + } + } + } + + public static boolean gamepadIsValid() { + return selectedGamepad != -1; + } + + public static String gamepadGetName() { + return selectedGamepad != -1 ? selectedGamepadName : "Unknown"; + } + + public static boolean gamepadGetButtonState(int button) { + return selectedGamepad != -1 && button >= 0 && button < gamepadButtonStates.length ? gamepadButtonStates[button] : false; + } + + public static float gamepadGetAxis(int axis) { + return selectedGamepad != -1 && axis >= 0 && axis < gamepadAxisStates.length ? gamepadAxisStates[axis] : 0.0f; + } + + public static float getDPI() { + float[] dpiX = new float[1]; + float[] dpiY = new float[1]; + glfwGetWindowContentScale(win, dpiX, dpiY); + float ret = dpiX[0] * 0.5f + dpiY[0] * 0.5f; + if(ret <= 0.0f) { + ret = 1.0f; + } + return ret; + } + } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformNetworking.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformNetworking.java index 2989e560..3b81f5d0 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformNetworking.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformNetworking.java @@ -2,17 +2,13 @@ package net.lax1dude.eaglercraft.v1_8.internal; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -import org.java_websocket.enums.ReadyState; +import net.lax1dude.eaglercraft.v1_8.internal.lwjgl.DesktopWebSocketClient; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; /** - * Copyright (c) 2022-2023 lax1dude, ayunami2000. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -28,124 +24,22 @@ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; */ public class PlatformNetworking { - static final Logger networkLogger = LogManager.getLogger("PlatformNetworking"); + private static final Logger logger = LogManager.getLogger("PlatformNetworking"); - private static WebSocketPlayClient wsPlayClient = null; - static EnumEaglerConnectionState playConnectState = EnumEaglerConnectionState.CLOSED; - static EnumServerRateLimit serverRateLimit = null; - - static String currentURI = null; - - public static EnumEaglerConnectionState playConnectionState() { - return ((wsPlayClient == null || wsPlayClient.isClosed()) && playConnectState == EnumEaglerConnectionState.CONNECTING) ? EnumEaglerConnectionState.FAILED : - ((wsPlayClient != null && wsPlayClient.getReadyState() == ReadyState.NOT_YET_CONNECTED) ? EnumEaglerConnectionState.CONNECTING : - (((wsPlayClient == null || wsPlayClient.isClosed()) && playConnectState != EnumEaglerConnectionState.FAILED) ? EnumEaglerConnectionState.CLOSED : playConnectState)); - } - - public static void startPlayConnection(String destination) { - if(!playConnectionState().isClosed()) { - networkLogger.warn("Tried connecting to a server while already connected to a different server!"); - playDisconnect(); - } - - currentURI = destination; - - synchronized(playPackets) { - playPackets.clear(); - } - - playConnectState = EnumEaglerConnectionState.CONNECTING; - networkLogger.info("Connecting to server: {}", destination); - - URI u; - + public static IWebSocketClient openWebSocket(String socketURI) { try { - u = new URI(destination); - }catch(URISyntaxException ex) { - networkLogger.error("Invalid server URI: {}", destination); - playConnectState = EnumEaglerConnectionState.FAILED; - return; - } - - wsPlayClient = new WebSocketPlayClient(u); - wsPlayClient.connect(); - } - - public static void playDisconnect() { - if(!playConnectionState().isClosed() && wsPlayClient != null) { - try { - wsPlayClient.closeBlocking(); - } catch (InterruptedException e) { - // :( - } - playConnectState = EnumEaglerConnectionState.CLOSED; - } - } - - private static final List playPackets = new LinkedList(); - - public static byte[] readPlayPacket() { - synchronized(playPackets) { - return playPackets.size() > 0 ? playPackets.remove(0) : null; - } - } - - public static List readAllPacket() { - synchronized(playPackets) { - if(!playPackets.isEmpty()) { - List ret = new ArrayList<>(playPackets); - playPackets.clear(); - return ret; - }else { - return null; - } - } - } - - public static int countAvailableReadData() { - int total = 0; - synchronized(playPackets) { - for(int i = 0, l = playPackets.size(); i < l; ++i) { - total += playPackets.get(i).length; - } - } - return total; - } - - static void recievedPlayPacket(byte[] arg0) { - synchronized(playPackets) { - playPackets.add(arg0); - } - } - - public static void writePlayPacket(byte[] pkt) { - if(wsPlayClient == null || wsPlayClient.isClosed()) { - networkLogger.error("Tried to send {} byte play packet while the socket was closed!", pkt.length); - }else { - wsPlayClient.send(pkt); - } - } - - public static IServerQuery sendServerQuery(String uri, String accept) { - URI u; - - try { - u = new URI(uri); - }catch(URISyntaxException ex) { - networkLogger.error("Invalid server URI: {}", uri); - playConnectState = EnumEaglerConnectionState.FAILED; + URI uri = new URI(socketURI); + return new DesktopWebSocketClient(uri); + }catch(Throwable t) { + logger.error("Could not open WebSocket to \"{}\"!", socketURI); + logger.error(t); return null; } - - return new WebSocketServerQuery(accept, u); } - public static EnumServerRateLimit getRateLimit() { - return serverRateLimit == null ? EnumServerRateLimit.OK : serverRateLimit; - } - - public static String getCurrentURI() { - return currentURI; + public static IWebSocketClient openWebSocketUnsafe(String socketURI) throws URISyntaxException { + URI uri = new URI(socketURI); + return new DesktopWebSocketClient(uri); } } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformOpenGL.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformOpenGL.java index 7fffdcfd..768dbb91 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformOpenGL.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformOpenGL.java @@ -6,6 +6,13 @@ import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer; import net.lax1dude.eaglercraft.v1_8.internal.buffer.IntBuffer; import static org.lwjgl.opengles.GLES30.*; +import static org.lwjgl.opengles.ANGLEInstancedArrays.*; +import static org.lwjgl.opengles.EXTInstancedArrays.*; +import static org.lwjgl.opengles.EXTTextureStorage.*; +import static org.lwjgl.opengles.OESVertexArrayObject.*; + +import java.util.ArrayList; +import java.util.List; import org.lwjgl.opengles.GLESCapabilities; @@ -26,10 +33,103 @@ import org.lwjgl.opengles.GLESCapabilities; */ public class PlatformOpenGL { + private static int glesVers = -1; + + private static boolean hasANGLEInstancedArrays = false; + private static boolean hasEXTColorBufferFloat = false; + private static boolean hasEXTColorBufferHalfFloat = false; + private static boolean hasEXTGPUShader5 = false; + private static boolean hasEXTInstancedArrays = false; + private static boolean hasEXTShaderTextureLOD = false; + private static boolean hasEXTTextureStorage = false; + private static boolean hasOESFBORenderMipmap = false; + private static boolean hasOESGPUShader5 = false; + private static boolean hasOESVertexArrayObject = false; + private static boolean hasOESTextureFloat = false; + private static boolean hasOESTextureFloatLinear = false; + private static boolean hasOESTextureHalfFloat = false; + private static boolean hasOESTextureHalfFloatLinear = false; + private static boolean hasEXTTextureFilterAnisotropic = false; + + private static boolean hasFBO16FSupport = false; + private static boolean hasFBO32FSupport = false; + private static boolean hasLinearHDR16FSupport = false; private static boolean hasLinearHDR32FSupport = false; - static void setCurrentContext(GLESCapabilities caps) { + private static final int VAO_IMPL_NONE = -1; + private static final int VAO_IMPL_CORE = 0; + private static final int VAO_IMPL_OES = 1; + private static int vertexArrayImpl = VAO_IMPL_NONE; + + private static final int INSTANCE_IMPL_NONE = -1; + private static final int INSTANCE_IMPL_CORE = 0; + private static final int INSTANCE_IMPL_ANGLE = 1; + private static final int INSTANCE_IMPL_EXT = 2; + private static int instancingImpl = INSTANCE_IMPL_NONE; + + private static final int TEX_STORAGE_IMPL_NONE = -1; + private static final int TEX_STORAGE_IMPL_CORE = 0; + private static final int TEX_STORAGE_IMPL_EXT = 1; + private static int texStorageImpl = TEX_STORAGE_IMPL_NONE; + + static void setCurrentContext(int glesVersIn, GLESCapabilities caps) { + glesVers = glesVersIn; + + hasANGLEInstancedArrays = glesVersIn == 200 && caps.GL_ANGLE_instanced_arrays; + hasOESTextureFloat = glesVersIn == 200 && caps.GL_OES_texture_float; + hasOESTextureFloatLinear = glesVersIn >= 300 && caps.GL_OES_texture_float_linear; + hasOESTextureHalfFloat = glesVersIn == 200 && caps.GL_OES_texture_half_float; + hasOESTextureHalfFloatLinear = glesVersIn == 200 && caps.GL_OES_texture_half_float_linear; + hasEXTColorBufferFloat = (glesVersIn == 310 || glesVersIn == 300) && caps.GL_EXT_color_buffer_float; + hasEXTColorBufferHalfFloat = !hasEXTColorBufferFloat + && (glesVersIn == 310 || glesVersIn == 300 || glesVersIn == 200) && caps.GL_EXT_color_buffer_half_float; + hasEXTInstancedArrays = !hasANGLEInstancedArrays && glesVersIn == 200 && caps.GL_EXT_instanced_arrays; + hasEXTShaderTextureLOD = glesVersIn == 200 && caps.GL_EXT_shader_texture_lod; + hasEXTTextureStorage = glesVersIn == 200 && caps.GL_EXT_texture_storage; + hasOESGPUShader5 = glesVersIn == 310 && caps.GL_OES_gpu_shader5; + hasEXTGPUShader5 = !hasOESGPUShader5 && glesVersIn == 310 && caps.GL_EXT_gpu_shader5; + hasOESFBORenderMipmap = glesVersIn == 200 && caps.GL_OES_fbo_render_mipmap; + hasOESVertexArrayObject = glesVersIn == 200 && caps.GL_OES_vertex_array_object; hasLinearHDR32FSupport = caps.GL_OES_texture_float_linear; + hasEXTTextureFilterAnisotropic = caps.GL_EXT_texture_filter_anisotropic; + + hasFBO16FSupport = glesVersIn >= 320 || ((glesVersIn >= 300 || hasOESTextureFloat) && (hasEXTColorBufferFloat || hasEXTColorBufferHalfFloat)); + hasFBO32FSupport = glesVersIn >= 320 || ((glesVersIn >= 300 || hasOESTextureHalfFloat) && hasEXTColorBufferFloat); + hasLinearHDR16FSupport = glesVersIn >= 300 || hasOESTextureHalfFloatLinear; + hasLinearHDR32FSupport = glesVersIn >= 300 && hasOESTextureFloatLinear; + + if(glesVersIn >= 300) { + vertexArrayImpl = VAO_IMPL_CORE; + instancingImpl = INSTANCE_IMPL_CORE; + texStorageImpl = TEX_STORAGE_IMPL_CORE; + }else if(glesVersIn == 200) { + vertexArrayImpl = hasOESVertexArrayObject ? VAO_IMPL_OES : VAO_IMPL_NONE; + instancingImpl = hasANGLEInstancedArrays ? INSTANCE_IMPL_ANGLE : (hasEXTInstancedArrays ? INSTANCE_IMPL_EXT : INSTANCE_IMPL_NONE); + texStorageImpl = hasEXTTextureStorage ? TEX_STORAGE_IMPL_EXT : TEX_STORAGE_IMPL_NONE; + }else { + vertexArrayImpl = VAO_IMPL_NONE; + instancingImpl = INSTANCE_IMPL_NONE; + texStorageImpl = TEX_STORAGE_IMPL_NONE; + } + } + + public static final List dumpActiveExtensions() { + List exts = new ArrayList<>(); + if(hasANGLEInstancedArrays) exts.add("ANGLE_instanced_arrays"); + if(hasEXTColorBufferFloat) exts.add("EXT_color_buffer_float"); + if(hasEXTColorBufferHalfFloat) exts.add("EXT_color_buffer_half_float"); + if(hasEXTGPUShader5) exts.add("EXT_gpu_shader5"); + if(hasEXTInstancedArrays) exts.add("EXT_instanced_arrays"); + if(hasEXTTextureStorage) exts.add("EXT_texture_storage"); + if(hasOESFBORenderMipmap) exts.add("OES_fbo_render_mipmap"); + if(hasOESGPUShader5) exts.add("OES_gpu_shader5"); + if(hasOESVertexArrayObject) exts.add("OES_vertex_array_object"); + if(hasOESTextureFloat) exts.add("OES_texture_float"); + if(hasOESTextureFloatLinear) exts.add("OES_texture_float_linear"); + if(hasOESTextureHalfFloat) exts.add("OES_texture_half_float"); + if(hasOESTextureHalfFloatLinear) exts.add("OES_texture_half_float_linear"); + if(hasEXTTextureFilterAnisotropic) exts.add("EXT_texture_filter_anisotropic"); + return exts; } public static final void _wglEnable(int glEnum) { @@ -89,17 +189,45 @@ public class PlatformOpenGL { } public static final void _wglDrawBuffers(int buffer) { - glDrawBuffers(buffer); + if(glesVers == 200) { + if(buffer != 0x8CE0) { // GL_COLOR_ATTACHMENT0 + throw new UnsupportedOperationException(); + } + }else { + glDrawBuffers(buffer); + } } public static final void _wglDrawBuffers(int[] buffers) { - glDrawBuffers(buffers); + if(glesVers == 200) { + if(buffers.length != 1 || buffers[0] != 0x8CE0) { // GL_COLOR_ATTACHMENT0 + throw new UnsupportedOperationException(); + } + }else { + glDrawBuffers(buffers); + } } public static final void _wglReadBuffer(int buffer) { glReadBuffer(buffer); } + public static final void _wglReadPixels(int x, int y, int width, int height, int format, int type, ByteBuffer data) { + nglReadPixels(x, y, width, height, format, type, EaglerLWJGLAllocator.getAddress(data)); + } + + public static final void _wglReadPixels_u16(int x, int y, int width, int height, int format, int type, ByteBuffer data) { + nglReadPixels(x, y, width, height, format, type, EaglerLWJGLAllocator.getAddress(data)); + } + + public static final void _wglReadPixels(int x, int y, int width, int height, int format, int type, IntBuffer data) { + nglReadPixels(x, y, width, height, format, type, EaglerLWJGLAllocator.getAddress(data)); + } + + public static final void _wglReadPixels(int x, int y, int width, int height, int format, int type, FloatBuffer data) { + nglReadPixels(x, y, width, height, format, type, EaglerLWJGLAllocator.getAddress(data)); + } + public static final void _wglPolygonOffset(float f1, float f2) { glPolygonOffset(f1, f2); } @@ -117,7 +245,14 @@ public class PlatformOpenGL { } public static final IBufferArrayGL _wglGenVertexArrays() { - return new OpenGLObjects.BufferArrayGL(glGenVertexArrays()); + switch(vertexArrayImpl) { + case VAO_IMPL_CORE: + return new OpenGLObjects.BufferArrayGL(glGenVertexArrays()); + case VAO_IMPL_OES: + return new OpenGLObjects.BufferArrayGL(glGenVertexArraysOES()); + default: + throw new UnsupportedOperationException(); + } } public static final IProgramGL _wglCreateProgram() { @@ -149,7 +284,17 @@ public class PlatformOpenGL { } public static final void _wglDeleteVertexArrays(IBufferArrayGL obj) { - glDeleteVertexArrays(((OpenGLObjects.BufferArrayGL) obj).ptr); + int ptr = ((OpenGLObjects.BufferArrayGL) obj).ptr; + switch(vertexArrayImpl) { + case VAO_IMPL_CORE: + glDeleteVertexArrays(ptr); + break; + case VAO_IMPL_OES: + glDeleteVertexArraysOES(ptr); + break; + default: + throw new UnsupportedOperationException(); + } } public static final void _wglDeleteProgram(IProgramGL obj) { @@ -211,7 +356,17 @@ public class PlatformOpenGL { } public static final void _wglBindVertexArray(IBufferArrayGL obj) { - glBindVertexArray(obj == null ? 0 : ((OpenGLObjects.BufferArrayGL) obj).ptr); + int ptr = obj == null ? 0 : ((OpenGLObjects.BufferArrayGL) obj).ptr; + switch(vertexArrayImpl) { + case VAO_IMPL_CORE: + glBindVertexArray(ptr); + break; + case VAO_IMPL_OES: + glBindVertexArrayOES(ptr); + break; + default: + throw new UnsupportedOperationException(); + } } public static final void _wglEnableVertexAttribArray(int index) { @@ -228,7 +383,19 @@ public class PlatformOpenGL { } public static final void _wglVertexAttribDivisor(int index, int divisor) { - glVertexAttribDivisor(index, divisor); + switch(instancingImpl) { + case INSTANCE_IMPL_CORE: + glVertexAttribDivisor(index, divisor); + break; + case INSTANCE_IMPL_ANGLE: + glVertexAttribDivisorANGLE(index, divisor); + break; + case INSTANCE_IMPL_EXT: + glVertexAttribDivisorEXT(index, divisor); + break; + default: + throw new UnsupportedOperationException(); + } } public static final void _wglActiveTexture(int texture) { @@ -313,7 +480,16 @@ public class PlatformOpenGL { } public static final void _wglTexStorage2D(int target, int levels, int internalFormat, int w, int h) { - glTexStorage2D(target, levels, internalFormat, w, h); + switch(texStorageImpl) { + case TEX_STORAGE_IMPL_CORE: + glTexStorage2D(target, levels, internalFormat, w, h); + break; + case TEX_STORAGE_IMPL_EXT: + glTexStorage2DEXT(target, levels, internalFormat, w, h); + break; + default: + throw new UnsupportedOperationException(); + } } public static final void _wglPixelStorei(int pname, int value) { @@ -377,7 +553,19 @@ public class PlatformOpenGL { } public static final void _wglDrawArraysInstanced(int mode, int first, int count, int instanced) { - glDrawArraysInstanced(mode, first, count, instanced); + switch(instancingImpl) { + case INSTANCE_IMPL_CORE: + glDrawArraysInstanced(mode, first, count, instanced); + break; + case INSTANCE_IMPL_ANGLE: + glDrawArraysInstancedANGLE(mode, first, count, instanced); + break; + case INSTANCE_IMPL_EXT: + glDrawArraysInstancedEXT(mode, first, count, instanced); + break; + default: + throw new UnsupportedOperationException(); + } } public static final void _wglDrawElements(int mode, int count, int type, int offset) { @@ -385,7 +573,19 @@ public class PlatformOpenGL { } public static final void _wglDrawElementsInstanced(int mode, int count, int type, int offset, int instanced) { - glDrawElementsInstanced(mode, count, type, offset, instanced); + switch(instancingImpl) { + case INSTANCE_IMPL_CORE: + glDrawElementsInstanced(mode, count, type, offset, instanced); + break; + case INSTANCE_IMPL_ANGLE: + glDrawElementsInstancedANGLE(mode, count, type, offset, instanced); + break; + case INSTANCE_IMPL_EXT: + glDrawElementsInstancedEXT(mode, count, type, offset, instanced); + break; + default: + throw new UnsupportedOperationException(); + } } public static final IUniformGL _wglGetUniformLocation(IProgramGL obj, String name) { @@ -533,11 +733,79 @@ public class PlatformOpenGL { return glGetError(); } - public static final boolean checkHDRFramebufferSupport(int bits) { - return true; + public static final int checkOpenGLESVersion() { + return glesVers; } + public static final boolean checkEXTGPUShader5Capable() { + return hasEXTGPUShader5; + } + + public static final boolean checkOESGPUShader5Capable() { + return hasOESGPUShader5; + } + + public static final boolean checkFBORenderMipmapCapable() { + return hasOESFBORenderMipmap; + } + + public static final boolean checkVAOCapable() { + return vertexArrayImpl != VAO_IMPL_NONE; + } + + public static final boolean checkInstancingCapable() { + return instancingImpl != INSTANCE_IMPL_NONE; + } + + public static final boolean checkTexStorageCapable() { + return texStorageImpl != TEX_STORAGE_IMPL_NONE; + } + + public static final boolean checkTextureLODCapable() { + return glesVers >= 300 || hasEXTShaderTextureLOD; + } + + public static final boolean checkNPOTCapable() { + return glesVers >= 300; + } + + public static final boolean checkHDRFramebufferSupport(int bits) { + switch(bits) { + case 16: + return hasFBO16FSupport; + case 32: + return hasFBO32FSupport; + default: + return false; + } + } + + public static final boolean checkLinearHDRFilteringSupport(int bits) { + switch(bits) { + case 16: + return hasLinearHDR16FSupport; + case 32: + return hasLinearHDR32FSupport; + default: + return false; + } + } + + // legacy public static final boolean checkLinearHDR32FSupport() { return hasLinearHDR32FSupport; } + + public static final boolean checkAnisotropicFilteringSupport() { + return hasEXTTextureFilterAnisotropic; + } + + public static final String[] getAllExtensions() { + return glGetString(GL_EXTENSIONS).split(" "); + } + + public static final void enterVAOEmulationHook() { + + } + } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformRuntime.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformRuntime.java index ee6dcc8a..28b9bcb5 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformRuntime.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformRuntime.java @@ -13,7 +13,9 @@ import java.io.OutputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; +import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.Random; import java.util.function.Consumer; import java.util.zip.DeflaterOutputStream; @@ -31,6 +33,7 @@ import org.lwjgl.opengles.GLDebugMessageKHRCallback; import org.lwjgl.opengles.GLDebugMessageKHRCallbackI; import org.lwjgl.opengles.GLES; import org.lwjgl.opengles.GLES30; +import org.lwjgl.opengles.GLESCapabilities; import org.lwjgl.opengles.KHRDebug; import org.lwjgl.system.MemoryStack; import org.lwjgl.system.MemoryUtil; @@ -38,16 +41,19 @@ import org.lwjgl.system.jemalloc.JEmalloc; import net.lax1dude.eaglercraft.v1_8.internal.buffer.EaglerLWJGLAllocator; import net.lax1dude.eaglercraft.v1_8.EaglerOutputStream; +import net.lax1dude.eaglercraft.v1_8.Filesystem; import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer; import net.lax1dude.eaglercraft.v1_8.internal.buffer.IntBuffer; import net.lax1dude.eaglercraft.v1_8.internal.lwjgl.DesktopClientConfigAdapter; +import net.lax1dude.eaglercraft.v1_8.internal.vfs2.VFile2; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.minecraft.EaglerFolderResourcePack; +import net.lax1dude.eaglercraft.v1_8.sp.server.internal.ServerPlatformSingleplayer; /** - * Copyright (c) 2022-2023 lax1dude, ayunami2000. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -74,7 +80,22 @@ public class PlatformRuntime { public static void create() { logger.info("Starting Desktop Runtime..."); - PlatformFilesystem.initialize(); + + ByteBuffer endiannessTestBytes = allocateByteBuffer(4); + try { + endiannessTestBytes.asIntBuffer().put(0x6969420); + if (((endiannessTestBytes.get(0) & 0xFF) | ((endiannessTestBytes.get(1) & 0xFF) << 8) + | ((endiannessTestBytes.get(2) & 0xFF) << 16) | ((endiannessTestBytes.get(3) & 0xFF) << 24)) != 0x6969420) { + throw new UnsupportedOperationException("Big endian CPU detected! (somehow)"); + }else { + logger.info("Endianness: this CPU is little endian"); + } + }finally { + freeByteBuffer(endiannessTestBytes); + } + + IEaglerFilesystem resourcePackFilesystem = Filesystem.getHandleFor(getClientConfigAdapter().getResourcePacksDB()); + VFile2.setPrimaryFilesystem(resourcePackFilesystem); EaglerFolderResourcePack.setSupported(true); if(requestedANGLEPlatform != EnumPlatformANGLE.DEFAULT) { @@ -91,23 +112,54 @@ public class PlatformRuntime { glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); glfwWindowHint(GLFW_CENTER_CURSOR, GLFW_TRUE); glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE); - PointerBuffer buf = glfwGetMonitors(); GLFWVidMode mon = glfwGetVideoMode(buf.get(0)); int windowWidth = mon.width() - 200; int windowHeight = mon.height() - 250; + String title = "Eaglercraft Desktop Runtime"; int winX = (mon.width() - windowWidth) / 2; int winY = (mon.height() - windowHeight - 20) / 2; - windowHandle = glfwCreateWindow(windowWidth, windowHeight, "Eaglercraft Desktop Runtime", 0l, 0l); + int myGLVersion = -1; + if(requestedGLVersion >= 310 && windowHandle == 0) { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); + windowHandle = glfwCreateWindow(windowWidth, windowHeight, title, 0l, 0l); + if(windowHandle == 0l) { + logger.error("Failed to create OpenGL ES 3.1 context!"); + }else { + myGLVersion = 310; + } + } + if(requestedGLVersion >= 300 && windowHandle == 0) { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + windowHandle = glfwCreateWindow(windowWidth, windowHeight, title, 0l, 0l); + if(windowHandle == 0l) { + logger.error("Failed to create OpenGL ES 3.0 context!"); + }else { + myGLVersion = 300; + } + } + if(requestedGLVersion >= 200 && windowHandle == 0) { + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + windowHandle = glfwCreateWindow(windowWidth, windowHeight, title, 0l, 0l); + if(windowHandle == 0l) { + logger.error("Failed to create OpenGL ES 2.0 context!"); + }else { + myGLVersion = 200; + } + } + if(myGLVersion == -1) { + throw new RuntimeException("Could not create a supported OpenGL ES context!"); + } glfwSetWindowPos(windowHandle, winX, winY); @@ -171,13 +223,28 @@ public class PlatformRuntime { EGL.createDisplayCapabilities(glfw_eglHandle, major[0], minor[0]); glfwMakeContextCurrent(windowHandle); - PlatformOpenGL.setCurrentContext(GLES.createCapabilities()); + GLESCapabilities caps = GLES.createCapabilities(); + PlatformOpenGL.setCurrentContext(myGLVersion, caps); logger.info("OpenGL Version: {}", (glVersion = GLES30.glGetString(GLES30.GL_VERSION))); logger.info("OpenGL Renderer: {}", (glRenderer = GLES30.glGetString(GLES30.GL_RENDERER))); - rendererANGLEPlatform = EnumPlatformANGLE.fromGLRendererString(glRenderer); + int realGLVersion = (glVersion != null && probablyGLES2(glVersion)) ? 200 + : (GLES30.glGetInteger(GLES30.GL_MAJOR_VERSION) * 100 + + GLES30.glGetInteger(GLES30.GL_MINOR_VERSION) * 10); + if(realGLVersion != myGLVersion) { + logger.warn("Unexpected GLES verison resolved for requested {} context: {}", myGLVersion, realGLVersion); + if(myGLVersion == 200) { + logger.warn("Note: try adding the \"d3d9\" option if you are on windows trying to get GLES 2.0"); + } + if(realGLVersion != 320 && realGLVersion != 310 && realGLVersion != 300 && realGLVersion != 200) { + throw new RuntimeException("Unsupported OpenGL ES version detected: " + realGLVersion); + } + myGLVersion = realGLVersion; + PlatformOpenGL.setCurrentContext(myGLVersion, caps); + } + if(requestedANGLEPlatform != EnumPlatformANGLE.DEFAULT && rendererANGLEPlatform != requestedANGLEPlatform) { logger.warn("Incorrect ANGLE Platform: {}", rendererANGLEPlatform.name); @@ -187,6 +254,18 @@ public class PlatformRuntime { logger.info("ANGLE Platform: {}", rendererANGLEPlatform.name); } + List exts = PlatformOpenGL.dumpActiveExtensions(); + if(exts.isEmpty()) { + logger.info("Unlocked the following OpenGL ES extensions: (NONE)"); + }else { + Collections.sort(exts); + logger.info("Unlocked the following OpenGL ES extensions:"); + for(int i = 0, l = exts.size(); i < l; ++i) { + logger.info(" - " + exts.get(i)); + } + } + + glfwSwapInterval(0); KHRDebug.glDebugMessageCallbackKHR(new GLDebugMessageKHRCallbackI() { @@ -245,13 +324,20 @@ public class PlatformRuntime { public static void destroy() { PlatformAudio.platformShutdown(); - PlatformFilesystem.platformShutdown(); + Filesystem.closeAllHandles(); + ServerPlatformSingleplayer.platformShutdown(); GLES.destroy(); EGL.destroy(); glfwDestroyWindow(windowHandle); glfwTerminate(); } + private static boolean probablyGLES2(String glVersion) { + if(glVersion == null) return false; + glVersion = glVersion.toLowerCase(); + return glVersion.contains("opengl es 2.0") || glVersion.contains("ES 2.0"); + } + public static EnumPlatformType getPlatformType() { return EnumPlatformType.DESKTOP; } @@ -274,11 +360,16 @@ public class PlatformRuntime { } private static EnumPlatformANGLE requestedANGLEPlatform = EnumPlatformANGLE.DEFAULT; + private static int requestedGLVersion = 300; public static void requestANGLE(EnumPlatformANGLE plaf) { requestedANGLEPlatform = plaf; } + public static void requestGL(int i) { + requestedGLVersion = i; + } + public static EnumPlatformANGLE getPlatformANGLE() { return rendererANGLEPlatform; } @@ -499,18 +590,6 @@ public class PlatformRuntime { return DesktopClientConfigAdapter.instance; } - public static String getRecText() { - return "recording.unsupported"; - } - - public static boolean recSupported() { - return false; - } - - public static void toggleRec() { - // - } - private static final Random seedProvider = new Random(); public static long randomSeed() { @@ -530,4 +609,25 @@ public class PlatformRuntime { public static long getWindowHandle() { return windowHandle; } + + public static long steadyTimeMillis() { + return System.nanoTime() / 1000000l; + } + + public static long nanoTime() { + return System.nanoTime(); + } + + public static void postCreate() { + + } + + public static void setDisplayBootMenuNextRefresh(boolean en) { + + } + + public static void immediateContinue() { + // nope + } + } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformScreenRecord.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformScreenRecord.java new file mode 100644 index 00000000..1568d60c --- /dev/null +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformScreenRecord.java @@ -0,0 +1,59 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +import net.lax1dude.eaglercraft.v1_8.recording.EnumScreenRecordingCodec; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class PlatformScreenRecord { + + public static boolean isSupported() { + return false; + } + + public static boolean isCodecSupported(EnumScreenRecordingCodec codec) { + return false; + } + + public static void setGameVolume(float volume) { + + } + + public static void setMicrophoneVolume(float volume) { + + } + + public static void startRecording(ScreenRecordParameters params) { + + } + + public static void endRecording() { + + } + + public static boolean isRecording() { + return false; + } + + public static boolean isMicVolumeLocked() { + return false; + } + + public static boolean isVSyncLocked() { + return false; + } + + +} diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformUpdateSvc.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformUpdateSvc.java index cd3cc71a..ae4529f6 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformUpdateSvc.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformUpdateSvc.java @@ -2,6 +2,7 @@ package net.lax1dude.eaglercraft.v1_8.internal; import net.lax1dude.eaglercraft.v1_8.update.UpdateCertificate; import net.lax1dude.eaglercraft.v1_8.update.UpdateProgressStruct; +import net.lax1dude.eaglercraft.v1_8.update.UpdateResultObj; /** * Copyright (c) 2024 lax1dude. All Rights Reserved. @@ -46,6 +47,15 @@ public class PlatformUpdateSvc { return dummyStruct; } + public static UpdateResultObj getUpdateResult() { + return null; + } + + public static void installSignedClient(UpdateCertificate clientCert, byte[] clientPayload, boolean setDefault, + boolean setTimeout) { + + } + public static void quine(String filename, byte[] cert, byte[] data, String date) { } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformWebRTC.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformWebRTC.java index 9b7eef75..cfea7c39 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformWebRTC.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformWebRTC.java @@ -4,20 +4,22 @@ import dev.onvoid.webrtc.*; import dev.onvoid.webrtc.internal.NativeLoader; import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EagUtils; -import net.lax1dude.eaglercraft.v1_8.EaglerInputStream; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.sp.lan.LANPeerEvent; -import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayManager; +import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayLoggerImpl; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayQuery; +import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayQueryImpl; +import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayQueryRateLimitDummy; +import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayServerRateLimitTracker; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayServerSocket; +import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayServerSocketImpl; +import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayServerSocketRateLimitDummy; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayWorldsQuery; -import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.*; -import net.lax1dude.eaglercraft.v1_8.update.UpdateService; +import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayWorldsQueryImpl; +import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayWorldsQueryRateLimitDummy; import org.apache.commons.lang3.SystemUtils; -import org.java_websocket.client.WebSocketClient; -import org.java_websocket.handshake.ServerHandshake; import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONWriter; @@ -25,10 +27,7 @@ import org.json.JSONWriter; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; -import java.io.DataInputStream; -import java.io.IOException; import java.lang.reflect.Field; -import java.net.URI; import java.nio.ByteBuffer; import java.nio.file.Paths; import java.util.*; @@ -52,10 +51,25 @@ public class PlatformWebRTC { private static final Logger logger = LogManager.getLogger("PlatformWebRTC"); + private static final RelayLoggerImpl loggerImpl = new RelayLoggerImpl(LogManager.getLogger("RelayPacket")); + private static final Object lock1 = new Object(); private static final Object lock2 = new Object(); private static final Object lock3 = new Object(); - private static final Object lock4 = new Object(); + + private static final List scheduledRunnables = new LinkedList<>(); + + private static class ScheduledRunnable { + + private final long runAt; + private final Runnable runnable; + + private ScheduledRunnable(long runAt, Runnable runnable) { + this.runAt = runAt; + this.runnable = runnable; + } + + } public static PeerConnectionFactory pcFactory; @@ -72,7 +86,8 @@ public class PlatformWebRTC { Runtime.getRuntime().addShutdownHook(new Thread(() -> pcFactory.dispose())); supported = true; } catch (Exception e) { - EagRuntime.debugPrintStackTrace(e); + logger.error("Failed to load WebRTC native library!"); + logger.error(e); supported = false; } } @@ -81,6 +96,46 @@ public class PlatformWebRTC { private static final Map fuckTeaVM = new HashMap<>(); + private static final Comparator sortTasks = (r1, r2) -> { + return (int)(r1.runAt - r2.runAt); + }; + + public static void runScheduledTasks() { + List toRun = null; + synchronized(scheduledRunnables) { + if(scheduledRunnables.isEmpty()) return; + long millis = PlatformRuntime.steadyTimeMillis(); + Iterator itr = scheduledRunnables.iterator(); + while(itr.hasNext()) { + ScheduledRunnable r = itr.next(); + if(r.runAt < millis) { + itr.remove(); + if(toRun == null) { + toRun = new ArrayList<>(1); + } + toRun.add(r); + } + } + } + if(toRun != null) { + Collections.sort(toRun, sortTasks); + for(int i = 0, l = toRun.size(); i < l; ++i) { + try { + toRun.get(i).runnable.run(); + }catch(Throwable t) { + logger.error("Caught exception running scheduled WebRTC task!"); + logger.error(t); + } + } + } + } + + static void scheduleTask(long runAfter, Runnable runnable) { + synchronized(scheduledRunnables) { + scheduledRunnables.add(new ScheduledRunnable(PlatformRuntime.steadyTimeMillis() + runAfter, runnable)); + } + } + public static class LANClient { public static final byte READYSTATE_INIT_FAILED = -2; public static final byte READYSTATE_FAILED = -1; @@ -123,15 +178,14 @@ public class PlatformWebRTC { synchronized (lock1) { if (iceCandidate.sdp != null && !iceCandidate.sdp.isEmpty()) { if (iceCandidates.isEmpty()) { - new Thread(() -> { - EagUtils.sleep(3000); + scheduleTask(3000l, () -> { synchronized (lock1) { if (peerConnection != null && peerConnection.getConnectionState() != RTCPeerConnectionState.DISCONNECTED) { clientICECandidate = JSONWriter.valueToString(iceCandidates); iceCandidates.clear(); } } - }).start(); + }); } Map m = new HashMap<>(); m.put("sdpMLineIndex", "" + iceCandidate.sdpMLineIndex); @@ -203,7 +257,7 @@ public class PlatformWebRTC { @Override public void onStateChange() { if (dataChannel != null && dataChannel.getState() == RTCDataChannelState.OPEN) { - new Thread(() -> { + scheduleTask(-1l, () -> { while (true) { synchronized (lock1) { if (iceCandidates.isEmpty()) { @@ -216,7 +270,7 @@ public class PlatformWebRTC { clientDataChannelClosed = false; clientDataChannelOpen = true; } - }).start(); + }); } } @@ -486,8 +540,7 @@ public class PlatformWebRTC { synchronized (lock3) { if (iceCandidate.sdp != null && !iceCandidate.sdp.isEmpty()) { if (iceCandidates.isEmpty()) { - new Thread(() -> { - EagUtils.sleep(3000); + scheduleTask(3000l, () -> { synchronized (lock3) { if (peerConnection[0] != null && peerConnection[0].getConnectionState() != RTCPeerConnectionState.DISCONNECTED) { LANPeerEvent.LANPeerICECandidateEvent e = new LANPeerEvent.LANPeerICECandidateEvent(peerId, JSONWriter.valueToString(iceCandidates)); @@ -497,7 +550,7 @@ public class PlatformWebRTC { iceCandidates.clear(); } } - }).start(); + }); } Map m = new HashMap<>(); m.put("sdpMLineIndex", "" + iceCandidate.sdpMLineIndex); @@ -509,7 +562,7 @@ public class PlatformWebRTC { @Override public void onDataChannel(RTCDataChannel dataChannel) { - new Thread(() -> { + scheduleTask(-1l, () -> { while (true) { synchronized (lock3) { if (iceCandidates.isEmpty()) { @@ -547,7 +600,7 @@ public class PlatformWebRTC { } } }); - }).start(); + }); } @Override @@ -625,795 +678,27 @@ public class PlatformWebRTC { return peerList.size(); } } - - private static final Map relayQueryLimited = new HashMap<>(); - private static final Map relayQueryBlocked = new HashMap<>(); - - private static class RelayQueryImpl implements RelayQuery { - - private final WebSocketClient sock; - private final String uri; - - private boolean open; - private boolean failed; - - private boolean hasRecievedAnyData = false; - - private int vers = -1; - private String comment = ""; - private String brand = ""; - - private long connectionOpenedAt; - private long connectionPingStart = -1; - private long connectionPingTimer = -1; - - private RateLimit rateLimitStatus = RateLimit.NONE; - - private VersionMismatch versError = VersionMismatch.UNKNOWN; - - private RelayQueryImpl(String uri) { - this.uri = uri; - WebSocketClient s; - try { - connectionOpenedAt = System.currentTimeMillis(); - s = new WebSocketClient(new URI(uri)) { - @Override - public void onOpen(ServerHandshake serverHandshake) { - try { - connectionPingStart = System.currentTimeMillis(); - sock.send(IPacket.writePacket(new IPacket00Handshake(0x03, RelayManager.preferredRelayVersion, ""))); - } catch (IOException e) { - logger.error(e.toString()); - sock.close(); - failed = true; - } - } - - @Override - public void onMessage(String s) { - // - } - - @Override - public void onMessage(ByteBuffer bb) { - if(bb != null) { - hasRecievedAnyData = true; - byte[] arr = new byte[bb.remaining()]; - bb.get(arr); - if(arr.length == 2 && arr[0] == (byte)0xFC) { - long millis = System.currentTimeMillis(); - if(arr[1] == (byte)0x00 || arr[1] == (byte)0x01) { - rateLimitStatus = RateLimit.BLOCKED; - relayQueryLimited.put(RelayQueryImpl.this.uri, millis); - }else if(arr[1] == (byte)0x02) { - rateLimitStatus = RateLimit.NOW_LOCKED; - relayQueryLimited.put(RelayQueryImpl.this.uri, millis); - relayQueryBlocked.put(RelayQueryImpl.this.uri, millis); - }else { - rateLimitStatus = RateLimit.LOCKED; - relayQueryBlocked.put(RelayQueryImpl.this.uri, millis); - } - failed = true; - open = false; - sock.close(); - }else { - if(open) { - try { - IPacket pkt = IPacket.readPacket(new DataInputStream(new EaglerInputStream(arr))); - if(pkt instanceof IPacket69Pong) { - IPacket69Pong ipkt = (IPacket69Pong)pkt; - versError = VersionMismatch.COMPATIBLE; - if(connectionPingTimer == -1) { - connectionPingTimer = System.currentTimeMillis() - connectionPingStart; - } - vers = ipkt.protcolVersion; - comment = ipkt.comment; - brand = ipkt.brand; - open = false; - failed = false; - sock.close(); - }else if(pkt instanceof IPacket70SpecialUpdate) { - IPacket70SpecialUpdate ipkt = (IPacket70SpecialUpdate)pkt; - if(ipkt.operation == IPacket70SpecialUpdate.OPERATION_UPDATE_CERTIFICATE) { - UpdateService.addCertificateToSet(ipkt.updatePacket); - } - }else if(pkt instanceof IPacketFFErrorCode) { - IPacketFFErrorCode ipkt = (IPacketFFErrorCode)pkt; - if(ipkt.code == IPacketFFErrorCode.TYPE_PROTOCOL_VERSION) { - String s1 = ipkt.desc.toLowerCase(); - if(s1.contains("outdated client") || s1.contains("client outdated")) { - versError = VersionMismatch.CLIENT_OUTDATED; - }else if(s1.contains("outdated server") || s1.contains("server outdated") || - s1.contains("outdated relay") || s1.contains("server relay")) { - versError = VersionMismatch.RELAY_OUTDATED; - }else { - versError = VersionMismatch.UNKNOWN; - } - } - logger.error("{}\": Recieved query error code {}: {}", uri, ipkt.code, ipkt.desc); - open = false; - failed = true; - sock.close(); - }else { - throw new IOException("Unexpected packet '" + pkt.getClass().getSimpleName() + "'"); - } - } catch (IOException e) { - logger.error("Relay Query Error: {}", e.toString()); - EagRuntime.debugPrintStackTrace(e); - open = false; - failed = true; - sock.close(); - } - } - } - } - } - - @Override - public void onClose(int i, String s, boolean b) { - open = false; - if(!hasRecievedAnyData) { - failed = true; - Long l = relayQueryBlocked.get(uri); - if(l != null) { - if(System.currentTimeMillis() - l.longValue() < 400000l) { - rateLimitStatus = RateLimit.LOCKED; - return; - } - } - l = relayQueryLimited.get(uri); - if(l != null) { - if(System.currentTimeMillis() - l.longValue() < 900000l) { - rateLimitStatus = RateLimit.BLOCKED; - return; - } - } - } - } - - @Override - public void onError(Exception e) { - EagRuntime.debugPrintStackTrace(e); - } - }; - s.connect(); - open = true; - failed = false; - }catch(Throwable t) { - connectionOpenedAt = 0l; - sock = null; - open = false; - failed = true; - return; - } - sock = s; - } - - @Override - public boolean isQueryOpen() { - return open; - } - - @Override - public boolean isQueryFailed() { - return failed; - } - - @Override - public RateLimit isQueryRateLimit() { - return rateLimitStatus; - } - - @Override - public void close() { - if(sock != null && open) { - sock.close(); - } - open = false; - } - - @Override - public int getVersion() { - return vers; - } - - @Override - public String getComment() { - return comment; - } - - @Override - public String getBrand() { - return brand; - } - - @Override - public long getPing() { - return connectionPingTimer < 1 ? 1 : connectionPingTimer; - } - - @Override - public VersionMismatch getCompatible() { - return versError; - } - - } - - private static class RelayQueryRatelimitDummy implements RelayQuery { - - private final RateLimit type; - - private RelayQueryRatelimitDummy(RateLimit type) { - this.type = type; - } - - @Override - public boolean isQueryOpen() { - return false; - } - - @Override - public boolean isQueryFailed() { - return true; - } - - @Override - public RateLimit isQueryRateLimit() { - return type; - } - - @Override - public void close() { - } - - @Override - public int getVersion() { - return RelayManager.preferredRelayVersion; - } - - @Override - public String getComment() { - return "this query was rate limited"; - } - - @Override - public String getBrand() { - return "lax1dude"; - } - - @Override - public long getPing() { - return 0l; - } - - @Override - public VersionMismatch getCompatible() { - return VersionMismatch.COMPATIBLE; - } - - } - public static RelayQuery openRelayQuery(String addr) { - long millis = System.currentTimeMillis(); - - Long l = relayQueryBlocked.get(addr); - if(l != null && millis - l.longValue() < 60000l) { - return new RelayQueryRatelimitDummy(RelayQuery.RateLimit.LOCKED); + RelayQuery.RateLimit limit = RelayServerRateLimitTracker.isLimited(addr); + if(limit == RelayQuery.RateLimit.LOCKED || limit == RelayQuery.RateLimit.BLOCKED) { + return new RelayQueryRateLimitDummy(limit); } - - l = relayQueryLimited.get(addr); - if(l != null && millis - l.longValue() < 10000l) { - return new RelayQueryRatelimitDummy(RelayQuery.RateLimit.BLOCKED); - } - return new RelayQueryImpl(addr); } - private static class RelayWorldsQueryImpl implements RelayWorldsQuery { - - private final WebSocketClient sock; - private final String uri; - - private boolean open; - private boolean failed; - - private boolean hasRecievedAnyData = false; - private RelayQuery.RateLimit rateLimitStatus = RelayQuery.RateLimit.NONE; - - private RelayQuery.VersionMismatch versError = RelayQuery.VersionMismatch.UNKNOWN; - - private List worlds = null; - - private RelayWorldsQueryImpl(String uri) { - this.uri = uri; - WebSocketClient s; - try { - s = new WebSocketClient(new URI(uri)) { - @Override - public void onOpen(ServerHandshake serverHandshake) { - try { - sock.send(IPacket.writePacket(new IPacket00Handshake(0x04, RelayManager.preferredRelayVersion, ""))); - } catch (IOException e) { - logger.error(e.toString()); - sock.close(); - open = false; - failed = true; - } - } - - @Override - public void onMessage(String s) { - // - } - - @Override - public void onMessage(ByteBuffer bb) { - if(bb != null) { - hasRecievedAnyData = true; - byte[] arr = new byte[bb.remaining()]; - bb.get(arr); - if(arr.length == 2 && arr[0] == (byte)0xFC) { - long millis = System.currentTimeMillis(); - if(arr[1] == (byte)0x00 || arr[1] == (byte)0x01) { - rateLimitStatus = RelayQuery.RateLimit.BLOCKED; - relayQueryLimited.put(RelayWorldsQueryImpl.this.uri, millis); - }else if(arr[1] == (byte)0x02) { - rateLimitStatus = RelayQuery.RateLimit.NOW_LOCKED; - relayQueryLimited.put(RelayWorldsQueryImpl.this.uri, millis); - relayQueryBlocked.put(RelayWorldsQueryImpl.this.uri, millis); - }else { - rateLimitStatus = RelayQuery.RateLimit.LOCKED; - relayQueryBlocked.put(RelayWorldsQueryImpl.this.uri, millis); - } - open = false; - failed = true; - sock.close(); - }else { - if(open) { - try { - IPacket pkt = IPacket.readPacket(new DataInputStream(new EaglerInputStream(arr))); - if(pkt instanceof IPacket07LocalWorlds) { - worlds = ((IPacket07LocalWorlds)pkt).worldsList; - sock.close(); - open = false; - failed = false; - }else if(pkt instanceof IPacket70SpecialUpdate) { - IPacket70SpecialUpdate ipkt = (IPacket70SpecialUpdate)pkt; - if(ipkt.operation == IPacket70SpecialUpdate.OPERATION_UPDATE_CERTIFICATE) { - UpdateService.addCertificateToSet(ipkt.updatePacket); - } - }else if(pkt instanceof IPacketFFErrorCode) { - IPacketFFErrorCode ipkt = (IPacketFFErrorCode)pkt; - if(ipkt.code == IPacketFFErrorCode.TYPE_PROTOCOL_VERSION) { - String s1 = ipkt.desc.toLowerCase(); - if(s1.contains("outdated client") || s1.contains("client outdated")) { - versError = RelayQuery.VersionMismatch.CLIENT_OUTDATED; - }else if(s1.contains("outdated server") || s1.contains("server outdated") || - s1.contains("outdated relay") || s1.contains("server relay")) { - versError = RelayQuery.VersionMismatch.RELAY_OUTDATED; - }else { - versError = RelayQuery.VersionMismatch.UNKNOWN; - } - } - logger.error("{}: Recieved query error code {}: {}", uri, ipkt.code, ipkt.desc); - open = false; - failed = true; - sock.close(); - }else { - throw new IOException("Unexpected packet '" + pkt.getClass().getSimpleName() + "'"); - } - } catch (IOException e) { - logger.error("Relay World Query Error: {}", e.toString()); - EagRuntime.debugPrintStackTrace(e); - open = false; - failed = true; - sock.close(); - } - } - } - } - } - - @Override - public void onClose(int i, String s, boolean b) { - open = false; - if(!hasRecievedAnyData) { - failed = true; - Long l = relayQueryBlocked.get(uri); - if(l != null) { - if(System.currentTimeMillis() - l.longValue() < 400000l) { - rateLimitStatus = RelayQuery.RateLimit.LOCKED; - return; - } - } - l = relayQueryLimited.get(uri); - if(l != null) { - if(System.currentTimeMillis() - l.longValue() < 900000l) { - rateLimitStatus = RelayQuery.RateLimit.BLOCKED; - return; - } - } - } - } - - @Override - public void onError(Exception e) { - EagRuntime.debugPrintStackTrace(e); - } - }; - s.connect(); - open = true; - failed = false; - }catch(Throwable t) { - sock = null; - open = false; - failed = true; - return; - } - sock = s; - } - - @Override - public boolean isQueryOpen() { - return open; - } - - @Override - public boolean isQueryFailed() { - return failed; - } - - @Override - public RelayQuery.RateLimit isQueryRateLimit() { - return rateLimitStatus; - } - - @Override - public void close() { - if(open && sock != null) { - sock.close(); - } - open = false; - } - - @Override - public List getWorlds() { - return worlds; - } - - @Override - public RelayQuery.VersionMismatch getCompatible() { - return versError; - } - - } - - private static class RelayWorldsQueryRatelimitDummy implements RelayWorldsQuery { - - private final RelayQuery.RateLimit rateLimit; - - private RelayWorldsQueryRatelimitDummy(RelayQuery.RateLimit rateLimit) { - this.rateLimit = rateLimit; - } - - @Override - public boolean isQueryOpen() { - return false; - } - - @Override - public boolean isQueryFailed() { - return true; - } - - @Override - public RelayQuery.RateLimit isQueryRateLimit() { - return rateLimit; - } - - @Override - public void close() { - } - - @Override - public List getWorlds() { - return new ArrayList(0); - } - - @Override - public RelayQuery.VersionMismatch getCompatible() { - return RelayQuery.VersionMismatch.COMPATIBLE; - } - } - public static RelayWorldsQuery openRelayWorldsQuery(String addr) { - long millis = System.currentTimeMillis(); - - Long l = relayQueryBlocked.get(addr); - if(l != null && millis - l.longValue() < 60000l) { - return new RelayWorldsQueryRatelimitDummy(RelayQuery.RateLimit.LOCKED); + RelayQuery.RateLimit limit = RelayServerRateLimitTracker.isLimited(addr); + if(limit == RelayQuery.RateLimit.LOCKED || limit == RelayQuery.RateLimit.BLOCKED) { + return new RelayWorldsQueryRateLimitDummy(limit); } - - l = relayQueryLimited.get(addr); - if(l != null && millis - l.longValue() < 10000l) { - return new RelayWorldsQueryRatelimitDummy(RelayQuery.RateLimit.BLOCKED); - } - return new RelayWorldsQueryImpl(addr); } - private static class RelayServerSocketImpl implements RelayServerSocket { - - private final WebSocketClient sock; - private final String uri; - - private boolean open; - private boolean closed; - private boolean failed; - - private boolean hasRecievedAnyData; - - private final List exceptions = new LinkedList(); - private final List packets = new LinkedList(); - - private RelayServerSocketImpl(String uri, int timeout) { - this.uri = uri; - WebSocketClient s; - try { - s = new WebSocketClient(new URI(uri)) { - @Override - public void onOpen(ServerHandshake serverHandshake) { - synchronized (lock4) { - open = true; - } - } - - @Override - public void onMessage(String s) { - // - } - - @Override - public void onMessage(ByteBuffer bb) { - if(bb != null) { - hasRecievedAnyData = true; - try { - byte[] arr = new byte[bb.remaining()]; - bb.get(arr); - IPacket pkt = IPacket.readPacket(new DataInputStream(new EaglerInputStream(arr))); - if(pkt instanceof IPacket70SpecialUpdate) { - IPacket70SpecialUpdate ipkt = (IPacket70SpecialUpdate)pkt; - if(ipkt.operation == IPacket70SpecialUpdate.OPERATION_UPDATE_CERTIFICATE) { - UpdateService.addCertificateToSet(ipkt.updatePacket); - } - }else { - packets.add(pkt); - } - } catch (IOException e) { - exceptions.add(e); - logger.error("Relay Socket Error: {}", e.toString()); - EagRuntime.debugPrintStackTrace(e); - synchronized (lock4) { - open = false; - failed = true; - } - closed = true; - synchronized (lock4) { - sock.close(); - } - } - } - } - - @Override - public void onClose(int i, String s, boolean b) { - if (!hasRecievedAnyData) { - failed = true; - } - synchronized (lock4) { - open = false; - closed = true; - } - } - - @Override - public void onError(Exception e) { - EagRuntime.debugPrintStackTrace(e); - } - }; - s.connect(); - synchronized (lock4) { - open = false; - closed = false; - } - failed = false; - }catch(Throwable t) { - exceptions.add(t); - synchronized (lock4) { - sock = null; - open = false; - closed = true; - } - failed = true; - return; - } - synchronized (lock4) { - sock = s; - } - new Thread(() -> { - EagUtils.sleep(timeout); - synchronized (lock4) { - if (!open && !closed) { - closed = true; - sock.close(); - } - } - }).start(); - } - - @Override - public boolean isOpen() { - return open; - } - - @Override - public boolean isClosed() { - synchronized (lock4) { - return closed; - } - } - - @Override - public void close() { - if(open && sock != null) { - synchronized (lock4) { - sock.close(); - } - } - synchronized (lock4) { - open = false; - closed = true; - } - } - - @Override - public boolean isFailed() { - return failed; - } - - @Override - public Throwable getException() { - if(!exceptions.isEmpty()) { - return exceptions.remove(0); - }else { - return null; - } - } - - @Override - public void writePacket(IPacket pkt) { - try { - sock.send(IPacket.writePacket(pkt)); - } catch (Throwable e) { - logger.error("Relay connection error: {}", e.toString()); - EagRuntime.debugPrintStackTrace(e); - exceptions.add(e); - failed = true; - synchronized (lock4) { - open = false; - closed = true; - sock.close(); - } - } - } - - @Override - public IPacket readPacket() { - if(!packets.isEmpty()) { - return packets.remove(0); - }else { - return null; - } - } - - @Override - public IPacket nextPacket() { - if(!packets.isEmpty()) { - return packets.get(0); - }else { - return null; - } - } - - @Override - public RelayQuery.RateLimit getRatelimitHistory() { - if(relayQueryBlocked.containsKey(uri)) { - return RelayQuery.RateLimit.LOCKED; - } - if(relayQueryLimited.containsKey(uri)) { - return RelayQuery.RateLimit.BLOCKED; - } - return RelayQuery.RateLimit.NONE; - } - - @Override - public String getURI() { - return uri; - } - - } - - private static class RelayServerSocketRatelimitDummy implements RelayServerSocket { - - private final RelayQuery.RateLimit limit; - - private RelayServerSocketRatelimitDummy(RelayQuery.RateLimit limit) { - this.limit = limit; - } - - @Override - public boolean isOpen() { - return false; - } - - @Override - public boolean isClosed() { - return true; - } - - @Override - public void close() { - } - - @Override - public boolean isFailed() { - return true; - } - - @Override - public Throwable getException() { - return null; - } - - @Override - public void writePacket(IPacket pkt) { - } - - @Override - public IPacket readPacket() { - return null; - } - - @Override - public IPacket nextPacket() { - return null; - } - - @Override - public RelayQuery.RateLimit getRatelimitHistory() { - return limit; - } - - @Override - public String getURI() { - return ""; - } - - } - public static RelayServerSocket openRelayConnection(String addr, int timeout) { - long millis = System.currentTimeMillis(); - - Long l = relayQueryBlocked.get(addr); - if(l != null && millis - l.longValue() < 60000l) { - return new RelayServerSocketRatelimitDummy(RelayQuery.RateLimit.LOCKED); + RelayQuery.RateLimit limit = RelayServerRateLimitTracker.isLimited(addr); + if(limit == RelayQuery.RateLimit.LOCKED || limit == RelayQuery.RateLimit.BLOCKED) { + return new RelayServerSocketRateLimitDummy(limit); } - - l = relayQueryLimited.get(addr); - if(l != null && millis - l.longValue() < 10000l) { - return new RelayServerSocketRatelimitDummy(RelayQuery.RateLimit.BLOCKED); - } - return new RelayServerSocketImpl(addr, timeout); } @@ -1453,7 +738,7 @@ public class PlatformWebRTC { public static List clientLANReadAllPacket() { synchronized(clientLANPacketBuffer) { if(!clientLANPacketBuffer.isEmpty()) { - List ret = new ArrayList(clientLANPacketBuffer); + List ret = new ArrayList<>(clientLANPacketBuffer); clientLANPacketBuffer.clear(); return ret; }else { diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformWebView.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformWebView.java new file mode 100644 index 00000000..650c9451 --- /dev/null +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformWebView.java @@ -0,0 +1,93 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +import net.lax1dude.eaglercraft.v1_8.internal.lwjgl.FallbackWebViewServer; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketWebViewMessageV4EAG; +import net.lax1dude.eaglercraft.v1_8.webview.WebViewOverlayController.IPacketSendCallback; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class PlatformWebView { + + private static FallbackWebViewServer fallbackServer = null; + private static IPacketSendCallback packetCallback = null; + + public static boolean supported() { + return false; + } + + public static boolean isShowing() { + return false; + } + + public static void beginShowing(WebViewOptions options, int x, int y, int w, int h) { + + } + + public static void resize(int x, int y, int w, int h) { + + } + + public static void endShowing() { + + } + + public static boolean fallbackSupported() { + return true; + } + + public static void launchFallback(WebViewOptions options) { + fallbackServer = new FallbackWebViewServer(options); + fallbackServer.setPacketSendCallback(packetCallback); + fallbackServer.start(); + } + + public static boolean fallbackRunning() { + return fallbackServer != null && !fallbackServer.isDead(); + } + + public static String getFallbackURL() { + return fallbackServer != null ? fallbackServer.getURL() : null; + } + + public static void endFallbackServer() { + if(fallbackServer != null && !fallbackServer.isDead()) { + fallbackServer.killServer(); + } + } + + public static void handleMessageFromServer(SPacketWebViewMessageV4EAG packet) { + if(fallbackServer != null && !fallbackServer.isDead()) { + fallbackServer.handleMessageFromServer(packet); + } + } + + public static void setPacketSendCallback(IPacketSendCallback callback) { + packetCallback = callback; + if(fallbackServer != null) { + fallbackServer.setPacketSendCallback(callback); + } + } + + public static void runTick() { + if(fallbackServer != null) { + fallbackServer.runTick(); + if(fallbackServer.isDead()) { + fallbackServer = null; + } + } + } + +} diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/WebSocketServerQuery.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/WebSocketServerQuery.java deleted file mode 100644 index 42feb107..00000000 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/WebSocketServerQuery.java +++ /dev/null @@ -1,173 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.internal; - -import java.net.URI; -import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.List; - -import org.java_websocket.client.WebSocketClient; -import org.java_websocket.drafts.Draft; -import org.java_websocket.drafts.Draft_6455; -import org.java_websocket.extensions.permessage_deflate.PerMessageDeflateExtension; -import org.java_websocket.handshake.ServerHandshake; -import org.json.JSONObject; - -import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; -import net.lax1dude.eaglercraft.v1_8.log4j.Logger; - -/** - * Copyright (c) 2022-2023 lax1dude, ayunami2000. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -class WebSocketServerQuery extends WebSocketClient implements IServerQuery { - - private static final Draft perMessageDeflateDraft = new Draft_6455(new PerMessageDeflateExtension()); - - public static final Logger logger = LogManager.getLogger("WebSocketQuery"); - - private final List queryResponses = new LinkedList(); - private final List queryResponsesBytes = new LinkedList(); - private final String type; - private boolean open = true; - private boolean alive = false; - private long pingStart = -1l; - private long pingTimer = -1l; - private EnumServerRateLimit rateLimit = EnumServerRateLimit.OK; - - WebSocketServerQuery(String type, URI serverUri) { - super(serverUri, perMessageDeflateDraft); - this.type = type; - this.setConnectionLostTimeout(5); - this.setTcpNoDelay(true); - this.connect(); - } - - @Override - public int responsesAvailable() { - synchronized(queryResponses) { - return queryResponses.size(); - } - } - - @Override - public QueryResponse getResponse() { - synchronized(queryResponses) { - if(queryResponses.size() > 0) { - return queryResponses.remove(0); - }else { - return null; - } - } - } - - @Override - public int binaryResponsesAvailable() { - synchronized(queryResponsesBytes) { - return queryResponsesBytes.size(); - } - } - - @Override - public byte[] getBinaryResponse() { - synchronized(queryResponsesBytes) { - if(queryResponsesBytes.size() > 0) { - return queryResponsesBytes.remove(0); - }else { - return null; - } - } - } - - @Override - public QueryReadyState readyState() { - return open ? (alive ? QueryReadyState.OPEN : QueryReadyState.CONNECTING) - : (alive ? QueryReadyState.CLOSED : QueryReadyState.FAILED); - } - - @Override - public EnumServerRateLimit getRateLimit() { - return rateLimit; - } - - @Override - public void onClose(int arg0, String arg1, boolean arg2) { - open = false; - } - - @Override - public void onError(Exception arg0) { - logger.error("Exception thrown by websocket query \"" + this.getURI().toString() + "\"!"); - logger.error(arg0); - } - - @Override - public void onMessage(String arg0) { - alive = true; - if(pingTimer == -1) { - pingTimer = System.currentTimeMillis() - pingStart; - if(pingTimer < 1) { - pingTimer = 1; - } - } - if(arg0.equalsIgnoreCase("BLOCKED")) { - logger.error("Reached full IP ratelimit for {}!", this.uri.toString()); - rateLimit = EnumServerRateLimit.BLOCKED; - return; - } - if(arg0.equalsIgnoreCase("LOCKED")) { - logger.error("Reached full IP ratelimit lockout for {}!", this.uri.toString()); - rateLimit = EnumServerRateLimit.LOCKED_OUT; - return; - } - try { - JSONObject obj = new JSONObject(arg0); - if("blocked".equalsIgnoreCase(obj.optString("type", null))) { - logger.error("Reached query ratelimit for {}!", this.uri.toString()); - rateLimit = EnumServerRateLimit.BLOCKED; - }else if("locked".equalsIgnoreCase(obj.optString("type", null))) { - logger.error("Reached query ratelimit lockout for {}!", this.uri.toString()); - rateLimit = EnumServerRateLimit.LOCKED_OUT; - }else { - QueryResponse response = new QueryResponse(obj, pingTimer); - synchronized(queryResponses) { - queryResponses.add(response); - } - } - }catch(Throwable t) { - logger.error("Exception thrown parsing websocket query response from \"" + this.getURI().toString() + "\"!"); - logger.error(t); - } - } - - @Override - public void onMessage(ByteBuffer arg0) { - alive = true; - if(pingTimer == -1) { - pingTimer = System.currentTimeMillis() - pingStart; - if(pingTimer < 1) { - pingTimer = 1; - } - } - synchronized(queryResponsesBytes) { - queryResponsesBytes.add(arg0.array()); - } - } - - @Override - public void onOpen(ServerHandshake arg0) { - pingStart = System.currentTimeMillis(); - send("Accept: " + type); - } - -} diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLByteBuffer.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLByteBuffer.java index 5cb11422..74902975 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLByteBuffer.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLByteBuffer.java @@ -1,7 +1,5 @@ package net.lax1dude.eaglercraft.v1_8.internal.buffer; -import org.lwjgl.system.jemalloc.JEmalloc; - import net.lax1dude.unsafememcpy.UnsafeMemcpy; import net.lax1dude.unsafememcpy.UnsafeUtils; @@ -29,7 +27,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { private int position; private int limit; private int mark; - + EaglerLWJGLByteBuffer(long address, int capacity, boolean original) { this(address, capacity, 0, capacity, -1, original); } @@ -68,18 +66,13 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { return position < limit; } - @Override - public boolean isReadOnly() { - return false; - } - @Override public boolean hasArray() { return false; } @Override - public Object array() { + public byte[] array() { throw new UnsupportedOperationException(); } @@ -88,50 +81,40 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { return true; } - @Override - public ByteBuffer slice() { - return new EaglerLWJGLByteBuffer(address + position, limit - position, false); - } - @Override public ByteBuffer duplicate() { return new EaglerLWJGLByteBuffer(address, capacity, position, limit, mark, false); } - @Override - public ByteBuffer asReadOnlyBuffer() { - return new EaglerLWJGLByteBuffer(address, capacity, position, limit, mark, false); - } - @Override public byte get() { - if(position >= limit) throw new ArrayIndexOutOfBoundsException(position); + if(position >= limit) throw Buffer.makeIOOBE(position); return UnsafeUtils.getMemByte(address + position++); } @Override public ByteBuffer put(byte b) { - if(position >= limit) throw new ArrayIndexOutOfBoundsException(position); + if(position >= limit) throw Buffer.makeIOOBE(position); UnsafeUtils.setMemByte(address + position++, b); return this; } @Override public byte get(int index) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemByte(address + index); } @Override public ByteBuffer put(int index, byte b) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemByte(address + index, b); return this; } @Override public ByteBuffer get(byte[] dst, int offset, int length) { - if(position + length > limit) throw new ArrayIndexOutOfBoundsException(position + length - 1); + if(position + length > limit) throw Buffer.makeIOOBE(position + length - 1); UnsafeMemcpy.memcpy(dst, offset, address + position, length); position += length; return this; @@ -139,7 +122,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public ByteBuffer get(byte[] dst) { - if(position + dst.length > limit) throw new ArrayIndexOutOfBoundsException(position + dst.length - 1); + if(position + dst.length > limit) throw Buffer.makeIOOBE(position + dst.length - 1); UnsafeMemcpy.memcpy(dst, 0, address + position, dst.length); position += dst.length; return this; @@ -150,13 +133,13 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { if(src instanceof EaglerLWJGLByteBuffer) { EaglerLWJGLByteBuffer c = (EaglerLWJGLByteBuffer)src; int l = c.limit - c.position; - if(position + l > limit) throw new ArrayIndexOutOfBoundsException(position + l - 1); + if(position + l > limit) throw Buffer.makeIOOBE(position + l - 1); UnsafeMemcpy.memcpy(address + position, c.address + c.position, l); position += l; c.position += l; }else { int l = src.remaining(); - if(position + l > limit) throw new ArrayIndexOutOfBoundsException(position + l - 1); + if(position + l > limit) throw Buffer.makeIOOBE(position + l - 1); for(int i = 0; i < l; ++i) { UnsafeUtils.setMemByte(address + position + l, src.get()); } @@ -167,7 +150,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public ByteBuffer put(byte[] src, int offset, int length) { - if(position + length > limit) throw new ArrayIndexOutOfBoundsException(position + length - 1); + if(position + length > limit) throw Buffer.makeIOOBE(position + length - 1); UnsafeMemcpy.memcpy(address + position, src, offset, length); position += length; return this; @@ -175,39 +158,15 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public ByteBuffer put(byte[] src) { - if(position + src.length > limit) throw new ArrayIndexOutOfBoundsException(position + src.length - 1); + if(position + src.length > limit) throw Buffer.makeIOOBE(position + src.length - 1); UnsafeMemcpy.memcpy(address + position, src, 0, src.length); position += src.length; return this; } - @Override - public int arrayOffset() { - return position; - } - - @Override - public ByteBuffer compact() { - if(limit > capacity) throw new ArrayIndexOutOfBoundsException(limit); - if(position > limit) throw new ArrayIndexOutOfBoundsException(position); - - if(position == limit) { - return new EaglerLWJGLByteBuffer(0l, 0, false); - } - - int newLen = limit - position; - long newAlloc = JEmalloc.nje_malloc(newLen); - if(newAlloc == 0l) { - throw new OutOfMemoryError("Native je_malloc call returned null pointer!"); - } - UnsafeMemcpy.memcpy(newAlloc, address + position, newLen); - - return new EaglerLWJGLByteBuffer(newAlloc, newLen, true); - } - @Override public char getChar() { - if(position + 2 > limit) throw new ArrayIndexOutOfBoundsException(position); + if(position + 2 > limit) throw Buffer.makeIOOBE(position); char c = UnsafeUtils.getMemChar(address + position); position += 2; return c; @@ -215,7 +174,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public ByteBuffer putChar(char value) { - if(position + 2 > limit) throw new ArrayIndexOutOfBoundsException(position); + if(position + 2 > limit) throw Buffer.makeIOOBE(position); UnsafeUtils.setMemChar(address + position, value); position += 2; return this; @@ -223,20 +182,20 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public char getChar(int index) { - if(index + 2 > limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index + 2 > limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemChar(address + index); } @Override public ByteBuffer putChar(int index, char value) { - if(index + 2 > limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index + 2 > limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemChar(address + index, value); return this; } @Override public short getShort() { - if(position + 2 > limit) throw new ArrayIndexOutOfBoundsException(position); + if(position + 2 > limit) throw Buffer.makeIOOBE(position); short s = UnsafeUtils.getMemShort(address + position); position += 2; return s; @@ -244,7 +203,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public ByteBuffer putShort(short value) { - if(position + 2 > limit) throw new ArrayIndexOutOfBoundsException(position); + if(position + 2 > limit) throw Buffer.makeIOOBE(position); UnsafeUtils.setMemShort(address + position, value); position += 2; return this; @@ -252,13 +211,13 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public short getShort(int index) { - if(index + 2 > limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index + 2 > limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemShort(address + index); } @Override public ByteBuffer putShort(int index, short value) { - if(index + 2 > limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index + 2 > limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemShort(address + index, value); return this; } @@ -270,7 +229,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public int getInt() { - if(position + 4 > limit) throw new ArrayIndexOutOfBoundsException(position); + if(position + 4 > limit) throw Buffer.makeIOOBE(position); int i = UnsafeUtils.getMemInt(address + position); position += 4; return i; @@ -278,7 +237,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public ByteBuffer putInt(int value) { - if(position + 4 > limit) throw new ArrayIndexOutOfBoundsException(position); + if(position + 4 > limit) throw Buffer.makeIOOBE(position); UnsafeUtils.setMemInt(address + position, value); position += 4; return this; @@ -286,13 +245,13 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public int getInt(int index) { - if(index + 4 > limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index + 4 > limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemInt(address + index); } @Override public ByteBuffer putInt(int index, int value) { - if(index + 4 > limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index + 4 > limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemInt(address + index, value); return this; } @@ -304,7 +263,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public long getLong() { - if(position + 8 > limit) throw new ArrayIndexOutOfBoundsException(position); + if(position + 8 > limit) throw Buffer.makeIOOBE(position); long l = UnsafeUtils.getMemLong(address + position); position += 8; return l; @@ -312,7 +271,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public ByteBuffer putLong(long value) { - if(position + 8 > limit) throw new ArrayIndexOutOfBoundsException(position); + if(position + 8 > limit) throw Buffer.makeIOOBE(position); UnsafeUtils.setMemLong(address + position, value); position += 8; return this; @@ -320,20 +279,20 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public long getLong(int index) { - if(index + 8 > limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index + 8 > limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemLong(address + index); } @Override public ByteBuffer putLong(int index, long value) { - if(index + 8 > limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index + 8 > limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemLong(address + index, value); return this; } @Override public float getFloat() { - if(position + 4 > limit) throw new ArrayIndexOutOfBoundsException(position); + if(position + 4 > limit) throw Buffer.makeIOOBE(position); float f = UnsafeUtils.getMemFloat(address + position); position += 4; return f; @@ -341,7 +300,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public ByteBuffer putFloat(float value) { - if(position + 4 > limit) throw new ArrayIndexOutOfBoundsException(position); + if(position + 4 > limit) throw Buffer.makeIOOBE(position); UnsafeUtils.setMemFloat(address + position, value); position += 4; return this; @@ -349,13 +308,13 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public float getFloat(int index) { - if(index + 4 > limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index + 4 > limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemFloat(address + index); } @Override public ByteBuffer putFloat(int index, float value) { - if(index + 4 > limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index + 4 > limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemFloat(address + index, value); return this; } @@ -374,7 +333,7 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public ByteBuffer reset() { int m = mark; - if(m < 0) throw new ArrayIndexOutOfBoundsException("Invalid mark: " + m); + if(m < 0) throw new IndexOutOfBoundsException("Invalid mark: " + m); position = m; return this; } @@ -404,14 +363,14 @@ public class EaglerLWJGLByteBuffer implements ByteBuffer { @Override public ByteBuffer limit(int newLimit) { - if(newLimit < 0 || newLimit > capacity) throw new ArrayIndexOutOfBoundsException(newLimit); + if(newLimit < 0 || newLimit > capacity) throw Buffer.makeIOOBE(newLimit); limit = newLimit; return this; } @Override public ByteBuffer position(int newPosition) { - if(newPosition < 0 || newPosition > limit) throw new ArrayIndexOutOfBoundsException(newPosition); + if(newPosition < 0 || newPosition > limit) throw Buffer.makeIOOBE(newPosition); position = newPosition; return this; } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLFloatBuffer.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLFloatBuffer.java index a163fe79..aa862fda 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLFloatBuffer.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLFloatBuffer.java @@ -1,7 +1,5 @@ package net.lax1dude.eaglercraft.v1_8.internal.buffer; -import org.lwjgl.system.jemalloc.JEmalloc; - import net.lax1dude.unsafememcpy.UnsafeMemcpy; import net.lax1dude.unsafememcpy.UnsafeUtils; @@ -29,9 +27,9 @@ public class EaglerLWJGLFloatBuffer implements FloatBuffer { private int position; private int limit; private int mark; - + private static final int SHIFT = 2; - + EaglerLWJGLFloatBuffer(long address, int capacity, boolean original) { this(address, capacity, 0, capacity, -1, original); } @@ -70,82 +68,62 @@ public class EaglerLWJGLFloatBuffer implements FloatBuffer { return position < limit; } - @Override - public boolean isReadOnly() { - return false; - } - @Override public boolean hasArray() { return false; } @Override - public Object array() { + public float[] array() { throw new UnsupportedOperationException(); } - @Override - public int arrayOffset() { - return position; - } - - @Override - public FloatBuffer slice() { - return new EaglerLWJGLFloatBuffer(address + (position << SHIFT), limit - position, false); - } - @Override public FloatBuffer duplicate() { return new EaglerLWJGLFloatBuffer(address, capacity, position, limit, mark, false); } - @Override - public FloatBuffer asReadOnlyBuffer() { - return new EaglerLWJGLFloatBuffer(address, capacity, position, limit, mark, false); - } - @Override public float get() { - if(position >= limit) throw new ArrayIndexOutOfBoundsException(position); + if(position >= limit) throw Buffer.makeIOOBE(position); return UnsafeUtils.getMemFloat(address + ((position++) << SHIFT)); } @Override public FloatBuffer put(float b) { - if(position >= limit) throw new ArrayIndexOutOfBoundsException(position); + if(position >= limit) throw Buffer.makeIOOBE(position); UnsafeUtils.setMemFloat(address + ((position++) << SHIFT), b); return this; } @Override public float get(int index) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemFloat(address + (index << SHIFT)); } @Override public FloatBuffer put(int index, float b) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemFloat(address + (index << SHIFT), b); return this; } @Override public float getElement(int index) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemFloat(address + (index << SHIFT)); } @Override public void putElement(int index, float value) { - if(position >= limit) throw new ArrayIndexOutOfBoundsException(position); - UnsafeUtils.setMemFloat(address + ((position++) << SHIFT), value); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); + UnsafeUtils.setMemFloat(address + (index << SHIFT), value); } @Override public FloatBuffer get(float[] dst, int offset, int length) { - if(position + length > limit) throw new ArrayIndexOutOfBoundsException(position + length - 1); + if(position + length > limit) throw Buffer.makeIOOBE(position + length - 1); UnsafeMemcpy.memcpyAlignDst(dst, offset << SHIFT, address + (position << SHIFT), length); position += length; return this; @@ -153,7 +131,7 @@ public class EaglerLWJGLFloatBuffer implements FloatBuffer { @Override public FloatBuffer get(float[] dst) { - if(position + dst.length > limit) throw new ArrayIndexOutOfBoundsException(position + dst.length - 1); + if(position + dst.length > limit) throw Buffer.makeIOOBE(position + dst.length - 1); UnsafeMemcpy.memcpyAlignDst(dst, 0, address + (position << SHIFT), dst.length); position += dst.length; return this; @@ -164,13 +142,13 @@ public class EaglerLWJGLFloatBuffer implements FloatBuffer { if(src instanceof EaglerLWJGLFloatBuffer) { EaglerLWJGLFloatBuffer c = (EaglerLWJGLFloatBuffer)src; int l = c.limit - c.position; - if(position + l > limit) throw new ArrayIndexOutOfBoundsException(position + l - 1); + if(position + l > limit) throw Buffer.makeIOOBE(position + l - 1); UnsafeMemcpy.memcpy(address + (position << SHIFT), c.address + (c.position << SHIFT), l << SHIFT); position += l; c.position += l; }else { int l = src.remaining(); - if(position + l > limit) throw new ArrayIndexOutOfBoundsException(position + l - 1); + if(position + l > limit) throw Buffer.makeIOOBE(position + l - 1); for(int i = 0; i < l; ++i) { UnsafeUtils.setMemFloat(address + ((position + l) << SHIFT), src.get()); } @@ -181,7 +159,7 @@ public class EaglerLWJGLFloatBuffer implements FloatBuffer { @Override public FloatBuffer put(float[] src, int offset, int length) { - if(position + length > limit) throw new ArrayIndexOutOfBoundsException(position + length - 1); + if(position + length > limit) throw Buffer.makeIOOBE(position + length - 1); UnsafeMemcpy.memcpyAlignSrc(address + (position << SHIFT), src, offset << SHIFT, length); position += length; return this; @@ -189,36 +167,12 @@ public class EaglerLWJGLFloatBuffer implements FloatBuffer { @Override public FloatBuffer put(float[] src) { - if(position + src.length > limit) throw new ArrayIndexOutOfBoundsException(position + src.length - 1); + if(position + src.length > limit) throw Buffer.makeIOOBE(position + src.length - 1); UnsafeMemcpy.memcpyAlignSrc(address + (position << SHIFT), src, 0, src.length); position += src.length; return this; } - @Override - public int getArrayOffset() { - return position; - } - - @Override - public FloatBuffer compact() { - if(limit > capacity) throw new ArrayIndexOutOfBoundsException(limit); - if(position > limit) throw new ArrayIndexOutOfBoundsException(position); - - if(position == limit) { - return new EaglerLWJGLFloatBuffer(0l, 0, false); - } - - int newLen = limit - position; - long newAlloc = JEmalloc.nje_malloc(newLen); - if(newAlloc == 0l) { - throw new OutOfMemoryError("Native je_malloc call returned null pointer!"); - } - UnsafeMemcpy.memcpy(newAlloc, address + (position << SHIFT), newLen << SHIFT); - - return new EaglerLWJGLFloatBuffer(newAlloc, newLen, true); - } - @Override public boolean isDirect() { return true; @@ -233,7 +187,7 @@ public class EaglerLWJGLFloatBuffer implements FloatBuffer { @Override public FloatBuffer reset() { int m = mark; - if(m < 0) throw new ArrayIndexOutOfBoundsException("Invalid mark: " + m); + if(m < 0) throw new IndexOutOfBoundsException("Invalid mark: " + m); position = m; return this; } @@ -263,14 +217,14 @@ public class EaglerLWJGLFloatBuffer implements FloatBuffer { @Override public FloatBuffer limit(int newLimit) { - if(newLimit < 0 || newLimit > capacity) throw new ArrayIndexOutOfBoundsException(newLimit); + if(newLimit < 0 || newLimit > capacity) throw Buffer.makeIOOBE(newLimit); limit = newLimit; return this; } @Override public FloatBuffer position(int newPosition) { - if(newPosition < 0 || newPosition > limit) throw new ArrayIndexOutOfBoundsException(newPosition); + if(newPosition < 0 || newPosition > limit) throw Buffer.makeIOOBE(newPosition); position = newPosition; return this; } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLIntBuffer.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLIntBuffer.java index bf306a2c..030c3a7b 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLIntBuffer.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLIntBuffer.java @@ -1,7 +1,5 @@ package net.lax1dude.eaglercraft.v1_8.internal.buffer; -import org.lwjgl.system.jemalloc.JEmalloc; - import net.lax1dude.unsafememcpy.UnsafeMemcpy; import net.lax1dude.unsafememcpy.UnsafeUtils; @@ -21,7 +19,7 @@ import net.lax1dude.unsafememcpy.UnsafeUtils; * */ public class EaglerLWJGLIntBuffer implements IntBuffer { - + final long address; final boolean original; @@ -29,9 +27,9 @@ public class EaglerLWJGLIntBuffer implements IntBuffer { private int position; private int limit; private int mark; - + private static final int SHIFT = 2; - + EaglerLWJGLIntBuffer(long address, int capacity, boolean original) { this(address, capacity, 0, capacity, -1, original); } @@ -44,7 +42,7 @@ public class EaglerLWJGLIntBuffer implements IntBuffer { this.mark = mark; this.original = original; } - + @Override public int capacity() { return capacity; @@ -70,82 +68,62 @@ public class EaglerLWJGLIntBuffer implements IntBuffer { return position < limit; } - @Override - public boolean isReadOnly() { - return false; - } - @Override public boolean hasArray() { return false; } @Override - public Object array() { + public int[] array() { throw new UnsupportedOperationException(); } - @Override - public int arrayOffset() { - return position; - } - - @Override - public IntBuffer slice() { - return new EaglerLWJGLIntBuffer(address + (position << SHIFT), limit - position, false); - } - @Override public IntBuffer duplicate() { return new EaglerLWJGLIntBuffer(address, capacity, position, limit, mark, false); } - @Override - public IntBuffer asReadOnlyBuffer() { - return new EaglerLWJGLIntBuffer(address, capacity, position, limit, mark, false); - } - @Override public int get() { - if(position >= limit) throw new ArrayIndexOutOfBoundsException(position); + if(position >= limit) throw Buffer.makeIOOBE(position); return UnsafeUtils.getMemInt(address + ((position++) << SHIFT)); } @Override public IntBuffer put(int b) { - if(position >= limit) throw new ArrayIndexOutOfBoundsException(position); + if(position >= limit) throw Buffer.makeIOOBE(position); UnsafeUtils.setMemInt(address + ((position++) << SHIFT), b); return this; } @Override public int get(int index) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemInt(address + (index << SHIFT)); } @Override public IntBuffer put(int index, int b) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemInt(address + (index << SHIFT), b); return this; } @Override public int getElement(int index) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemInt(address + (index << SHIFT)); } @Override public void putElement(int index, int value) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemInt(address + (index << SHIFT), value); } @Override public IntBuffer get(int[] dst, int offset, int length) { - if(position + length > limit) throw new ArrayIndexOutOfBoundsException(position + length - 1); + if(position + length > limit) throw Buffer.makeIOOBE(position + length - 1); UnsafeMemcpy.memcpyAlignDst(dst, offset << SHIFT, address + (position << SHIFT), length); position += length; return this; @@ -153,7 +131,7 @@ public class EaglerLWJGLIntBuffer implements IntBuffer { @Override public IntBuffer get(int[] dst) { - if(position + dst.length > limit) throw new ArrayIndexOutOfBoundsException(position + dst.length - 1); + if(position + dst.length > limit) throw Buffer.makeIOOBE(position + dst.length - 1); UnsafeMemcpy.memcpyAlignDst(dst, 0, address + (position << SHIFT), dst.length); position += dst.length; return this; @@ -164,13 +142,13 @@ public class EaglerLWJGLIntBuffer implements IntBuffer { if(src instanceof EaglerLWJGLIntBuffer) { EaglerLWJGLIntBuffer c = (EaglerLWJGLIntBuffer)src; int l = c.limit - c.position; - if(position + l > limit) throw new ArrayIndexOutOfBoundsException(position + l - 1); + if(position + l > limit) throw Buffer.makeIOOBE(position + l - 1); UnsafeMemcpy.memcpy(address + (position << SHIFT), c.address + (c.position << SHIFT), l << SHIFT); position += l; c.position += l; }else { int l = src.remaining(); - if(position + l > limit) throw new ArrayIndexOutOfBoundsException(position + l - 1); + if(position + l > limit) throw Buffer.makeIOOBE(position + l - 1); for(int i = 0; i < l; ++i) { UnsafeUtils.setMemInt(address + ((position + l) << SHIFT), src.get()); } @@ -181,7 +159,7 @@ public class EaglerLWJGLIntBuffer implements IntBuffer { @Override public IntBuffer put(int[] src, int offset, int length) { - if(position + length > limit) throw new ArrayIndexOutOfBoundsException(position + length - 1); + if(position + length > limit) throw Buffer.makeIOOBE(position + length - 1); UnsafeMemcpy.memcpyAlignSrc(address + (position << SHIFT), src, offset << SHIFT, length); position += length; return this; @@ -189,36 +167,12 @@ public class EaglerLWJGLIntBuffer implements IntBuffer { @Override public IntBuffer put(int[] src) { - if(position + src.length > limit) throw new ArrayIndexOutOfBoundsException(position + src.length - 1); + if(position + src.length > limit) throw Buffer.makeIOOBE(position + src.length - 1); UnsafeMemcpy.memcpyAlignSrc(address + (position << SHIFT), src, 0, src.length); position += src.length; return this; } - @Override - public int getArrayOffset() { - return position; - } - - @Override - public IntBuffer compact() { - if(limit > capacity) throw new ArrayIndexOutOfBoundsException(limit); - if(position > limit) throw new ArrayIndexOutOfBoundsException(position); - - if(position == limit) { - return new EaglerLWJGLIntBuffer(0l, 0, false); - } - - int newLen = limit - position; - long newAlloc = JEmalloc.nje_malloc(newLen); - if(newAlloc == 0l) { - throw new OutOfMemoryError("Native je_malloc call returned null pointer!"); - } - UnsafeMemcpy.memcpy(newAlloc, address + (position << SHIFT), newLen << SHIFT); - - return new EaglerLWJGLIntBuffer(newAlloc, newLen, true); - } - @Override public boolean isDirect() { return true; @@ -233,7 +187,7 @@ public class EaglerLWJGLIntBuffer implements IntBuffer { @Override public IntBuffer reset() { int m = mark; - if(m < 0) throw new ArrayIndexOutOfBoundsException("Invalid mark: " + m); + if(m < 0) throw new IndexOutOfBoundsException("Invalid mark: " + m); position = m; return this; } @@ -263,14 +217,14 @@ public class EaglerLWJGLIntBuffer implements IntBuffer { @Override public IntBuffer limit(int newLimit) { - if(newLimit < 0 || newLimit > capacity) throw new ArrayIndexOutOfBoundsException(newLimit); + if(newLimit < 0 || newLimit > capacity) throw Buffer.makeIOOBE(newLimit); limit = newLimit; return this; } @Override public IntBuffer position(int newPosition) { - if(newPosition < 0 || newPosition > limit) throw new ArrayIndexOutOfBoundsException(newPosition); + if(newPosition < 0 || newPosition > limit) throw Buffer.makeIOOBE(newPosition); position = newPosition; return this; } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLShortBuffer.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLShortBuffer.java index eba363d1..41d45f61 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLShortBuffer.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerLWJGLShortBuffer.java @@ -1,7 +1,5 @@ package net.lax1dude.eaglercraft.v1_8.internal.buffer; -import org.lwjgl.system.jemalloc.JEmalloc; - import net.lax1dude.unsafememcpy.UnsafeMemcpy; import net.lax1dude.unsafememcpy.UnsafeUtils; @@ -21,7 +19,7 @@ import net.lax1dude.unsafememcpy.UnsafeUtils; * */ public class EaglerLWJGLShortBuffer implements ShortBuffer { - + final long address; final boolean original; @@ -29,9 +27,9 @@ public class EaglerLWJGLShortBuffer implements ShortBuffer { private int position; private int limit; private int mark; - + private static final int SHIFT = 1; - + EaglerLWJGLShortBuffer(long address, int capacity, boolean original) { this(address, capacity, 0, capacity, -1, original); } @@ -44,7 +42,7 @@ public class EaglerLWJGLShortBuffer implements ShortBuffer { this.mark = mark; this.original = original; } - + @Override public int capacity() { return capacity; @@ -70,82 +68,62 @@ public class EaglerLWJGLShortBuffer implements ShortBuffer { return position < limit; } - @Override - public boolean isReadOnly() { - return false; - } - @Override public boolean hasArray() { return false; } @Override - public Object array() { + public short[] array() { throw new UnsupportedOperationException(); } - @Override - public int arrayOffset() { - return position; - } - - @Override - public ShortBuffer slice() { - return new EaglerLWJGLShortBuffer(address + (position << SHIFT), limit - position, false); - } - @Override public ShortBuffer duplicate() { return new EaglerLWJGLShortBuffer(address, capacity, position, limit, mark, false); } - @Override - public ShortBuffer asReadOnlyBuffer() { - return new EaglerLWJGLShortBuffer(address, capacity, position, limit, mark, false); - } - @Override public short get() { - if(position >= limit) throw new ArrayIndexOutOfBoundsException(position); + if(position >= limit) throw Buffer.makeIOOBE(position); return UnsafeUtils.getMemShort(address + ((position++) << SHIFT)); } @Override public ShortBuffer put(short b) { - if(position >= limit) throw new ArrayIndexOutOfBoundsException(position); + if(position >= limit) throw Buffer.makeIOOBE(position); UnsafeUtils.setMemShort(address + ((position++) << SHIFT), b); return this; } @Override public short get(int index) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemShort(address + (index << SHIFT)); } @Override public ShortBuffer put(int index, short b) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemShort(address + (index << SHIFT), b); return this; } @Override public short getElement(int index) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); return UnsafeUtils.getMemShort(address + (index << SHIFT)); } @Override public void putElement(int index, short value) { - if(index >= limit) throw new ArrayIndexOutOfBoundsException(index); + if(index < 0 || index >= limit) throw Buffer.makeIOOBE(index); UnsafeUtils.setMemShort(address + (index << SHIFT), value); } @Override public ShortBuffer get(short[] dst, int offset, int length) { - if(position + length > limit) throw new ArrayIndexOutOfBoundsException(position + length - 1); + if(position + length > limit) throw Buffer.makeIOOBE(position + length - 1); UnsafeMemcpy.memcpyAlignDst(dst, offset << SHIFT, address + (position << SHIFT), length); position += length; return this; @@ -153,7 +131,7 @@ public class EaglerLWJGLShortBuffer implements ShortBuffer { @Override public ShortBuffer get(short[] dst) { - if(position + dst.length > limit) throw new ArrayIndexOutOfBoundsException(position + dst.length - 1); + if(position + dst.length > limit) throw Buffer.makeIOOBE(position + dst.length - 1); UnsafeMemcpy.memcpyAlignDst(dst, 0, address + (position << SHIFT), dst.length); position += dst.length; return this; @@ -164,13 +142,13 @@ public class EaglerLWJGLShortBuffer implements ShortBuffer { if(src instanceof EaglerLWJGLShortBuffer) { EaglerLWJGLShortBuffer c = (EaglerLWJGLShortBuffer)src; int l = c.limit - c.position; - if(position + l > limit) throw new ArrayIndexOutOfBoundsException(position + l - 1); + if(position + l > limit) throw Buffer.makeIOOBE(position + l - 1); UnsafeMemcpy.memcpy(address + (position << SHIFT), c.address + (c.position << SHIFT), l << SHIFT); position += l; c.position += l; }else { int l = src.remaining(); - if(position + l > limit) throw new ArrayIndexOutOfBoundsException(position + l - 1); + if(position + l > limit) throw Buffer.makeIOOBE(position + l - 1); for(int i = 0; i < l; ++i) { UnsafeUtils.setMemInt(address + ((position + l) << SHIFT), src.get()); } @@ -181,7 +159,7 @@ public class EaglerLWJGLShortBuffer implements ShortBuffer { @Override public ShortBuffer put(short[] src, int offset, int length) { - if(position + length > limit) throw new ArrayIndexOutOfBoundsException(position + length - 1); + if(position + length > limit) throw Buffer.makeIOOBE(position + length - 1); UnsafeMemcpy.memcpyAlignSrc(address + (position << SHIFT), src, offset << SHIFT, length); position += length; return this; @@ -189,36 +167,12 @@ public class EaglerLWJGLShortBuffer implements ShortBuffer { @Override public ShortBuffer put(short[] src) { - if(position + src.length > limit) throw new ArrayIndexOutOfBoundsException(position + src.length - 1); + if(position + src.length > limit) throw Buffer.makeIOOBE(position + src.length - 1); UnsafeMemcpy.memcpyAlignSrc(address + (position << SHIFT), src, 0, src.length); position += src.length; return this; } - @Override - public int getArrayOffset() { - return position; - } - - @Override - public ShortBuffer compact() { - if(limit > capacity) throw new ArrayIndexOutOfBoundsException(limit); - if(position > limit) throw new ArrayIndexOutOfBoundsException(position); - - if(position == limit) { - return new EaglerLWJGLShortBuffer(0l, 0, false); - } - - int newLen = limit - position; - long newAlloc = JEmalloc.nje_malloc(newLen); - if(newAlloc == 0l) { - throw new OutOfMemoryError("Native je_malloc call returned null pointer!"); - } - UnsafeMemcpy.memcpy(newAlloc, address + (position << SHIFT), newLen << SHIFT); - - return new EaglerLWJGLShortBuffer(newAlloc, newLen, true); - } - @Override public boolean isDirect() { return true; @@ -233,7 +187,7 @@ public class EaglerLWJGLShortBuffer implements ShortBuffer { @Override public ShortBuffer reset() { int m = mark; - if(m < 0) throw new ArrayIndexOutOfBoundsException("Invalid mark: " + m); + if(m < 0) throw new IndexOutOfBoundsException("Invalid mark: " + m); position = m; return this; } @@ -263,14 +217,14 @@ public class EaglerLWJGLShortBuffer implements ShortBuffer { @Override public ShortBuffer limit(int newLimit) { - if(newLimit < 0 || newLimit > capacity) throw new ArrayIndexOutOfBoundsException(newLimit); + if(newLimit < 0 || newLimit > capacity) throw Buffer.makeIOOBE(newLimit); limit = newLimit; return this; } @Override public ShortBuffer position(int newPosition) { - if(newPosition < 0 || newPosition > limit) throw new ArrayIndexOutOfBoundsException(newPosition); + if(newPosition < 0 || newPosition > limit) throw Buffer.makeIOOBE(newPosition); position = newPosition; return this; } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DebugFilesystem.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DebugFilesystem.java index a5ee8df6..eb5cbcd8 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DebugFilesystem.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DebugFilesystem.java @@ -5,6 +5,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import net.lax1dude.eaglercraft.v1_8.internal.IEaglerFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.PlatformFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; import net.lax1dude.eaglercraft.v1_8.internal.VFSFilenameIterator; @@ -27,21 +28,33 @@ import net.lax1dude.eaglercraft.v1_8.internal.vfs2.VFSIterator2.BreakLoop; * POSSIBILITY OF SUCH DAMAGE. * */ -public class DebugFilesystem implements PlatformFilesystem.IFilesystemProvider { +public class DebugFilesystem implements IEaglerFilesystem { - public static DebugFilesystem initialize(File filesystemRoot) { + public static DebugFilesystem initialize(String fsName, File filesystemRoot) { if(!filesystemRoot.isDirectory() && !filesystemRoot.mkdirs()) { throw new EaglerFileSystemException("Could not create directory for virtual filesystem: " + filesystemRoot.getAbsolutePath()); } - return new DebugFilesystem(filesystemRoot); + return new DebugFilesystem(fsName, filesystemRoot); } private final File filesystemRoot; + private final String fsName; - private DebugFilesystem(File root) { + private DebugFilesystem(String fsName, File root) { + this.fsName = fsName; this.filesystemRoot = root; } + @Override + public String getFilesystemName() { + return fsName; + } + + @Override + public String getInternalDBName() { + return "desktopruntime:" + filesystemRoot.getAbsolutePath(); + } + @Override public boolean eaglerDelete(String pathName) { File f = getJREFile(pathName); @@ -78,7 +91,7 @@ public class DebugFilesystem implements PlatformFilesystem.IFilesystemProvider { return tmp; }catch (IOException e) { throw new EaglerFileSystemException("Failed to read: " + f.getAbsolutePath(), e); - }catch(ArrayIndexOutOfBoundsException ex) { + }catch(IndexOutOfBoundsException ex) { throw new EaglerFileSystemException("ERROR: Expected " + fileSize + " bytes, buffer overflow reading: " + f.getAbsolutePath(), ex); }finally { if(buf != null) { @@ -221,4 +234,15 @@ public class DebugFilesystem implements PlatformFilesystem.IFilesystemProvider { f.delete(); } } + + @Override + public boolean isRamdisk() { + return false; + } + + @Override + public void closeHandle() { + + } + } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopClientConfigAdapter.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopClientConfigAdapter.java index 3c1fd1de..5de69dc5 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopClientConfigAdapter.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopClientConfigAdapter.java @@ -31,7 +31,7 @@ public class DesktopClientConfigAdapter implements IClientConfigAdapter { public static final IClientConfigAdapter instance = new DesktopClientConfigAdapter(); - public final List defaultServers = new ArrayList(); + public final List defaultServers = new ArrayList<>(); private final DesktopClientConfigAdapterHooks hooks = new DesktopClientConfigAdapterHooks(); @@ -52,17 +52,17 @@ public class DesktopClientConfigAdapter implements IClientConfigAdapter { @Override public String getWorldsDB() { - return "desktop"; + return "worlds"; } @Override public String getResourcePacksDB() { - return "desktop"; + return "resourcePacks"; } @Override public JSONObject getIntegratedServerOpts() { - return new JSONObject("{\"container\":null,\"worldsDB\":\"desktop\"}"); + return new JSONObject("{\"container\":null,\"worldsDB\":\"worlds\"}"); } private final List relays = new ArrayList<>(); @@ -148,6 +148,51 @@ public class DesktopClientConfigAdapter implements IClientConfigAdapter { return true; } + @Override + public boolean isEnableServerCookies() { + return true; + } + + @Override + public boolean isAllowServerRedirects() { + return true; + } + + @Override + public boolean isOpenDebugConsoleOnLaunch() { + return false; + } + + @Override + public boolean isForceWebViewSupport() { + return false; + } + + @Override + public boolean isEnableWebViewCSP() { + return true; + } + + @Override + public boolean isAllowBootMenu() { + return false; + } + + @Override + public boolean isForceProfanityFilter() { + return false; + } + + @Override + public boolean isEaglerNoDelay() { + return false; + } + + @Override + public boolean isRamdiskMode() { + return false; + } + @Override public IClientConfigAdapterHooks getHooks() { return hooks; @@ -170,5 +215,12 @@ public class DesktopClientConfigAdapter implements IClientConfigAdapter { } + @Override + public void callScreenChangedHook(String screenName, int scaledWidth, int scaledHeight, int realWidth, + int realHeight, int scaleFactor) { + + } + } + } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketClient.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketClient.java new file mode 100644 index 00000000..a3b72a89 --- /dev/null +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketClient.java @@ -0,0 +1,109 @@ +package net.lax1dude.eaglercraft.v1_8.internal.lwjgl; + +import java.net.URI; + +import net.lax1dude.eaglercraft.v1_8.EaglercraftVersion; +import net.lax1dude.eaglercraft.v1_8.internal.AbstractWebSocketClient; +import net.lax1dude.eaglercraft.v1_8.internal.EnumEaglerConnectionState; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class DesktopWebSocketClient extends AbstractWebSocketClient { + + static final Logger logger = LogManager.getLogger("DesktopWebSocketClient"); + + volatile EnumEaglerConnectionState playConnectState = EnumEaglerConnectionState.CONNECTING; + final Object connectOpenMutex = new Object(); + final WebSocketClientImpl clientImpl; + final URI currentURI; + final String currentURIStr; + + public DesktopWebSocketClient(URI currentURI) { + super(currentURI.toString()); + this.currentURI = currentURI; + currentURIStr = currentURI.toString(); + clientImpl = new WebSocketClientImpl(this, currentURI); + clientImpl.addHeader("Origin", "EAG_LWJGL_" + (EaglercraftVersion.projectForkName + "_" + + EaglercraftVersion.projectOriginVersion).replaceAll("[^a-zA-Z0-9\\-_\\.]", "_")); + } + + @Override + public EnumEaglerConnectionState getState() { + return playConnectState; + } + + @Override + public boolean connectBlocking(int timeoutMS) { + synchronized(connectOpenMutex) { + try { + connectOpenMutex.wait(timeoutMS); + } catch (InterruptedException e) { + return false; + } + } + return playConnectState.isOpen(); + } + + @Override + public boolean isOpen() { + return playConnectState.isOpen(); + } + + @Override + public boolean isClosed() { + return playConnectState.isClosed(); + } + + @Override + public void close() { + if(!playConnectState.isClosed()) { + try { + clientImpl.closeBlocking(); + } catch (InterruptedException e) { + } + playConnectState = EnumEaglerConnectionState.CLOSED; + } + } + + @Override + public void send(String str) { + if(clientImpl.isClosed()) { + logger.error("[{}]: Client tried to send {} char packet while the socket was closed!", currentURIStr, str.length()); + }else { + clientImpl.send(str); + } + } + + @Override + public void send(byte[] bytes) { + if(clientImpl.isClosed()) { + logger.error("[{}]: Client tried to send {} byte packet while the socket was closed!", currentURIStr, bytes.length); + }else { + clientImpl.send(bytes); + } + } + + public void handleString(String str) { + addRecievedFrame(new DesktopWebSocketFrameString(str)); + } + + public void handleBytes(byte[] array) { + addRecievedFrame(new DesktopWebSocketFrameBinary(array)); + } + +} diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketFrameBinary.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketFrameBinary.java new file mode 100644 index 00000000..c8919714 --- /dev/null +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketFrameBinary.java @@ -0,0 +1,64 @@ +package net.lax1dude.eaglercraft.v1_8.internal.lwjgl; + +import java.io.InputStream; + +import net.lax1dude.eaglercraft.v1_8.EaglerInputStream; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketFrame; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class DesktopWebSocketFrameBinary implements IWebSocketFrame { + + private final byte[] byteArray; + private final long timestamp; + + public DesktopWebSocketFrameBinary(byte[] byteArray) { + this.byteArray = byteArray; + this.timestamp = PlatformRuntime.steadyTimeMillis(); + } + + @Override + public boolean isString() { + return false; + } + + @Override + public String getString() { + return null; + } + + @Override + public byte[] getByteArray() { + return byteArray; + } + + @Override + public InputStream getInputStream() { + return new EaglerInputStream(byteArray); + } + + @Override + public int getLength() { + return byteArray.length; + } + + @Override + public long getTimestamp() { + return timestamp; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket70SpecialUpdate.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketFrameString.java similarity index 50% rename from sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket70SpecialUpdate.java rename to sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketFrameString.java index 1bdd4630..3be7a6b2 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket70SpecialUpdate.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/DesktopWebSocketFrameString.java @@ -1,8 +1,9 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; +package net.lax1dude.eaglercraft.v1_8.internal.lwjgl; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +import java.io.InputStream; + +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketFrame; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; /** * Copyright (c) 2024 lax1dude. All Rights Reserved. @@ -19,37 +20,44 @@ import java.io.IOException; * POSSIBILITY OF SUCH DAMAGE. * */ -public class IPacket70SpecialUpdate extends IPacket { +public class DesktopWebSocketFrameString implements IWebSocketFrame { - public static final int OPERATION_UPDATE_CERTIFICATE = 0x69; + private final String string; + private final long timestamp; - public int operation; - public byte[] updatePacket; - - public IPacket70SpecialUpdate() { - } - - public IPacket70SpecialUpdate(int operation, byte[] updatePacket) { - this.operation = operation; - this.updatePacket = updatePacket; + public DesktopWebSocketFrameString(String string) { + this.string = string; + this.timestamp = PlatformRuntime.steadyTimeMillis(); } @Override - public void read(DataInputStream input) throws IOException { - operation = input.read(); - updatePacket = new byte[input.readUnsignedShort()]; - input.read(updatePacket); + public boolean isString() { + return true; } @Override - public void write(DataOutputStream output) throws IOException { - output.write(operation); - output.writeShort(updatePacket.length); - output.write(updatePacket); + public String getString() { + return string; } @Override - public int packetLength() { - return 3 + updatePacket.length; + public byte[] getByteArray() { + return null; } + + @Override + public InputStream getInputStream() { + return null; + } + + @Override + public int getLength() { + return string.length(); + } + + @Override + public long getTimestamp() { + return timestamp; + } + } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewHTTPD.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewHTTPD.java new file mode 100644 index 00000000..f2eabebf --- /dev/null +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewHTTPD.java @@ -0,0 +1,41 @@ +package net.lax1dude.eaglercraft.v1_8.internal.lwjgl; + +import fi.iki.elonen.NanoHTTPD; +import fi.iki.elonen.NanoHTTPD.Response.Status; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +class FallbackWebViewHTTPD extends NanoHTTPD { + + static final Logger logger = FallbackWebViewServer.logger; + + private String index; + + FallbackWebViewHTTPD(String hostname, int port, String index) { + super(hostname, port); + this.index = index; + } + + @Override + public Response serve(IHTTPSession session) { + if("/RTWebViewClient".equals(session.getUri())) { + return newFixedLengthResponse(Status.OK, MIME_HTML, index); + }else { + return newFixedLengthResponse(Status.NOT_FOUND, MIME_HTML, "Eaglercraft Desktop Runtime

404 Not Found

"); + } + } +} diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewProtocol.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewProtocol.java new file mode 100644 index 00000000..8516510e --- /dev/null +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewProtocol.java @@ -0,0 +1,299 @@ +package net.lax1dude.eaglercraft.v1_8.internal.lwjgl; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.json.JSONException; +import org.json.JSONObject; + +import net.lax1dude.eaglercraft.v1_8.internal.WebViewOptions; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.webview.PermissionsCache; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +class FallbackWebViewProtocol { + + static final Logger logger = FallbackWebViewServer.logger; + + private static final Map> packetIDToClass = new HashMap<>(); + private static final Map,Integer> packetClassToID = new HashMap<>(); + + private static final int CLIENT_TO_SERVER = 0; + private static final int SERVER_TO_CLIENT = 1; + + static { + register(0x00, CLIENT_TO_SERVER, CPacketClientHandshake.class); + register(0x01, SERVER_TO_CLIENT, SPacketServerHandshake.class); + register(0x02, SERVER_TO_CLIENT, SPacketServerError.class); + register(0x03, CLIENT_TO_SERVER, CPacketWebViewChannelOpen.class); + register(0x04, CLIENT_TO_SERVER, CPacketWebViewChannelClose.class); + register(0x05, CLIENT_TO_SERVER, CPacketWebViewMessage.class); + register(0x06, SERVER_TO_CLIENT, SPacketWebViewMessage.class); + register(0x07, CLIENT_TO_SERVER, CPacketWebViewJSPermission.class); + } + + private static void register(int id, int dir, Class packet) { + if(dir == CLIENT_TO_SERVER) { + packetIDToClass.put(id, packet); + }else if(dir == SERVER_TO_CLIENT) { + packetClassToID.put(packet, id); + }else { + throw new IllegalArgumentException(); + } + } + + static String writePacket(FallbackWebViewPacket packet) { + Class cls = packet.getClass(); + Integer id = packetClassToID.get(cls); + if(id == null) { + throw new RuntimeException("Tried to send unknown packet to client: " + cls.getSimpleName()); + } + JSONObject json = new JSONObject(); + json.put("$", id); + packet.writePacket(json); + return json.toString(); + } + + static FallbackWebViewPacket readPacket(String data) { + try { + JSONObject json = new JSONObject(data); + int id = json.getInt("$"); + Class cls = packetIDToClass.get(id); + if(cls == null) { + logger.error("Unknown packet ID {} recieved from webview controller", id); + return null; + } + FallbackWebViewPacket ret; + try { + ret = cls.newInstance(); + }catch(Throwable t) { + throw new RuntimeException("Failed to call packet constructor for \"" + cls.getSimpleName() + "\"! (is it defined?)"); + } + ret.readPacket(json); + return ret; + }catch(Throwable ex) { + logger.error("Failed to parse message from webview controller: \"{}\"", data); + logger.error(ex); + return null; + } + } + + static interface FallbackWebViewPacket { + + void readPacket(JSONObject json); + + void writePacket(JSONObject json); + + } + + static class CPacketClientHandshake implements FallbackWebViewPacket { + + public boolean cspSupport; + + public CPacketClientHandshake() { + } + + @Override + public void readPacket(JSONObject json) { + cspSupport = json.getBoolean("cspSupport"); + } + + @Override + public void writePacket(JSONObject json) { + throw new UnsupportedOperationException("Client only!"); + } + + } + + static class CPacketWebViewChannelOpen implements FallbackWebViewPacket { + + public String messageChannel; + + public CPacketWebViewChannelOpen() { + } + + public CPacketWebViewChannelOpen(String messageChannel) { + this.messageChannel = messageChannel; + } + + @Override + public void readPacket(JSONObject json) { + messageChannel = json.getString("channel"); + if(messageChannel.length() > 255) { + throw new JSONException("Channel name too long!"); + } + } + + @Override + public void writePacket(JSONObject json) { + throw new UnsupportedOperationException("Client only!"); + } + + } + + static class CPacketWebViewChannelClose implements FallbackWebViewPacket { + + public CPacketWebViewChannelClose() { + } + + @Override + public void readPacket(JSONObject json) { + + } + + @Override + public void writePacket(JSONObject json) { + throw new UnsupportedOperationException("Client only!"); + } + + } + + // for string messages, binary are sent as a binary frame + static class CPacketWebViewMessage implements FallbackWebViewPacket { + + public String messageContent; + + public CPacketWebViewMessage() { + } + + public CPacketWebViewMessage(String messageContent) { + this.messageContent = messageContent; + } + + @Override + public void readPacket(JSONObject json) { + messageContent = json.getString("msg"); + } + + @Override + public void writePacket(JSONObject json) { + throw new UnsupportedOperationException("Client only!"); + } + + } + + static class SPacketServerHandshake implements FallbackWebViewPacket { + + public WebViewOptions options; + public EnumWebViewJSPermission hasApprovedJS; + + public SPacketServerHandshake() { + } + + public SPacketServerHandshake(WebViewOptions options, EnumWebViewJSPermission hasApprovedJS) { + this.options = options; + this.hasApprovedJS = hasApprovedJS; + } + + @Override + public void readPacket(JSONObject json) { + throw new UnsupportedOperationException("Server only!"); + } + + @Override + public void writePacket(JSONObject json) { + json.put("contentMode", options.contentMode.toString()); + json.put("fallbackTitle", options.fallbackTitle); + json.put("scriptEnabled", options.scriptEnabled); + json.put("strictCSPEnable", options.strictCSPEnable); + json.put("serverMessageAPIEnabled", options.serverMessageAPIEnabled); + json.put("url", options.url); + json.put("blob", options.blob != null ? new String(options.blob, StandardCharsets.UTF_8) : null); + json.put("hasApprovedJS", hasApprovedJS.toString()); + } + + } + + static class SPacketServerError implements FallbackWebViewPacket { + + public String errorMessage; + + public SPacketServerError() { + } + + public SPacketServerError(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public void readPacket(JSONObject json) { + throw new UnsupportedOperationException("Server only!"); + } + + @Override + public void writePacket(JSONObject json) { + json.put("msg", errorMessage); + } + + } + + static class SPacketWebViewMessage implements FallbackWebViewPacket { + + public String message; + + public SPacketWebViewMessage() { + } + + public SPacketWebViewMessage(String message) { + this.message = message; + } + + @Override + public void readPacket(JSONObject json) { + throw new UnsupportedOperationException("Server only!"); + } + + @Override + public void writePacket(JSONObject json) { + json.put("msg", message); + } + + } + + static enum EnumWebViewJSPermission { + NOT_SET, ALLOW, BLOCK; + + static EnumWebViewJSPermission fromPermission(PermissionsCache.Permission perm) { + if(perm != null) { + return perm.choice ? ALLOW : BLOCK; + }else { + return NOT_SET; + } + } + } + + static class CPacketWebViewJSPermission implements FallbackWebViewPacket { + + public EnumWebViewJSPermission permission; + + public CPacketWebViewJSPermission() { + } + + @Override + public void readPacket(JSONObject json) { + permission = EnumWebViewJSPermission.valueOf(json.getString("perm")); + } + + @Override + public void writePacket(JSONObject json) { + throw new UnsupportedOperationException("Client only!"); + } + + } + +} diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewServer.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewServer.java new file mode 100644 index 00000000..79da7b11 --- /dev/null +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewServer.java @@ -0,0 +1,190 @@ +package net.lax1dude.eaglercraft.v1_8.internal.lwjgl; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.nio.charset.StandardCharsets; + +import org.json.JSONObject; + +import net.lax1dude.eaglercraft.v1_8.internal.IClientConfigAdapter; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; +import net.lax1dude.eaglercraft.v1_8.internal.WebViewOptions; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketWebViewMessageV4EAG; +import net.lax1dude.eaglercraft.v1_8.webview.WebViewOverlayController.IPacketSendCallback; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class FallbackWebViewServer { + + static final Logger logger = LogManager.getLogger("FallbackWebViewServer"); + + public static final String LISTEN_ADDR = "127.69.69.69"; + + public static final File webViewClientHTML = new File("RTWebViewClient.html"); + + public final WebViewOptions options; + + private FallbackWebViewWSD websocketServer; + private FallbackWebViewHTTPD httpServer; + + private String currentURL; + private volatile boolean dead; + + private IPacketSendCallback callback = null; + + public FallbackWebViewServer(WebViewOptions options) { + this.options = options; + } + + public void start() throws RuntimeException { + dead = false; + StringBuilder vigg = new StringBuilder(); + try(BufferedReader reader = new BufferedReader( + new InputStreamReader(new FileInputStream(webViewClientHTML), StandardCharsets.UTF_8))) { + String line; + while((line = reader.readLine()) != null) { + vigg.append(line).append('\n'); + } + }catch(IOException ex) { + logger.error("Failed to read \"{}\"!"); + } + String indexHTML = vigg.toString(); + + Object mutex = new Object(); + websocketServer = new FallbackWebViewWSD(LISTEN_ADDR, randomPort(), options); + websocketServer.setEaglerPacketSendCallback(callback); + synchronized(mutex) { + websocketServer.doStartup(mutex); + try { + mutex.wait(5000l); + } catch (InterruptedException e) { + } + } + if(!websocketServer.hasStarted) { + logger.error("Failed to start WebSocket in time!"); + try { + websocketServer.stop(5000); + }catch(Throwable t) { + } + websocketServer = null; + throw new RuntimeException("Failed to start WebSocket server!"); + } + InetSocketAddress addr = websocketServer.getAddress(); + String wsAddr = "ws://" + addr.getHostString() + ":" + addr.getPort() + "/"; + logger.info("Listening for WebSocket on {}", wsAddr); + indexHTML = indexHTML.replace("${client_websocket_uri}", wsAddr); + + JSONObject optsExport = new JSONObject(); + IClientConfigAdapter cfgAdapter = PlatformRuntime.getClientConfigAdapter(); + optsExport.put("forceWebViewSupport", cfgAdapter.isForceWebViewSupport()); + optsExport.put("enableWebViewCSP", cfgAdapter.isEnableWebViewCSP()); + indexHTML = indexHTML.replace("{eaglercraftXOpts}", optsExport.toString()); + + httpServer = new FallbackWebViewHTTPD(LISTEN_ADDR, 0, indexHTML); + try { + httpServer.start(5000, true); + } catch (IOException e) { + logger.error("Failed to start NanoHTTPD!"); + try { + websocketServer.stop(5000); + }catch(Throwable t) { + } + websocketServer = null; + httpServer = null; + throw new RuntimeException("Failed to start NanoHTTPD!", e); + } + int httpPort = httpServer.getListeningPort(); + currentURL = "http://" + LISTEN_ADDR + ":" + httpPort + "/RTWebViewClient"; + logger.info("Listening for HTTP on {}", currentURL); + } + + private int randomPort() { + try(ServerSocket sockler = new ServerSocket(0)) { + return sockler.getLocalPort(); + }catch(IOException ex) { + throw new RuntimeException("Failed to find random port to bind to!", ex); + } + } + + public boolean isDead() { + return dead; + } + + public String getURL() { + return !dead ? currentURL : null; + } + + public void handleMessageFromServer(SPacketWebViewMessageV4EAG packet) { + if(packet.type == SPacketWebViewMessageV4EAG.TYPE_STRING) { + if(websocketServer != null) { + websocketServer.handleServerMessageStr(new String(packet.data, StandardCharsets.UTF_8)); + }else { + logger.error("Recieved string message, but the webview server is not running!"); + } + }else if(packet.type == SPacketWebViewMessageV4EAG.TYPE_BINARY) { + if(websocketServer != null) { + websocketServer.handleServerMessageBytes(packet.data); + }else { + logger.error("Recieved string message, but the webview server is not running!"); + } + }else { + logger.error("Unknown server webview message type {}", packet.type); + } + } + + public void setPacketSendCallback(IPacketSendCallback callback) { + this.callback = callback; + if(websocketServer != null) { + websocketServer.setEaglerPacketSendCallback(callback); + } + } + + public void runTick() { + + } + + public void killServer() { + if(!dead) { + dead = true; + if(websocketServer != null) { + try { + websocketServer.stop(10000); + } catch (Throwable th) { + logger.error("Failed to stop WebSocket server, aborting"); + logger.error(th); + } + websocketServer = null; + } + if(httpServer != null) { + try { + httpServer.stop(); + } catch (Throwable th) { + logger.error("Failed to stop HTTP server, aborting"); + logger.error(th); + } + httpServer = null; + } + } + } + +} diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewWSD.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewWSD.java new file mode 100644 index 00000000..bb941103 --- /dev/null +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/FallbackWebViewWSD.java @@ -0,0 +1,273 @@ +package net.lax1dude.eaglercraft.v1_8.internal.lwjgl; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +import org.java_websocket.WebSocket; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.server.WebSocketServer; + +import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; +import net.lax1dude.eaglercraft.v1_8.internal.WebViewOptions; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.CPacketWebViewMessageEnV4EAG; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.CPacketWebViewMessageV4EAG; +import net.lax1dude.eaglercraft.v1_8.webview.PermissionsCache; +import net.lax1dude.eaglercraft.v1_8.webview.PermissionsCache.Permission; +import net.lax1dude.eaglercraft.v1_8.webview.WebViewOverlayController.IPacketSendCallback; + +import static net.lax1dude.eaglercraft.v1_8.internal.lwjgl.FallbackWebViewProtocol.*; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +class FallbackWebViewWSD extends WebSocketServer { + + static final Logger logger = FallbackWebViewServer.logger; + + private Object onStartNotify; + + volatile boolean hasStarted = false; + + private final Object webSockMutex = new Object(); + volatile WebSocket webSocket = null; + + volatile boolean hasHandshake = false; + volatile String currentChannelName = null; + + private IPacketSendCallback callback = null; + WebViewOptions options; + + private boolean enableCSP; + private boolean cspSupport; + + FallbackWebViewWSD(String address, int port, WebViewOptions options) { + super(new InetSocketAddress(address, port), 1); + this.setTcpNoDelay(true); + this.setReuseAddr(true); + this.setConnectionLostTimeout(30); + this.options = options; + this.enableCSP = PlatformRuntime.getClientConfigAdapter().isEnableWebViewCSP(); + this.cspSupport = true; + } + + public void doStartup(Object onStartNotify) { + this.onStartNotify = onStartNotify; + this.start(); + } + + private void handleOpen() { + hasHandshake = false; + currentChannelName = null; + } + + private void handleClose() { + if(currentChannelName != null && callback != null) { + callback.sendPacket(new CPacketWebViewMessageEnV4EAG(false, null)); + } + currentChannelName = null; + } + + private int hashPermissionFlags() { + int i = (options.scriptEnabled ? 1 : 0); + i |= ((enableCSP && cspSupport && options.strictCSPEnable) ? 0 : 2); + i |= (options.serverMessageAPIEnabled ? 4 : 0); + return i; + } + + private void handleMessage(String str) { + WebSocket ws = webSocket; + FallbackWebViewPacket _packet = readPacket(str); + if(_packet != null) { + if(!hasHandshake) { + if(_packet instanceof CPacketClientHandshake) { + hasHandshake = true; + Permission perm = PermissionsCache.getJavaScriptAllowed(options.permissionsOriginUUID, hashPermissionFlags()); + ws.send(writePacket(new SPacketServerHandshake(options, EnumWebViewJSPermission.fromPermission(perm)))); + }else { + terminate("Unknown or unexpected packet: " + _packet.getClass().getSimpleName()); + } + }else { + if(_packet instanceof CPacketWebViewChannelOpen) { + CPacketWebViewChannelOpen packet = (CPacketWebViewChannelOpen)_packet; + if(currentChannelName == null) { + currentChannelName = packet.messageChannel; + logger.info("[{}]: opened WebView channel \"{}\"", ws.getRemoteSocketAddress(), packet.messageChannel); + safeCallbackSend(new CPacketWebViewMessageEnV4EAG(true, packet.messageChannel)); + }else { + terminate("Tried to open multiple channels"); + } + }else if(_packet instanceof CPacketWebViewMessage) { + CPacketWebViewMessage packet = (CPacketWebViewMessage)_packet; + if(currentChannelName != null) { + safeCallbackSend(new CPacketWebViewMessageV4EAG(packet.messageContent)); + }else { + terminate("Tried to send message without opening channel"); + } + }else if(_packet instanceof CPacketWebViewChannelClose) { + if(currentChannelName != null) { + currentChannelName = null; + safeCallbackSend(new CPacketWebViewMessageEnV4EAG(false, null)); + }else { + terminate("Tried to close missing channel"); + } + }else if(_packet instanceof CPacketWebViewJSPermission) { + CPacketWebViewJSPermission packet = (CPacketWebViewJSPermission)_packet; + switch(packet.permission) { + case NOT_SET: + PermissionsCache.clearJavaScriptAllowed(options.permissionsOriginUUID); + break; + case ALLOW: + PermissionsCache.setJavaScriptAllowed(options.permissionsOriginUUID, hashPermissionFlags(), true); + break; + case BLOCK: + PermissionsCache.setJavaScriptAllowed(options.permissionsOriginUUID, hashPermissionFlags(), false); + break; + default: + terminate("Unknown permission state selected!"); + break; + } + + }else { + terminate("Unknown or unexpected packet: " + _packet.getClass().getSimpleName()); + } + } + }else { + terminate("Invalid packet recieved"); + } + } + + private void handleMessage(ByteBuffer buffer) { + if(currentChannelName != null) { + safeCallbackSend(new CPacketWebViewMessageV4EAG(buffer.array())); + }else { + terminate("Sent binary webview message while channel was closed"); + } + } + + private void terminate(String msg) { + if(webSocket != null) { + logger.error("[{}]: Terminating connection, reason: \"{}\"", webSocket.getRemoteSocketAddress(), msg); + webSocket.send(writePacket(new SPacketServerError(msg))); + webSocket.close(); + } + } + + private void safeCallbackSend(GameMessagePacket packet) { + if(callback != null) { + callback.sendPacket(packet); + }else { + logger.error("webview sent packet to server, but there's no callback registered to send packets!"); + } + } + + void handleServerMessageStr(String msg) { + if(webSocket != null) { + if(currentChannelName != null) { + webSocket.send(writePacket(new SPacketWebViewMessage(msg))); + }else { + logger.error("Recieved string message from server, but the channel is not open!"); + } + }else { + logger.error("Recieved string message from server, but there is no active websocket!"); + } + } + + void handleServerMessageBytes(byte[] msg) { + if(webSocket != null) { + if(currentChannelName != null) { + webSocket.send(msg); + }else { + logger.error("Recieved binary message from server, but the channel is not open!"); + } + }else { + logger.error("Recieved binary message from server, but there is no active websocket!"); + } + } + + @Override + public void onStart() { + hasStarted = true; + if(onStartNotify != null) { + synchronized(onStartNotify) { + onStartNotify.notifyAll(); + } + onStartNotify = null; + }else { + logger.warn("No mutex to notify!"); + } + } + + @Override + public void onOpen(WebSocket arg0, ClientHandshake arg1) { + boolean result; + synchronized(webSockMutex) { + if(webSocket == null) { + webSocket = arg0; + result = true; + }else { + result = false; + } + } + if(result) { + logger.info("[{}]: WebSocket connection opened", arg0.getRemoteSocketAddress()); + handleOpen(); + }else { + logger.error("[{}]: Rejecting duplicate connection", arg0.getRemoteSocketAddress()); + arg0.send(writePacket(new SPacketServerError("You already have a tab open!"))); + arg0.close(); + } + } + + @Override + public void onMessage(WebSocket arg0, String arg1) { + if(arg0 == webSocket) { + handleMessage(arg1); + } + } + + @Override + public void onMessage(WebSocket arg0, ByteBuffer arg1) { + if(arg0 == webSocket) { + handleMessage(arg1); + } + } + + @Override + public void onClose(WebSocket arg0, int arg1, String arg2, boolean arg3) { + synchronized(webSockMutex) { + if(arg0 == webSocket) { + logger.info("[{}]: WebSocket connection closed", arg0.getRemoteSocketAddress()); + try { + handleClose(); + }finally { + webSocket = null; + } + } + } + } + + @Override + public void onError(WebSocket arg0, Exception arg1) { + logger.error("[{}]: WebSocket caught exception", arg0 != null ? arg0.getRemoteSocketAddress() : "null"); + logger.error(arg1); + } + + public void setEaglerPacketSendCallback(IPacketSendCallback callback) { + this.callback = callback; + } + +} diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/JDBCFilesystem.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/JDBCFilesystem.java index c6aae923..34d5c71a 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/JDBCFilesystem.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/JDBCFilesystem.java @@ -13,8 +13,8 @@ import java.util.LinkedList; import java.util.Map.Entry; import java.util.Properties; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformFilesystem.IFilesystemProvider; import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; +import net.lax1dude.eaglercraft.v1_8.internal.IEaglerFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.PlatformFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.VFSFilenameIterator; import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; @@ -38,15 +38,16 @@ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; * POSSIBILITY OF SUCH DAMAGE. * */ -public class JDBCFilesystem implements IFilesystemProvider { +public class JDBCFilesystem implements IEaglerFilesystem { public static final Logger logger = LogManager.getLogger("JDBCFilesystem"); private boolean newFilesystem = true; private static volatile boolean cleanupThreadStarted = false; - private static final Collection jdbcFilesystems = new LinkedList(); + private static final Collection jdbcFilesystems = new LinkedList<>(); + private final String dbName; private final String jdbcUri; private final String jdbcDriver; @@ -64,8 +65,8 @@ public class JDBCFilesystem implements IFilesystemProvider { private final Object mutex = new Object(); - public static IFilesystemProvider initialize(String jdbcUri, String jdbcDriver) { - Class driver; + public static IEaglerFilesystem initialize(String dbName, String jdbcUri, String jdbcDriver) { + Class driver; try { driver = Class.forName(jdbcDriver); } catch (ClassNotFoundException e) { @@ -87,8 +88,8 @@ public class JDBCFilesystem implements IFilesystemProvider { for(Entry etr : System.getProperties().entrySet()) { if(etr.getKey() instanceof String) { String str = (String)etr.getKey(); - if(str.startsWith("eagler.jdbc.opts.")) { - props.put(str.substring(17), etr.getValue()); + if(str.startsWith("eagler.jdbc." + dbName + ".opts.")) { + props.put(str.substring(18 + dbName.length()), etr.getValue()); } } } @@ -104,7 +105,7 @@ public class JDBCFilesystem implements IFilesystemProvider { throw new EaglerFileSystemException("Failed to connect to database: \"" + jdbcUri + "\"", ex); } try { - return new JDBCFilesystem(conn, jdbcUri, jdbcDriver); + return new JDBCFilesystem(dbName, conn, jdbcUri, jdbcDriver); } catch (SQLException ex) { try { conn.close(); @@ -114,7 +115,8 @@ public class JDBCFilesystem implements IFilesystemProvider { } } - private JDBCFilesystem(Connection conn, String jdbcUri, String jdbcDriver) throws SQLException { + private JDBCFilesystem(String dbName, Connection conn, String jdbcUri, String jdbcDriver) throws SQLException { + this.dbName = dbName; this.conn = conn; this.jdbcUri = jdbcUri; this.jdbcDriver = jdbcDriver; @@ -152,6 +154,16 @@ public class JDBCFilesystem implements IFilesystemProvider { } } + @Override + public String getFilesystemName() { + return dbName; + } + + @Override + public String getInternalDBName() { + return "desktopruntime:" + jdbcUri; + } + public boolean isNewFilesystem() { return newFilesystem; } @@ -172,7 +184,8 @@ public class JDBCFilesystem implements IFilesystemProvider { } } - public void shutdown() { + @Override + public void closeHandle() { shutdown0(); synchronized(jdbcFilesystems) { jdbcFilesystems.remove(this); @@ -438,4 +451,9 @@ public class JDBCFilesystem implements IFilesystemProvider { } } + @Override + public boolean isRamdisk() { + return false; + } + } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/JDBCFilesystemConverter.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/JDBCFilesystemConverter.java index 62d19b2d..eef8d922 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/JDBCFilesystemConverter.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/JDBCFilesystemConverter.java @@ -7,7 +7,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformFilesystem.IFilesystemProvider; +import net.lax1dude.eaglercraft.v1_8.internal.IEaglerFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; import net.lax1dude.eaglercraft.v1_8.internal.vfs2.EaglerFileSystemException; @@ -33,7 +33,7 @@ public class JDBCFilesystemConverter { private static final Logger logger = LogManager.getLogger("JDBCFilesystemConverter"); - public static void convertFilesystem(String title, File oldFS, IFilesystemProvider newFS, boolean deleteOld) { + public static void convertFilesystem(String title, File oldFS, IEaglerFilesystem newFS, boolean deleteOld) { FilesystemConvertingDialog progressDialog = new FilesystemConvertingDialog(title); try { progressDialog.setProgressIndeterminate(true); @@ -41,7 +41,7 @@ public class JDBCFilesystemConverter { progressDialog.setVisible(true); String slug = oldFS.getAbsolutePath(); - List filesToCopy = new ArrayList(); + List filesToCopy = new ArrayList<>(); logger.info("Discovering files to convert..."); iterateFolder(slug.length(), oldFS, filesToCopy); logger.info("Found {} files in the old directory", filesToCopy.size()); diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/LWJGLEntryPoint.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/LWJGLEntryPoint.java index 849e836c..23b13589 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/LWJGLEntryPoint.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/LWJGLEntryPoint.java @@ -6,7 +6,6 @@ import javax.swing.UnsupportedLookAndFeelException; import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EagUtils; import net.lax1dude.eaglercraft.v1_8.internal.EnumPlatformANGLE; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.PlatformInput; import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.program.ShaderSource; @@ -82,11 +81,12 @@ public class LWJGLEntryPoint { PlatformInput.setStartupFullscreen(true); }else if(args[i].equalsIgnoreCase("highp")) { ShaderSource.setHighP(true); - }else if(args[i].startsWith("jdbc:")) { - if(i < args.length - 1) { - PlatformFilesystem.setUseJDBC(args[i]); - PlatformFilesystem.setJDBCDriverClass(args[++i]); - } + }else if(args[i].equalsIgnoreCase("gles=200")) { + PlatformRuntime.requestGL(200); + }else if(args[i].equalsIgnoreCase("gles=300")) { + PlatformRuntime.requestGL(300); + }else if(args[i].equalsIgnoreCase("gles=310")) { + PlatformRuntime.requestGL(310); }else { EnumPlatformANGLE angle = EnumPlatformANGLE.fromId(args[i]); if(angle != EnumPlatformANGLE.DEFAULT) { diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/WebSocketPlayClient.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/WebSocketClientImpl.java similarity index 57% rename from sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/WebSocketPlayClient.java rename to sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/WebSocketClientImpl.java index 0e201823..feed7751 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/WebSocketPlayClient.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/lwjgl/WebSocketClientImpl.java @@ -1,4 +1,4 @@ -package net.lax1dude.eaglercraft.v1_8.internal; +package net.lax1dude.eaglercraft.v1_8.internal.lwjgl; import java.net.URI; import java.nio.ByteBuffer; @@ -9,11 +9,10 @@ import org.java_websocket.drafts.Draft_6455; import org.java_websocket.extensions.permessage_deflate.PerMessageDeflateExtension; import org.java_websocket.handshake.ServerHandshake; -import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; -import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.internal.EnumEaglerConnectionState; /** - * Copyright (c) 2022-2023 lax1dude, ayunami2000. All Rights Reserved. + * Copyright (c) 2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -27,51 +26,54 @@ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; * POSSIBILITY OF SUCH DAMAGE. * */ -class WebSocketPlayClient extends WebSocketClient { +class WebSocketClientImpl extends WebSocketClient { private static final Draft perMessageDeflateDraft = new Draft_6455(new PerMessageDeflateExtension()); - public static final Logger logger = LogManager.getLogger("WebSocket"); + protected final DesktopWebSocketClient clientObj; - WebSocketPlayClient(URI serverUri) { + WebSocketClientImpl(DesktopWebSocketClient clientObj, URI serverUri) { super(serverUri, perMessageDeflateDraft); + this.clientObj = clientObj; this.setConnectionLostTimeout(15); this.setTcpNoDelay(true); + this.connect(); } @Override public void onOpen(ServerHandshake arg0) { - PlatformNetworking.playConnectState = EnumEaglerConnectionState.CONNECTED; - PlatformNetworking.serverRateLimit = EnumServerRateLimit.OK; - logger.info("Connection opened: {}", this.uri.toString()); - } - - @Override - public void onClose(int arg0, String arg1, boolean arg2) { - logger.info("Connection closed: {}", this.uri.toString()); - } - - @Override - public void onError(Exception arg0) { - logger.error("Exception thrown by websocket \"" + this.getURI().toString() + "\"!"); - logger.error(arg0); - PlatformNetworking.playConnectState = EnumEaglerConnectionState.FAILED; - } - - @Override - public void onMessage(String arg0) { - if(arg0.equalsIgnoreCase("BLOCKED")) { - logger.error("Reached full IP ratelimit!"); - PlatformNetworking.serverRateLimit = EnumServerRateLimit.BLOCKED; - }else if(arg0.equalsIgnoreCase("LOCKED")) { - logger.error("Reached full IP ratelimit lockout!"); - PlatformNetworking.serverRateLimit = EnumServerRateLimit.LOCKED_OUT; + clientObj.playConnectState = EnumEaglerConnectionState.CONNECTED; + DesktopWebSocketClient.logger.info("Connection opened: {}", this.uri.toString()); + synchronized(clientObj.connectOpenMutex) { + clientObj.connectOpenMutex.notifyAll(); } } @Override - public void onMessage(ByteBuffer arg0) { - PlatformNetworking.recievedPlayPacket(arg0.array()); + public void onClose(int arg0, String arg1, boolean arg2) { + DesktopWebSocketClient.logger.info("Connection closed: {}", this.uri.toString()); + if(clientObj.playConnectState != EnumEaglerConnectionState.FAILED) { + clientObj.playConnectState = EnumEaglerConnectionState.CLOSED; + } } - + + @Override + public void onError(Exception arg0) { + DesktopWebSocketClient.logger.error("Exception thrown by websocket \"" + this.getURI().toString() + "\"!"); + DesktopWebSocketClient.logger.error(arg0); + if(clientObj.playConnectState == EnumEaglerConnectionState.CONNECTING) { + clientObj.playConnectState = EnumEaglerConnectionState.FAILED; + } + } + + @Override + public void onMessage(String arg0) { + clientObj.handleString(arg0); + } + + @Override + public void onMessage(ByteBuffer arg0) { + clientObj.handleBytes(arg0.array()); + } + } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/internal/ClientPlatformSingleplayer.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/internal/ClientPlatformSingleplayer.java index 380acb53..8981e803 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/internal/ClientPlatformSingleplayer.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/internal/ClientPlatformSingleplayer.java @@ -28,7 +28,7 @@ public class ClientPlatformSingleplayer { private static CrashScreenPopup crashOverlay = null; - public static void startIntegratedServer() { + public static void startIntegratedServer(boolean forceSingleThread) { DesktopIntegratedServer.startIntegratedServer(); } @@ -52,7 +52,7 @@ public class ClientPlatformSingleplayer { if(MemoryConnection.serverToClientQueue.size() == 0) { return null; }else { - List ret = new ArrayList(MemoryConnection.serverToClientQueue); + List ret = new ArrayList<>(MemoryConnection.serverToClientQueue); MemoryConnection.serverToClientQueue.clear(); return ret; } @@ -71,6 +71,14 @@ public class ClientPlatformSingleplayer { return false; } + public static boolean isSingleThreadModeSupported() { + return false; + } + + public static void updateSingleThreadMode() { + + } + public static void showCrashReportOverlay(String report, int x, int y, int w, int h) { if(crashOverlay == null) { crashOverlay = new CrashScreenPopup(); diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/server/internal/ServerPlatformSingleplayer.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/server/internal/ServerPlatformSingleplayer.java index 5c6af429..f1a54681 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/server/internal/ServerPlatformSingleplayer.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/server/internal/ServerPlatformSingleplayer.java @@ -2,10 +2,12 @@ package net.lax1dude.eaglercraft.v1_8.sp.server.internal; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; +import net.lax1dude.eaglercraft.v1_8.Filesystem; import net.lax1dude.eaglercraft.v1_8.internal.IClientConfigAdapter; +import net.lax1dude.eaglercraft.v1_8.internal.IEaglerFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.IPCPacketData; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.lwjgl.DesktopClientConfigAdapter; import net.lax1dude.eaglercraft.v1_8.sp.server.internal.lwjgl.MemoryConnection; @@ -26,8 +28,20 @@ import net.lax1dude.eaglercraft.v1_8.sp.server.internal.lwjgl.MemoryConnection; */ public class ServerPlatformSingleplayer { + private static IEaglerFilesystem filesystem = null; + public static void initializeContext() { - PlatformFilesystem.initialize(); + if(filesystem == null) { + filesystem = Filesystem.getHandleFor(getClientConfigAdapter().getWorldsDB()); + } + } + + public static void initializeContextSingleThread(Consumer packetSendCallback) { + throw new UnsupportedOperationException(); + } + + public static IEaglerFilesystem getWorldsDatabase() { + return filesystem; } public static void sendPacket(IPCPacketData packet) { @@ -50,7 +64,7 @@ public class ServerPlatformSingleplayer { if(MemoryConnection.clientToServerQueue.size() == 0) { return null; }else { - List ret = new ArrayList(MemoryConnection.clientToServerQueue); + List ret = new ArrayList<>(MemoryConnection.clientToServerQueue); MemoryConnection.clientToServerQueue.clear(); return ret; } @@ -60,4 +74,17 @@ public class ServerPlatformSingleplayer { public static IClientConfigAdapter getClientConfigAdapter() { return DesktopClientConfigAdapter.instance; } + + public static void immediateContinue() { + + } + + public static void platformShutdown() { + filesystem = null; + } + + public static boolean isSingleThreadMode() { + return false; + } + } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/server/internal/lwjgl/MemoryConnection.java b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/server/internal/lwjgl/MemoryConnection.java index 3398d3d0..e77091c9 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/server/internal/lwjgl/MemoryConnection.java +++ b/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/sp/server/internal/lwjgl/MemoryConnection.java @@ -22,7 +22,7 @@ import net.lax1dude.eaglercraft.v1_8.internal.IPCPacketData; */ public class MemoryConnection { - public static final List clientToServerQueue = new LinkedList(); - public static final List serverToClientQueue = new LinkedList(); + public static final List clientToServerQueue = new LinkedList<>(); + public static final List serverToClientQueue = new LinkedList<>(); } diff --git a/sources/main/java/com/google/common/base/CharMatcher.java b/sources/main/java/com/google/common/base/CharMatcher.java index e84449d4..97f9b3a4 100644 --- a/sources/main/java/com/google/common/base/CharMatcher.java +++ b/sources/main/java/com/google/common/base/CharMatcher.java @@ -140,9 +140,9 @@ public abstract class CharMatcher implements Predicate { } // Must be in ascending order. - private static final String ZEROES = "0\u0660\u06f0\u07c0\u0966\u09e6\u0a66\u0ae6\u0b66\u0be6" - + "\u0c66\u0ce6\u0d66\u0e50\u0ed0\u0f20\u1040\u1090\u17e0\u1810\u1946\u19d0\u1b50\u1bb0" - + "\u1c40\u1c50\ua620\ua8d0\ua900\uaa50\uff10"; + private static final String ZEROES = new String(new char[] { '0', 0x0660, 0x06f0, 0x07c0, 0x0966, 0x09e6, 0x0a66, + 0x0ae6, 0x0b66, 0x0be6, 0x0c66, 0x0ce6, 0x0d66, 0x0e50, 0x0ed0, 0x0f20, 0x1040, 0x1090, 0x17e0, 0x1810, + 0x1946, 0x19d0, 0x1b50, 0x1bb0, 0x1c40, 0x1c50, 0xa620, 0xa8d0, 0xa900, 0xaa50, 0xff10 }); private static final String NINES; static { @@ -234,10 +234,10 @@ public abstract class CharMatcher implements Predicate { * FORMAT, SURROGATE, and PRIVATE_USE according to ICU4J. */ public static final CharMatcher INVISIBLE = new RangesMatcher("CharMatcher.INVISIBLE", - ("\u0000\u007f\u00ad\u0600\u061c\u06dd\u070f\u1680\u180e\u2000\u2028\u205f\u2066\u2067\u2068" - + "\u2069\u206a\u3000\ud800\ufeff\ufff9\ufffa").toCharArray(), - ("\u0020\u00a0\u00ad\u0604\u061c\u06dd\u070f\u1680\u180e\u200f\u202f\u2064\u2066\u2067\u2068" - + "\u2069\u206f\u3000\uf8ff\ufeff\ufff9\ufffb").toCharArray()); + new char[] { 0x0000, 0x007f, 0x00ad, 0x0600, 0x061c, 0x06dd, 0x070f, 0x1680, 0x180e, 0x2000, 0x2028, 0x205f, + 0x2066, 0x2067, 0x2068, 0x2069, 0x206a, 0x3000, 0xd800, 0xfeff, 0xfff9, 0xfffa }, + new char[] { 0x0020, 0x00a0, 0x00ad, 0x0604, 0x061c, 0x06dd, 0x070f, 0x1680, 0x180e, 0x200f, 0x202f, 0x2064, + 0x2066, 0x2067, 0x2068, 0x2069, 0x206f, 0x3000, 0xf8ff, 0xfeff, 0xfff9, 0xfffb }); private static String showCharacter(char c) { String hex = "0123456789ABCDEF"; @@ -260,8 +260,8 @@ public abstract class CharMatcher implements Predicate { * keep it up to date. */ public static final CharMatcher SINGLE_WIDTH = new RangesMatcher("CharMatcher.SINGLE_WIDTH", - "\u0000\u05be\u05d0\u05f3\u0600\u0750\u0e00\u1e00\u2100\ufb50\ufe70\uff61".toCharArray(), - "\u04f9\u05be\u05ea\u05f4\u06ff\u077f\u0e7f\u20af\u213a\ufdff\ufeff\uffdc".toCharArray()); + new char[] { 0x0000, 0x05be, 0x05d0, 0x05f3, 0x0600, 0x0750, 0x0e00, 0x1e00, 0x2100, 0xfb50, 0xfe70, 0xff61 }, + new char[] { 0x04f9, 0x05be, 0x05ea, 0x05f4, 0x06ff, 0x077f, 0x0e7f, 0x20af, 0x213a, 0xfdff, 0xfeff, 0xffdc }); /** Matches any character. */ public static final CharMatcher ANY = new FastMatcher("CharMatcher.ANY") { @@ -1474,9 +1474,9 @@ public abstract class CharMatcher implements Predicate { return description; } - static final String WHITESPACE_TABLE = "" + "\u2002\u3000\r\u0085\u200A\u2005\u2000\u3000" - + "\u2029\u000B\u3000\u2008\u2003\u205F\u3000\u1680" + "\u0009\u0020\u2006\u2001\u202F\u00A0\u000C\u2009" - + "\u3000\u2004\u3000\u3000\u2028\n\u2007\u3000"; + static final String WHITESPACE_TABLE = new String(new char[] { 0x2002, 0x3000, '\r', 0x0085, 0x200A, 0x2005, 0x2000, + 0x3000, 0x2029, 0x000B, 0x3000, 0x2008, 0x2003, 0x205F, 0x3000, 0x1680, 0x0009, 0x0020, 0x2006, 0x2001, + 0x202F, 0x00A0, 0x000C, 0x2009, 0x3000, 0x2004, 0x3000, 0x3000, 0x2028, '\n', 0x2007, 0x3000 }); static final int WHITESPACE_MULTIPLIER = 1682554634; static final int WHITESPACE_SHIFT = Integer.numberOfLeadingZeros(WHITESPACE_TABLE.length() - 1); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Base64.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Base64.java index 7fcf7cf1..177b54ee 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Base64.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Base64.java @@ -116,6 +116,14 @@ public class Base64 extends BaseNCodec { 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 // 70-7a p-z }; + public static int lookupCharInt(char c) { + return c < 123 ? DECODE_TABLE[c] : -1; + } + + public static char lookupIntChar(int i) { + return (char)STANDARD_ENCODE_TABLE[i]; + } + /** * Base64 uses 6-bit fields. */ diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/ClientUUIDLoadingCache.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/ClientUUIDLoadingCache.java new file mode 100644 index 00000000..aa60cda4 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/ClientUUIDLoadingCache.java @@ -0,0 +1,152 @@ +package net.lax1dude.eaglercraft.v1_8; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.CPacketGetOtherClientUUIDV4EAG; +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.AbstractClientPlayer; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ClientUUIDLoadingCache { + + private static final Logger logger = LogManager.getLogger("ClientUUIDLoadingCache"); + + public static final EaglercraftUUID NULL_UUID = new EaglercraftUUID(0l, 0l); + public static final EaglercraftUUID PENDING_UUID = new EaglercraftUUID(0x6969696969696969l, 0x6969696969696969l); + public static final EaglercraftUUID VANILLA_UUID = new EaglercraftUUID(0x1DCE015CD384374El, 0x85030A4DE95E5736l); + + /** + * For client devs, allows you to get EaglercraftVersion.clientBrandUUID of + * other players on a server, to detect other players who also use your client. + * + * Requires EaglerXBungee 1.3.0 or EaglerXVelocity 1.1.0 + * + * @return NULL_UUID if not found, PENDING_UUID if pending, + * VANILLA_UUID if vanilla, or the remote player's + * client's EaglercraftVersion.clientBrandUUID + */ + public static EaglercraftUUID getPlayerClientBrandUUID(EntityPlayer player) { + EaglercraftUUID ret = null; + if(player instanceof AbstractClientPlayer) { + ret = ((AbstractClientPlayer)player).clientBrandUUIDCache; + if(ret == null) { + Minecraft mc = Minecraft.getMinecraft(); + if(mc != null && mc.thePlayer != null && mc.thePlayer.sendQueue.getEaglerMessageProtocol().ver >= 4) { + ret = PENDING_UUID; + EaglercraftUUID playerUUID = player.getUniqueID(); + if(!waitingUUIDs.containsKey(playerUUID) && !evictedUUIDs.containsKey(playerUUID)) { + int reqID = ++requestId & 0x3FFF; + WaitingLookup newLookup = new WaitingLookup(reqID, playerUUID, EagRuntime.steadyTimeMillis(), + (AbstractClientPlayer) player); + waitingIDs.put(reqID, newLookup); + waitingUUIDs.put(playerUUID, newLookup); + mc.thePlayer.sendQueue.sendEaglerMessage( + new CPacketGetOtherClientUUIDV4EAG(reqID, newLookup.uuid.msb, newLookup.uuid.lsb)); + } + } + } + }else if(player instanceof EntityPlayerMP) { + ret = ((EntityPlayerMP)player).clientBrandUUID; + } + if(ret == null) { + ret = NULL_UUID; + } + return ret; + } + + private static final Map waitingIDs = new HashMap<>(); + private static final Map waitingUUIDs = new HashMap<>(); + private static final Map evictedUUIDs = new HashMap<>(); + + private static int requestId = 0; + private static long lastFlushReq = EagRuntime.steadyTimeMillis(); + private static long lastFlushEvict = EagRuntime.steadyTimeMillis(); + + public static void update() { + long timestamp = EagRuntime.steadyTimeMillis(); + if(timestamp - lastFlushReq > 5000l) { + lastFlushReq = timestamp; + if(!waitingIDs.isEmpty()) { + Iterator itr = waitingIDs.values().iterator(); + while(itr.hasNext()) { + WaitingLookup lookup = itr.next(); + if(timestamp - lookup.timestamp > 15000l) { + itr.remove(); + waitingUUIDs.remove(lookup.uuid); + } + } + } + } + if(timestamp - lastFlushEvict > 1000l) { + lastFlushEvict = timestamp; + if(!evictedUUIDs.isEmpty()) { + Iterator evictItr = evictedUUIDs.values().iterator(); + while(evictItr.hasNext()) { + if(timestamp - evictItr.next().longValue() > 3000l) { + evictItr.remove(); + } + } + } + } + } + + public static void flushRequestCache() { + waitingIDs.clear(); + waitingUUIDs.clear(); + evictedUUIDs.clear(); + } + + public static void handleResponse(int requestId, EaglercraftUUID clientId) { + WaitingLookup lookup = waitingIDs.remove(requestId); + if(lookup != null) { + lookup.player.clientBrandUUIDCache = clientId; + waitingUUIDs.remove(lookup.uuid); + }else { + logger.warn("Unsolicited client brand UUID lookup response #{} recieved! (Brand UUID: {})", requestId, clientId); + } + } + + public static void evict(EaglercraftUUID clientId) { + evictedUUIDs.put(clientId, Long.valueOf(EagRuntime.steadyTimeMillis())); + WaitingLookup lk = waitingUUIDs.remove(clientId); + if(lk != null) { + waitingIDs.remove(lk.reqID); + } + } + + private static class WaitingLookup { + + private final int reqID; + private final EaglercraftUUID uuid; + private final long timestamp; + private final AbstractClientPlayer player; + + public WaitingLookup(int reqID, EaglercraftUUID uuid, long timestamp, AbstractClientPlayer player) { + this.reqID = reqID; + this.uuid = uuid; + this.timestamp = timestamp; + this.player = player; + } + + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Display.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Display.java index 956ae1ea..db9f1587 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Display.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Display.java @@ -20,6 +20,8 @@ import net.lax1dude.eaglercraft.v1_8.internal.PlatformInput; public class Display { private static long lastSwap = 0l; + private static long lastDPIUpdate = -250l; + private static float cacheDPI = 1.0f; public static int getWidth() { return PlatformInput.getWindowWidth(); @@ -29,6 +31,22 @@ public class Display { return PlatformInput.getWindowHeight(); } + public static int getVisualViewportX() { + return PlatformInput.getVisualViewportX(); + } + + public static int getVisualViewportY() { + return PlatformInput.getVisualViewportY(); + } + + public static int getVisualViewportW() { + return PlatformInput.getVisualViewportW(); + } + + public static int getVisualViewportH() { + return PlatformInput.getVisualViewportH(); + } + public static boolean isActive() { return PlatformInput.getWindowFocused(); } @@ -61,24 +79,32 @@ public class Display { boolean limitFPS = limitFramerate > 0 && limitFramerate < 1000; if(limitFPS) { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); long frameMillis = (1000l / limitFramerate) - (millis - lastSwap); if(frameMillis > 0l) { EagUtils.sleep(frameMillis); } } - lastSwap = System.currentTimeMillis(); + lastSwap = EagRuntime.steadyTimeMillis(); } public static boolean contextLost() { return PlatformInput.contextLost(); } - + public static boolean wasResized() { return PlatformInput.wasResized(); } + public static boolean wasVisualViewportResized() { + return PlatformInput.wasVisualViewportResized(); + } + + public static boolean supportsFullscreen() { + return PlatformInput.supportsFullscreen(); + } + public static boolean isFullscreen() { return PlatformInput.isFullscreen(); } @@ -87,4 +113,13 @@ public class Display { PlatformInput.toggleFullscreen(); } + public static float getDPI() { + long millis = EagRuntime.steadyTimeMillis(); + if(millis - lastDPIUpdate > 250l) { + lastDPIUpdate = millis; + cacheDPI = PlatformInput.getDPI(); + } + return cacheDPI; + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EagRuntime.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EagRuntime.java index e0ff3b01..9aeb060d 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EagRuntime.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EagRuntime.java @@ -3,17 +3,16 @@ package net.lax1dude.eaglercraft.v1_8; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.StringReader; +import java.io.InputStreamReader; import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer; import net.lax1dude.eaglercraft.v1_8.internal.buffer.IntBuffer; import java.nio.charset.StandardCharsets; -import java.text.DateFormat; import java.util.ArrayList; -import java.util.Calendar; import java.util.List; import java.util.function.Consumer; +import net.lax1dude.eaglercraft.v1_8.internal.EaglerMissingResourceException; import net.lax1dude.eaglercraft.v1_8.internal.EnumPlatformANGLE; import net.lax1dude.eaglercraft.v1_8.internal.EnumPlatformAgent; import net.lax1dude.eaglercraft.v1_8.internal.EnumPlatformOS; @@ -26,6 +25,7 @@ import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; +import net.lax1dude.eaglercraft.v1_8.recording.ScreenRecordingController; import net.lax1dude.eaglercraft.v1_8.update.UpdateService; /** @@ -70,6 +70,8 @@ public class EagRuntime { UpdateService.initialize(); EaglerXBungeeVersion.initialize(); EaglercraftGPU.warmUpCache(); + ScreenRecordingController.initialize(); + PlatformRuntime.postCreate(); } public static void destroy() { @@ -119,11 +121,23 @@ public class EagRuntime { public static void freeFloatBuffer(FloatBuffer byteBuffer) { PlatformRuntime.freeFloatBuffer(byteBuffer); } - + + public static boolean getResourceExists(String path) { + return PlatformAssets.getResourceExists(path); + } + public static byte[] getResourceBytes(String path) { return PlatformAssets.getResourceBytes(path); } - + + public static byte[] getRequiredResourceBytes(String path) { + byte[] ret = PlatformAssets.getResourceBytes(path); + if(ret == null) { + throw new EaglerMissingResourceException("Could not load required resource from EPK: " + path); + } + return ret; + } + public static InputStream getResourceStream(String path) { byte[] b = PlatformAssets.getResourceBytes(path); if(b != null) { @@ -132,24 +146,41 @@ public class EagRuntime { return null; } } - + + public static InputStream getRequiredResourceStream(String path) { + byte[] ret = PlatformAssets.getResourceBytes(path); + if(ret == null) { + throw new EaglerMissingResourceException("Could not load required resource from EPK: " + path); + } + return new EaglerInputStream(ret); + } + public static String getResourceString(String path) { byte[] bytes = PlatformAssets.getResourceBytes(path); return bytes != null ? new String(bytes, StandardCharsets.UTF_8) : null; } - + + public static String getRequiredResourceString(String path) { + byte[] ret = PlatformAssets.getResourceBytes(path); + if(ret == null) { + throw new EaglerMissingResourceException("Could not load required resource from EPK: " + path); + } + return new String(ret, StandardCharsets.UTF_8); + } + public static List getResourceLines(String path) { byte[] bytes = PlatformAssets.getResourceBytes(path); if(bytes != null) { - List ret = new ArrayList(); + List ret = new ArrayList<>(); try { - BufferedReader rd = new BufferedReader(new StringReader(path)); + BufferedReader rd = new BufferedReader(new InputStreamReader(new EaglerInputStream(bytes), StandardCharsets.UTF_8)); String s; while((s = rd.readLine()) != null) { ret.add(s); } }catch(IOException ex) { // ?? + return null; } return ret; }else { @@ -157,6 +188,14 @@ public class EagRuntime { } } + public static List getRequiredResourceLines(String path) { + List ret = getResourceLines(path); + if(ret == null) { + throw new EaglerMissingResourceException("Could not load required resource from EPK: " + path); + } + return ret; + } + public static void debugPrintStackTraceToSTDERR(Throwable t) { debugPrintStackTraceToSTDERR0("", t); Throwable c = t.getCause(); @@ -180,7 +219,7 @@ public class EagRuntime { } public static String[] getStackTraceElements(Throwable t) { - List lst = new ArrayList(); + List lst = new ArrayList<>(); PlatformRuntime.getStackTrace(t, (s) -> { lst.add(s); }); @@ -222,17 +261,23 @@ public class EagRuntime { PlatformRuntime.exit(); } + /** + * Note to skids: This doesn't do anything in javascript runtime! + */ public static long maxMemory() { return PlatformRuntime.maxMemory(); } /** - * Note to skids: This doesn't do anything in TeaVM runtime! + * Note to skids: This doesn't do anything in javascript runtime! */ public static long totalMemory() { return PlatformRuntime.totalMemory(); } + /** + * Note to skids: This doesn't do anything in javascript runtime! + */ public static long freeMemory() { return PlatformRuntime.freeMemory(); } @@ -289,18 +334,6 @@ public class EagRuntime { return PlatformRuntime.getClientConfigAdapter(); } - public static String getRecText() { - return PlatformRuntime.getRecText(); - } - - public static void toggleRec() { - PlatformRuntime.toggleRec(); - } - - public static boolean recSupported() { - return PlatformRuntime.recSupported(); - } - public static void openCreditsPopup(String text) { PlatformApplication.openCreditsPopup(text); } @@ -317,16 +350,24 @@ public class EagRuntime { PlatformApplication.showDebugConsole(); } - public static Calendar getLocaleCalendar() { - return Calendar.getInstance(); //TODO: fix teavm calendar's time zone offset - } - - public static T fixDateFormat(T input) { - input.setCalendar(getLocaleCalendar()); - return input; + public static void setDisplayBootMenuNextRefresh(boolean en) { + PlatformRuntime.setDisplayBootMenuNextRefresh(en); } public static void setMCServerWindowGlobal(String url) { PlatformApplication.setMCServerWindowGlobal(url); } + + public static long steadyTimeMillis() { + return PlatformRuntime.steadyTimeMillis(); + } + + public static long nanoTime() { + return PlatformRuntime.nanoTime(); + } + + public static void immediateContinue() { + PlatformRuntime.immediateContinue(); + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EagUtils.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EagUtils.java index 4246350c..808aa6c5 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EagUtils.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EagUtils.java @@ -1,5 +1,6 @@ package net.lax1dude.eaglercraft.v1_8; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; @@ -85,4 +86,12 @@ public class EagUtils { } } + public static EaglercraftUUID makeClientBrandUUID(String name) { + return EaglercraftUUID.nameUUIDFromBytes(("EaglercraftXClient:" + name).getBytes(StandardCharsets.UTF_8)); + } + + public static EaglercraftUUID makeClientBrandUUIDLegacy(String name) { + return EaglercraftUUID.nameUUIDFromBytes(("EaglercraftXClientOld:" + name).getBytes(StandardCharsets.UTF_8)); + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglerOutputStream.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglerOutputStream.java index a39c268f..3f4864bc 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglerOutputStream.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglerOutputStream.java @@ -74,6 +74,11 @@ public class EaglerOutputStream extends OutputStream { return count; } + public void skipBytes(int num) { + ensureCapacity(count + num); + count += num; + } + public void close() throws IOException { } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglerXBungeeVersion.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglerXBungeeVersion.java index 9a53c774..d1bd0fdf 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglerXBungeeVersion.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglerXBungeeVersion.java @@ -28,10 +28,7 @@ public class EaglerXBungeeVersion { private static String pluginFilename = null; public static void initialize() { - String pluginVersionJson = EagRuntime.getResourceString("plugin_version.json"); - if(pluginVersionJson == null) { - throw new RuntimeException("File \"plugin_version.json\" is missing in the epk!"); - } + String pluginVersionJson = EagRuntime.getRequiredResourceString("plugin_version.json"); JSONObject json = new JSONObject(pluginVersionJson); pluginName = json.getString("pluginName"); pluginVersion = json.getString("pluginVersion"); @@ -76,11 +73,7 @@ public class EaglerXBungeeVersion { } public static byte[] getPluginDownload() { - byte[] ret = EagRuntime.getResourceBytes(pluginFileEPK); - if(ret == null) { - throw new RuntimeException("File \"" + pluginFileEPK + "\" is missing in the epk!"); - } - return ret; + return EagRuntime.getRequiredResourceBytes(pluginFileEPK); } public static void startPluginDownload() { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftSoundManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftSoundManager.java index 98b96649..86fff434 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftSoundManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftSoundManager.java @@ -130,10 +130,10 @@ public class EaglercraftSoundManager { settings.getSoundLevel(SoundCategory.RECORDS), settings.getSoundLevel(SoundCategory.WEATHER), settings.getSoundLevel(SoundCategory.BLOCKS), settings.getSoundLevel(SoundCategory.MOBS), settings.getSoundLevel(SoundCategory.ANIMALS), settings.getSoundLevel(SoundCategory.PLAYERS), - settings.getSoundLevel(SoundCategory.AMBIENT), settings.getSoundLevel(SoundCategory.VOICE) + settings.getSoundLevel(SoundCategory.AMBIENT) }; - activeSounds = new LinkedList(); - queuedSounds = new LinkedList(); + activeSounds = new LinkedList<>(); + queuedSounds = new LinkedList<>(); } public void unloadSoundSystem() { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftUUID.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftUUID.java index d3d771d4..8e75d8bb 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftUUID.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftUUID.java @@ -21,6 +21,8 @@ public class EaglercraftUUID implements Comparable { public final long msb; public final long lsb; + private int hash = 0; + private boolean hasHash; public EaglercraftUUID(long msb, long lsb) { this.msb = msb; @@ -125,13 +127,21 @@ public class EaglercraftUUID implements Comparable { @Override public int hashCode() { - long hilo = msb ^ lsb; - return ((int) (hilo >> 32)) ^ (int) hilo; + if(hash == 0 && !hasHash) { + long hilo = msb ^ lsb; + hash = ((int) (hilo >> 32)) ^ (int) hilo; + hasHash = true; + } + return hash; } @Override public boolean equals(Object o) { - return (o instanceof EaglercraftUUID) && ((EaglercraftUUID) o).lsb == lsb && ((EaglercraftUUID) o).msb == msb; + if(!(o instanceof EaglercraftUUID)) return false; + EaglercraftUUID oo = (EaglercraftUUID)o; + return (hasHash && oo.hasHash) + ? (hash == oo.hash && msb == oo.msb && lsb == oo.lsb) + : (msb == oo.msb && lsb == oo.lsb); } public long getMostSignificantBits() { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftVersion.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftVersion.java index d09b0400..2dae9373 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftVersion.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/EaglercraftVersion.java @@ -10,7 +10,7 @@ public class EaglercraftVersion { /// Customize these to fit your fork: public static final String projectForkName = "EaglercraftX"; - public static final String projectForkVersion = "u36"; + public static final String projectForkVersion = "u37"; public static final String projectForkVendor = "lax1dude"; public static final String projectForkURL = "https://gitlab.com/lax1dude/eaglercraftx-1.8"; @@ -20,7 +20,7 @@ public class EaglercraftVersion { public static final String projectOriginName = "EaglercraftX"; public static final String projectOriginAuthor = "lax1dude"; public static final String projectOriginRevision = "1.8"; - public static final String projectOriginVersion = "u36"; + public static final String projectOriginVersion = "u37"; public static final String projectOriginURL = "https://gitlab.com/lax1dude/eaglercraftx-1.8"; // rest in peace @@ -31,7 +31,7 @@ public class EaglercraftVersion { public static final boolean enableUpdateService = true; public static final String updateBundlePackageName = "net.lax1dude.eaglercraft.v1_8.client"; - public static final int updateBundlePackageVersionInt = 36; + public static final int updateBundlePackageVersionInt = 37; public static final String updateLatestLocalStorageKey = "latestUpdate_" + updateBundlePackageName; @@ -40,6 +40,13 @@ public class EaglercraftVersion { + // Client brand identification system configuration + + public static final EaglercraftUUID clientBrandUUID = EagUtils.makeClientBrandUUID(projectForkName); + + public static final EaglercraftUUID legacyClientUUIDInSharedWorld = EagUtils.makeClientBrandUUIDLegacy(projectOriginName); + + // Miscellaneous variables: public static final String mainMenuStringA = "Minecraft 1.8.8"; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Filesystem.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Filesystem.java new file mode 100644 index 00000000..2755d2d1 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Filesystem.java @@ -0,0 +1,158 @@ +package net.lax1dude.eaglercraft.v1_8; + +import java.util.HashMap; +import java.util.Map; + +import net.lax1dude.eaglercraft.v1_8.internal.IEaglerFilesystem; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformFilesystem; +import net.lax1dude.eaglercraft.v1_8.internal.RamdiskFilesystemImpl; +import net.lax1dude.eaglercraft.v1_8.internal.VFSFilenameIterator; +import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class Filesystem { + + private static final Logger logger = LogManager.getLogger("PlatformFilesystem"); + + private static final Map openFilesystems = new HashMap<>(); + + public static IEaglerFilesystem getHandleFor(String dbName) { + FilesystemHandle handle = openFilesystems.get(dbName); + if(handle != null) { + ++handle.refCount; + return new FilesystemHandleWrapper(handle); + } + IEaglerFilesystem handleImpl = null; + if(!EagRuntime.getConfiguration().isRamdiskMode()) { + handleImpl = PlatformFilesystem.initializePersist(dbName); + } + if(handleImpl == null) { + handleImpl = new RamdiskFilesystemImpl(dbName); + } + if(handleImpl.isRamdisk()) { + logger.warn("Using RAMDisk filesystem for database \"{}\", data will not be saved to local storage!", dbName); + } + handle = new FilesystemHandle(handleImpl); + openFilesystems.put(dbName, handle); + return new FilesystemHandleWrapper(handle); + } + + public static void closeAllHandles() { + for(FilesystemHandle handle : openFilesystems.values()) { + handle.refCount = 0; + handle.handle.closeHandle(); + } + openFilesystems.clear(); + } + + private static class FilesystemHandle { + + private final IEaglerFilesystem handle; + private int refCount; + + private FilesystemHandle(IEaglerFilesystem handle) { + this.handle = handle; + this.refCount = 1; + } + + } + + private static class FilesystemHandleWrapper implements IEaglerFilesystem { + + private final FilesystemHandle handle; + private final IEaglerFilesystem handleImpl; + private boolean closed; + + private FilesystemHandleWrapper(FilesystemHandle handle) { + this.handle = handle; + this.handleImpl = handle.handle; + this.closed = false; + } + + @Override + public String getFilesystemName() { + return handleImpl.getFilesystemName(); + } + + @Override + public String getInternalDBName() { + return handleImpl.getInternalDBName(); + } + + @Override + public boolean isRamdisk() { + return handleImpl.isRamdisk(); + } + + @Override + public boolean eaglerDelete(String pathName) { + return handleImpl.eaglerDelete(pathName); + } + + @Override + public ByteBuffer eaglerRead(String pathName) { + return handleImpl.eaglerRead(pathName); + } + + @Override + public void eaglerWrite(String pathName, ByteBuffer data) { + handleImpl.eaglerWrite(pathName, data); + } + + @Override + public boolean eaglerExists(String pathName) { + return handleImpl.eaglerExists(pathName); + } + + @Override + public boolean eaglerMove(String pathNameOld, String pathNameNew) { + return handleImpl.eaglerMove(pathNameOld, pathNameNew); + } + + @Override + public int eaglerCopy(String pathNameOld, String pathNameNew) { + return handleImpl.eaglerCopy(pathNameOld, pathNameNew); + } + + @Override + public int eaglerSize(String pathName) { + return handleImpl.eaglerSize(pathName); + } + + @Override + public void eaglerIterate(String pathName, VFSFilenameIterator itr, boolean recursive) { + handleImpl.eaglerIterate(pathName, itr, recursive); + } + + @Override + public void closeHandle() { + if(!closed && handle.refCount > 0) { + closed = true; + --handle.refCount; + if(handle.refCount <= 0) { + logger.info("Releasing filesystem handle for: \"{}\"", handleImpl.getFilesystemName()); + handleImpl.closeHandle(); + openFilesystems.remove(handleImpl.getFilesystemName()); + } + } + } + + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Gamepad.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Gamepad.java new file mode 100644 index 00000000..2c90683c --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Gamepad.java @@ -0,0 +1,115 @@ +package net.lax1dude.eaglercraft.v1_8; + +import java.util.LinkedList; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.internal.GamepadConstants; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformInput; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class Gamepad { + + private static final boolean[] buttonsLastState = new boolean[24]; + private static final List buttonEvents = new LinkedList<>(); + private static VirtualButtonEvent currentVEvent = null; + + private static class VirtualButtonEvent { + + private final int button; + private final boolean state; + + public VirtualButtonEvent(int button, boolean state) { + this.button = button; + this.state = state; + } + + } + + public static int getValidDeviceCount() { + return PlatformInput.gamepadGetValidDeviceCount(); + } + + public static String getDeviceName(int deviceId) { + return PlatformInput.gamepadGetDeviceName(deviceId); + } + + public static void setSelectedDevice(int deviceId) { + PlatformInput.gamepadSetSelectedDevice(deviceId); + } + + public static void update() { + PlatformInput.gamepadUpdate(); + if(isValid()) { + for(int i = 0; i < buttonsLastState.length; ++i) { + boolean b = PlatformInput.gamepadGetButtonState(i); + if(b != buttonsLastState[i]) { + buttonsLastState[i] = b; + buttonEvents.add(new VirtualButtonEvent(i, b)); + if(buttonEvents.size() > 64) { + buttonEvents.remove(0); + } + } + } + }else { + for(int i = 0; i < buttonsLastState.length; ++i) { + buttonsLastState[i] = false; + } + } + } + + public static boolean next() { + currentVEvent = null; + return !buttonEvents.isEmpty() && (currentVEvent = buttonEvents.remove(0)) != null; + } + + public static int getEventButton() { + return currentVEvent != null ? currentVEvent.button : -1; + } + + public static boolean getEventButtonState() { + return currentVEvent != null ? currentVEvent.state : false; + } + + public static boolean isValid() { + return PlatformInput.gamepadIsValid(); + } + + public static String getName() { + return PlatformInput.gamepadGetName(); + } + + public static boolean getButtonState(int button) { + return PlatformInput.gamepadGetButtonState(button); + } + + public static String getButtonName(int button) { + return GamepadConstants.getButtonName(button); + } + + public static float getAxis(int axis) { + return PlatformInput.gamepadGetAxis(axis); + } + + public static String getAxisName(int button) { + return GamepadConstants.getAxisName(button); + } + + public static void clearEventBuffer() { + buttonEvents.clear(); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/HashKey.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/HashKey.java new file mode 100644 index 00000000..75cd5582 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/HashKey.java @@ -0,0 +1,54 @@ +package net.lax1dude.eaglercraft.v1_8; + +import java.util.Arrays; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class HashKey { + + public final byte[] key; + public final int hash; + + public HashKey(byte[] key, int hashCode) { + this.key = key; + this.hash = hashCode; + } + + public HashKey(byte[] key) { + this.key = key; + this.hash = Arrays.hashCode(key); + } + + public Object clone() { + return new HashKey(key, hash); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null || !(obj instanceof HashKey)) + return false; + HashKey other = (HashKey) obj; + return hash == other.hash && Arrays.equals(key, other.key); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/IOUtils.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/IOUtils.java index e42561fe..83655734 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/IOUtils.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/IOUtils.java @@ -32,7 +32,7 @@ public class IOUtils { return Arrays.asList( new String(((EaglerInputStream) parInputStream).getAsArray(), charset).split("(\\r\\n|\\n|\\r)")); }else { - List ret = new ArrayList(); + List ret = new ArrayList<>(); try(InputStream is = parInputStream) { BufferedReader rd = new BufferedReader(new InputStreamReader(is, charset)); String s; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Keyboard.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Keyboard.java index c93c53f9..9355a7f0 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Keyboard.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Keyboard.java @@ -1,5 +1,6 @@ package net.lax1dude.eaglercraft.v1_8; +import net.lax1dude.eaglercraft.v1_8.internal.EnumFireKeyboardEvent; import net.lax1dude.eaglercraft.v1_8.internal.KeyboardConstants; import net.lax1dude.eaglercraft.v1_8.internal.PlatformInput; @@ -60,4 +61,8 @@ public class Keyboard { return PlatformInput.keyboardIsRepeatEvent(); } + public static void fireEvent(EnumFireKeyboardEvent eventType, int eagKey, char keyChar) { + PlatformInput.keyboardFireEvent(eventType, eagKey, keyChar); + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Mouse.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Mouse.java index 9f1f3f98..da236bac 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Mouse.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Mouse.java @@ -1,6 +1,7 @@ package net.lax1dude.eaglercraft.v1_8; import net.lax1dude.eaglercraft.v1_8.internal.EnumCursorType; +import net.lax1dude.eaglercraft.v1_8.internal.EnumFireMouseEvent; import net.lax1dude.eaglercraft.v1_8.internal.PlatformInput; /** @@ -92,6 +93,22 @@ public class Mouse { return PlatformInput.isMouseGrabbed(); } + public static boolean isMouseGrabSupported() { + return PlatformInput.mouseGrabSupported(); + } + + public static void fireMoveEvent(EnumFireMouseEvent eventType, int posX, int posY) { + PlatformInput.mouseFireMoveEvent(eventType, posX, posY); + } + + public static void fireButtonEvent(EnumFireMouseEvent eventType, int posX, int posY, int button) { + PlatformInput.mouseFireButtonEvent(eventType, posX, posY, button); + } + + public static void fireWheelEvent(EnumFireMouseEvent eventType, int posX, int posY, float wheel) { + PlatformInput.mouseFireWheelEvent(eventType, posX, posY, wheel); + } + private static int customCursorCounter = 0; private static EnumCursorType currentCursorType = EnumCursorType.DEFAULT; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/PauseMenuCustomizeState.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/PauseMenuCustomizeState.java new file mode 100644 index 00000000..3dc81252 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/PauseMenuCustomizeState.java @@ -0,0 +1,257 @@ +package net.lax1dude.eaglercraft.v1_8; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.opengl.ImageData; +import net.lax1dude.eaglercraft.v1_8.profile.EaglerSkinTexture; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketCustomizePauseMenuV4EAG; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.util.PacketImageData; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.util.ResourceLocation; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class PauseMenuCustomizeState { + + private static final Logger logger = LogManager.getLogger("PauseMenuCustomizeState"); + + public static ResourceLocation icon_title_L; + public static float icon_title_L_aspect = 1.0f; + public static ResourceLocation icon_title_R; + public static float icon_title_R_aspect = 1.0f; + public static ResourceLocation icon_backToGame_L; + public static float icon_backToGame_L_aspect = 1.0f; + public static ResourceLocation icon_backToGame_R; + public static float icon_backToGame_R_aspect = 1.0f; + public static ResourceLocation icon_achievements_L; + public static float icon_achievements_L_aspect = 1.0f; + public static ResourceLocation icon_achievements_R; + public static float icon_achievements_R_aspect = 1.0f; + public static ResourceLocation icon_statistics_L; + public static float icon_statistics_L_aspect = 1.0f; + public static ResourceLocation icon_statistics_R; + public static float icon_statistics_R_aspect = 1.0f; + public static ResourceLocation icon_serverInfo_L; + public static float icon_serverInfo_L_aspect = 1.0f; + public static ResourceLocation icon_serverInfo_R; + public static float icon_serverInfo_R_aspect = 1.0f; + public static ResourceLocation icon_options_L; + public static float icon_options_L_aspect = 1.0f; + public static ResourceLocation icon_options_R; + public static float icon_options_R_aspect = 1.0f; + public static ResourceLocation icon_discord_L; + public static float icon_discord_L_aspect = 1.0f; + public static ResourceLocation icon_discord_R; + public static float icon_discord_R_aspect = 1.0f; + public static ResourceLocation icon_disconnect_L; + public static float icon_disconnect_L_aspect = 1.0f; + public static ResourceLocation icon_disconnect_R; + public static float icon_disconnect_R_aspect = 1.0f; + public static ResourceLocation icon_background_pause; + public static float icon_background_pause_aspect = 1.0f; + public static ResourceLocation icon_background_all; + public static float icon_background_all_aspect = 1.0f; + public static ResourceLocation icon_watermark_pause; + public static float icon_watermark_pause_aspect = 1.0f; + public static ResourceLocation icon_watermark_all; + public static float icon_watermark_all_aspect = 1.0f; + + public static final int SERVER_INFO_MODE_NONE = SPacketCustomizePauseMenuV4EAG.SERVER_INFO_MODE_NONE; + public static final int SERVER_INFO_MODE_EXTERNAL_URL = SPacketCustomizePauseMenuV4EAG.SERVER_INFO_MODE_EXTERNAL_URL; + public static final int SERVER_INFO_MODE_SHOW_EMBED_OVER_HTTP = SPacketCustomizePauseMenuV4EAG.SERVER_INFO_MODE_SHOW_EMBED_OVER_HTTP; + public static final int SERVER_INFO_MODE_SHOW_EMBED_OVER_WS = SPacketCustomizePauseMenuV4EAG.SERVER_INFO_MODE_SHOW_EMBED_OVER_WS; + + public static final int SERVER_INFO_EMBED_PERMS_JAVASCRIPT = SPacketCustomizePauseMenuV4EAG.SERVER_INFO_EMBED_PERMS_JAVASCRIPT; + public static final int SERVER_INFO_EMBED_PERMS_MESSAGE_API = SPacketCustomizePauseMenuV4EAG.SERVER_INFO_EMBED_PERMS_MESSAGE_API; + public static final int SERVER_INFO_EMBED_PERMS_STRICT_CSP = SPacketCustomizePauseMenuV4EAG.SERVER_INFO_EMBED_PERMS_STRICT_CSP; + + public static final int DISCORD_MODE_NONE = SPacketCustomizePauseMenuV4EAG.DISCORD_MODE_NONE; + public static final int DISCORD_MODE_INVITE_URL = SPacketCustomizePauseMenuV4EAG.DISCORD_MODE_INVITE_URL; + + public static int serverInfoMode; + public static int serverInfoEmbedPerms = SERVER_INFO_EMBED_PERMS_STRICT_CSP; + public static String serverInfoEmbedTitle; + public static String serverInfoButtonText; + public static String serverInfoURL; + public static byte[] serverInfoHash; + + public static int discordButtonMode; + public static String discordButtonText; + public static String discordInviteURL; + + private static final List toFree = new ArrayList<>(); + + private static int textureId = 0; + + private static class PauseMenuSprite { + + private final ResourceLocation loc; + private final EaglerSkinTexture tex; + + public PauseMenuSprite(EaglerSkinTexture tex) { + this.loc = newLoc(); + this.tex = tex; + } + + } + + public static void loadPacket(SPacketCustomizePauseMenuV4EAG packet) { + reset(); + + serverInfoMode = packet.serverInfoMode; + switch(packet.serverInfoMode) { + case SERVER_INFO_MODE_NONE: + default: + serverInfoButtonText = null; + serverInfoURL = null; + serverInfoHash = null; + break; + case SERVER_INFO_MODE_EXTERNAL_URL: + serverInfoButtonText = packet.serverInfoButtonText; + serverInfoURL = packet.serverInfoURL; + break; + case SERVER_INFO_MODE_SHOW_EMBED_OVER_HTTP: + serverInfoButtonText = packet.serverInfoButtonText; + serverInfoEmbedPerms = packet.serverInfoEmbedPerms; + serverInfoURL = packet.serverInfoURL; + serverInfoEmbedTitle = packet.serverInfoEmbedTitle; + break; + case SERVER_INFO_MODE_SHOW_EMBED_OVER_WS: + serverInfoButtonText = packet.serverInfoButtonText; + serverInfoEmbedPerms = packet.serverInfoEmbedPerms; + serverInfoHash = packet.serverInfoHash; + serverInfoEmbedTitle = packet.serverInfoEmbedTitle; + break; + } + + discordButtonMode = packet.discordButtonMode; + switch(packet.discordButtonMode) { + case DISCORD_MODE_NONE: + default: + discordButtonText = null; + serverInfoURL = null; + serverInfoHash = null; + break; + case DISCORD_MODE_INVITE_URL: + discordButtonText = packet.discordButtonText; + discordInviteURL = packet.discordInviteURL; + break; + } + + if(packet.imageMappings != null) { + Map spriteCache = new HashMap<>(); + icon_title_L = cacheLoadHelperFunction("icon_title_L", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_title_L_aspect = a); + icon_title_R = cacheLoadHelperFunction("icon_title_R", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_title_R_aspect = a); + icon_backToGame_L = cacheLoadHelperFunction("icon_backToGame_L", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_backToGame_L_aspect = a); + icon_backToGame_R = cacheLoadHelperFunction("icon_backToGame_R", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_backToGame_R_aspect = a); + icon_achievements_L = cacheLoadHelperFunction("icon_achievements_L", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_achievements_L_aspect = a); + icon_achievements_R = cacheLoadHelperFunction("icon_achievements_R", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_achievements_R_aspect = a); + icon_statistics_L = cacheLoadHelperFunction("icon_statistics_L", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_statistics_L_aspect = a); + icon_statistics_R = cacheLoadHelperFunction("icon_statistics_R", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_statistics_R_aspect = a); + icon_serverInfo_L = cacheLoadHelperFunction("icon_serverInfo_L", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_serverInfo_L_aspect = a); + icon_serverInfo_R = cacheLoadHelperFunction("icon_serverInfo_R", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_serverInfo_R_aspect = a); + icon_options_L = cacheLoadHelperFunction("icon_options_L", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_options_L_aspect = a); + icon_options_R = cacheLoadHelperFunction("icon_options_R", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_options_R_aspect = a); + icon_discord_L = cacheLoadHelperFunction("icon_discord_L", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_discord_L_aspect = a); + icon_discord_R = cacheLoadHelperFunction("icon_discord_R", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_discord_R_aspect = a); + icon_disconnect_L = cacheLoadHelperFunction("icon_disconnect_L", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_disconnect_L_aspect = a); + icon_disconnect_R = cacheLoadHelperFunction("icon_disconnect_R", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_disconnect_R_aspect = a); + icon_background_pause = cacheLoadHelperFunction("icon_background_pause", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_background_pause_aspect = a); + icon_background_all = cacheLoadHelperFunction("icon_background_all", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_background_all_aspect = a); + icon_watermark_pause = cacheLoadHelperFunction("icon_watermark_pause", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_watermark_pause_aspect = a); + icon_watermark_all = cacheLoadHelperFunction("icon_watermark_all", packet.imageMappings, spriteCache, packet.imageData, (a) -> icon_watermark_all_aspect = a); + } + } + + private static ResourceLocation cacheLoadHelperFunction(String name, Map lookup, + Map spriteCache, + List sourceData, Consumer aspectCB) { + Integer i = lookup.get(name); + if(i == null) { + return null; + } + PauseMenuSprite ret = spriteCache.get(i); + if(ret != null) { + if(name.startsWith("icon_background_") && ImageData.isNPOTStatic(ret.tex.getWidth(), ret.tex.getHeight())) { + logger.warn("An NPOT (non-power-of-two) texture was used for \"{}\", this texture's width and height must be powers of two for this texture to display properly on all hardware"); + } + aspectCB.accept((float)ret.tex.getWidth() / ret.tex.getHeight()); + return ret.loc; + } + int ii = i.intValue(); + if(ii < 0 || ii >= sourceData.size()) { + return null; + } + PacketImageData data = sourceData.get(ii); + ret = new PauseMenuSprite(new EaglerSkinTexture(ImageData.swapRB(data.rgba), data.width, data.height)); + Minecraft.getMinecraft().getTextureManager().loadTexture(ret.loc, ret.tex); + spriteCache.put(i, ret); + toFree.add(ret); + aspectCB.accept((float)data.width / data.height); + return ret.loc; + } + + private static ResourceLocation newLoc() { + return new ResourceLocation("eagler:gui/server/custom_pause_menu/tex_" + textureId++); + } + + public static void reset() { + icon_title_L = icon_title_R = null; + icon_backToGame_L = icon_backToGame_R = null; + icon_achievements_L = icon_achievements_R = null; + icon_statistics_L = icon_statistics_R = null; + icon_serverInfo_L = icon_serverInfo_R = null; + icon_options_L = icon_options_R = null; + icon_discord_L = icon_discord_R = null; + icon_disconnect_L = icon_disconnect_R = null; + icon_background_pause = icon_background_all = null; + icon_watermark_pause = icon_watermark_all = null; + icon_title_L_aspect = icon_title_R_aspect = 1.0f; + icon_backToGame_L_aspect = icon_backToGame_R_aspect = 1.0f; + icon_achievements_L_aspect = icon_achievements_R_aspect = 1.0f; + icon_statistics_L_aspect = icon_statistics_R_aspect = 1.0f; + icon_serverInfo_L_aspect = icon_serverInfo_R_aspect = 1.0f; + icon_options_L_aspect = icon_options_R_aspect = 1.0f; + icon_discord_L_aspect = icon_discord_R_aspect = 1.0f; + icon_disconnect_L_aspect = icon_disconnect_R_aspect = 1.0f; + icon_background_pause_aspect = icon_background_all_aspect = 1.0f; + icon_watermark_pause_aspect = icon_watermark_all_aspect = 1.0f; + serverInfoMode = 0; + serverInfoEmbedPerms = SERVER_INFO_EMBED_PERMS_STRICT_CSP; + serverInfoButtonText = null; + serverInfoURL = null; + serverInfoHash = null; + serverInfoEmbedTitle = null; + discordButtonMode = 0; + discordButtonText = null; + discordInviteURL = null; + if(!toFree.isEmpty()) { + TextureManager mgr = Minecraft.getMinecraft().getTextureManager(); + for(PauseMenuSprite rc : toFree) { + mgr.deleteTexture(rc.loc); + } + toFree.clear(); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/PointerInputAbstraction.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/PointerInputAbstraction.java new file mode 100644 index 00000000..c3ed1ca2 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/PointerInputAbstraction.java @@ -0,0 +1,227 @@ +package net.lax1dude.eaglercraft.v1_8; + +import net.lax1dude.eaglercraft.v1_8.touch_gui.TouchControls; +import net.minecraft.client.Minecraft; + +/** + * Copyright (c) 2024 lax1dude, ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class PointerInputAbstraction { + + protected static Minecraft mc; + protected static int oldMX = -1; + protected static int oldMY = -1; + protected static int oldTX = -1; + protected static int oldTY = -1; + protected static int dragStartX = -1; + protected static int dragStartY = -1; + protected static int dragStepStartX = -1; + protected static int dragStepStartY = -1; + protected static int dragUID = -1; + + protected static int cursorX = -1; + protected static int cursorY = -1; + protected static int cursorDX = 0; + protected static int cursorDY = 0; + protected static boolean touchingScreen = false; + protected static boolean touchingScreenNotButton = false; + protected static boolean draggingNotTouching = false; + protected static boolean touchMode = false; + + public static void init(Minecraft mcIn) { + mc = mcIn; + oldMX = -1; + oldMY = -1; + oldTX = -1; + oldTY = -1; + dragStartX = -1; + dragStartY = -1; + dragStepStartX = -1; + dragStepStartY = -1; + dragUID = -1; + cursorX = -1; + cursorY = -1; + cursorDX = 0; + cursorDY = 0; + touchingScreen = false; + touchingScreenNotButton = false; + draggingNotTouching = false; + touchMode = !mcIn.mouseGrabSupported; + } + + public static void runGameLoop() { + if(touchMode) { + runTouchUpdate(); + }else { + oldTX = -1; + oldTY = -1; + cursorX = oldMX = Mouse.getX(); + cursorY = oldMY = Mouse.getY(); + cursorDX += Mouse.getDX(); + cursorDY += Mouse.getDY(); + } + } + + private static void runTouchUpdate() { + int tc = Touch.touchPointCount(); + if (tc > 0) { + TouchControls.update(true); + touchingScreen = true; + for(int i = 0; i < tc; ++i) { + int uid = Touch.touchPointUID(i); + if(TouchControls.touchControls.containsKey(uid)) { + continue; + } + int tx = Touch.touchPointX(i); + int ty = Touch.touchPointY(i); + if(TouchControls.overlappingControl(tx, ty) != null) { + continue; + } + if(mc.currentScreen == null && mc.ingameGUI.isTouchOverlapEagler(uid, tx, ty)) { + continue; + } + cursorX = oldTX = tx; + cursorY = oldTY = ty; + oldMX = Mouse.getX(); + oldMY = Mouse.getY(); + touchingScreenNotButton = true; + runTouchDeltaUpdate(uid); + return; + } + touchingScreenNotButton = false; + } else { + TouchControls.update(false); + touchingScreen = false; + touchingScreenNotButton = false; + dragStepStartX = -1; + dragStepStartY = -1; + dragStartX = -1; + dragStartY = -1; + dragUID = -1; + final int tmp = Mouse.getX(); + final int tmp2 = Mouse.getY(); + if(oldTX == -1 || oldTY == -1) { + cursorX = oldMX = tmp; + cursorY = oldMY = tmp2; + cursorDX += Mouse.getDX(); + cursorDY += Mouse.getDY(); + return; + } + if (oldMX == -1 || oldMY == -1) { + oldMX = tmp; + oldMY = tmp2; + } + if (oldMX == tmp && oldMY == tmp2) { + cursorX = oldTX; + cursorY = oldTY; + }else { + cursorX = oldMX = tmp; + cursorY = oldMY = tmp2; + cursorDX += Mouse.getDX(); + cursorDY += Mouse.getDY(); + } + } + } + + private static void runTouchDeltaUpdate(int uid) { + if(uid != dragUID) { + dragStartX = oldTX; + dragStartY = oldTY; + dragStepStartX = -1; + dragStepStartY = -1; + dragUID = uid; + draggingNotTouching = false; + return; + } + if(dragStepStartX != -1) { + cursorDX += oldTX - dragStepStartX; + } + dragStepStartX = oldTX; + if(dragStepStartY != -1) { + cursorDY += oldTY - dragStepStartY; + } + dragStepStartY = oldTY; + if(dragStartX != -1 && dragStartY != -1) { + int dx = oldTX - dragStartX; + int dy = oldTY - dragStartY; + int len = dx * dx + dy * dy; + int dm = Math.max((int)(6 * Display.getDPI()), 2); + if(len > dm * dm) { + draggingNotTouching = true; + } + } + } + + public static boolean isTouchMode() { + return touchMode; + } + + public static boolean isTouchingScreen() { + return touchingScreen; + } + + public static boolean isTouchingScreenNotButton() { + return touchingScreenNotButton; + } + + public static boolean isDraggingNotTouching() { + return draggingNotTouching; + } + + public static void enterTouchModeHook() { + if(!touchMode) { + touchMode = true; + if(mc.mouseGrabSupported) { + mc.mouseHelper.ungrabMouseCursor(); + } + } + } + + public static void enterMouseModeHook() { + if(touchMode) { + touchMode = false; + touchingScreen = false; + touchingScreenNotButton = false; + if(mc.inGameHasFocus && mc.mouseGrabSupported) { + mc.mouseHelper.grabMouseCursor(); + } + } + } + + public static int getVCursorX() { + return cursorX; + } + + public static int getVCursorY() { + return cursorY; + } + + public static int getVCursorDX() { + int tmp = cursorDX; + cursorDX = 0; + return tmp; + } + + public static int getVCursorDY() { + int tmp = cursorDY; + cursorDY = 0; + return tmp; + } + + public static boolean getVCursorButtonDown(int bt) { + return (touchingScreenNotButton && bt == 0) || Mouse.isButtonDown(bt); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/Touch.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Touch.java new file mode 100644 index 00000000..eb27322d --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/Touch.java @@ -0,0 +1,134 @@ +package net.lax1dude.eaglercraft.v1_8; + +import net.lax1dude.eaglercraft.v1_8.internal.EnumTouchEvent; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformInput; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class Touch { + + public static boolean next() { + return PlatformInput.touchNext(); + } + + public static EnumTouchEvent getEventType() { + return PlatformInput.touchGetEventType(); + } + + public static int getEventTouchPointCount() { + return PlatformInput.touchGetEventTouchPointCount(); + } + + public static int getEventTouchX(int pointId) { + return PlatformInput.touchGetEventTouchX(pointId); + } + + public static int getEventTouchY(int pointId) { + return PlatformInput.touchGetEventTouchY(pointId); + } + + public static float getEventTouchRadiusX(int pointId) { + return PlatformInput.touchGetEventTouchRadiusX(pointId); + } + + public static float getEventTouchRadiusY(int pointId) { + return PlatformInput.touchGetEventTouchRadiusY(pointId); + } + + public static float getEventTouchRadiusMixed(int pointId) { + return PlatformInput.touchGetEventTouchRadiusMixed(pointId); + } + + public static float getEventTouchForce(int pointId) { + return PlatformInput.touchGetEventTouchForce(pointId); + } + + public static int getEventTouchPointUID(int pointId) { + return PlatformInput.touchGetEventTouchPointUID(pointId); + } + + public static int touchPointCount() { + return PlatformInput.touchPointCount(); + } + + public static int touchPointX(int pointId) { + return PlatformInput.touchPointX(pointId); + } + + public static int touchPointY(int pointId) { + return PlatformInput.touchPointY(pointId); + } + + public static float touchPointRadiusX(int pointId) { + return PlatformInput.touchRadiusX(pointId); + } + + public static float touchPointRadiusY(int pointId) { + return PlatformInput.touchRadiusY(pointId); + } + + public static float touchPointRadiusMixed(int pointId) { + return PlatformInput.touchRadiusMixed(pointId); + } + + public static float touchPointForce(int pointId) { + return PlatformInput.touchForce(pointId); + } + + public static int touchPointUID(int pointId) { + return PlatformInput.touchPointUID(pointId); + } + + public static void touchSetOpenKeyboardZone(int x, int y, int w, int h) { + PlatformInput.touchSetOpenKeyboardZone(x, y, w, h); + } + + public static void closeDeviceKeyboard() { + PlatformInput.touchCloseDeviceKeyboard(); + } + + public static boolean isDeviceKeyboardOpenMAYBE() { + return PlatformInput.touchIsDeviceKeyboardOpenMAYBE(); + } + + public static String getPastedString() { + return PlatformInput.touchGetPastedString(); + } + + public static boolean checkPointTouching(int uid) { + int cnt = PlatformInput.touchPointCount(); + if(cnt > 0) { + for(int i = 0; i < cnt; ++i) { + if(PlatformInput.touchPointUID(i) == uid) { + return true; + } + } + } + return false; + } + + public static int fetchPointIdx(int uid) { + int cnt = PlatformInput.touchPointCount(); + if(cnt > 0) { + for(int i = 0; i < cnt; ++i) { + if(PlatformInput.touchPointUID(i) == uid) { + return i; + } + } + } + return -1; + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/boot_menu/GuiScreenEnterBootMenu.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/boot_menu/GuiScreenEnterBootMenu.java new file mode 100644 index 00000000..e5f5b6c5 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/boot_menu/GuiScreenEnterBootMenu.java @@ -0,0 +1,54 @@ +package net.lax1dude.eaglercraft.v1_8.boot_menu; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenEnterBootMenu extends GuiScreen { + + private final GuiScreen parent; + + public GuiScreenEnterBootMenu(GuiScreen parent) { + this.parent = parent; + } + + public void initGui() { + EagRuntime.setDisplayBootMenuNextRefresh(true); + this.buttonList.clear(); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 96, I18n.format("gui.cancel"))); + } + + public void onGuiClosed() { + EagRuntime.setDisplayBootMenuNextRefresh(false); + } + + public void drawScreen(int par1, int par2, float par3) { + this.drawDefaultBackground(); + this.drawCenteredString(fontRendererObj, I18n.format("enterBootMenu.title"), this.width / 2, 70, 11184810); + this.drawCenteredString(fontRendererObj, I18n.format("enterBootMenu.text0"), this.width / 2, 90, 16777215); + super.drawScreen(par1, par2, par3); + } + + protected void actionPerformed(GuiButton par1GuiButton) { + if(par1GuiButton.id == 0) { + this.mc.displayGuiScreen(parent); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/cache/EaglerLoadingCache.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cache/EaglerLoadingCache.java index a9a7ecc9..54d55a00 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/cache/EaglerLoadingCache.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cache/EaglerLoadingCache.java @@ -25,7 +25,7 @@ public class EaglerLoadingCache { public EaglerLoadingCache(EaglerCacheProvider provider) { this.provider = provider; - this.cacheMap = new HashMap(); + this.cacheMap = new HashMap<>(); } public V get(K key) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenInspectSessionToken.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenInspectSessionToken.java new file mode 100644 index 00000000..ea9897cb --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenInspectSessionToken.java @@ -0,0 +1,84 @@ +package net.lax1dude.eaglercraft.v1_8.cookie; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import net.lax1dude.eaglercraft.v1_8.cookie.ServerCookieDataStore.ServerCookie; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenInspectSessionToken extends GuiScreen { + + private final GuiScreen parent; + private final ServerCookie cookie; + + public GuiScreenInspectSessionToken(GuiScreenRevokeSessionToken parent, ServerCookie cookie) { + this.parent = parent; + this.cookie = cookie; + } + + public void initGui() { + this.buttonList.clear(); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 106, I18n.format("gui.done"))); + } + + public void drawScreen(int par1, int par2, float par3) { + this.drawDefaultBackground(); + String[][] toDraw = new String[][] { + { + I18n.format("inspectSessionToken.details.server"), + I18n.format("inspectSessionToken.details.expires"), + I18n.format("inspectSessionToken.details.length") + }, + { + cookie.server.length() > 32 ? cookie.server.substring(0, 30) + "..." : cookie.server, + (new SimpleDateFormat("M/d/yyyy h:mm aa")).format(new Date(cookie.expires)), + Integer.toString(cookie.cookie.length) + } + }; + int[] maxWidth = new int[2]; + for(int i = 0; i < 2; ++i) { + String[] strs = toDraw[i]; + int w = 0; + for(int j = 0; j < strs.length; ++j) { + int k = fontRendererObj.getStringWidth(strs[j]); + if(k > w) { + w = k; + } + } + maxWidth[i] = w + 10; + } + int totalWidth = maxWidth[0] + maxWidth[1]; + this.drawCenteredString(fontRendererObj, I18n.format("inspectSessionToken.title"), this.width / 2, 70, 16777215); + this.drawString(fontRendererObj, toDraw[0][0], (this.width - totalWidth) / 2, 90, 11184810); + this.drawString(fontRendererObj, toDraw[0][1], (this.width - totalWidth) / 2, 104, 11184810); + this.drawString(fontRendererObj, toDraw[0][2], (this.width - totalWidth) / 2, 118, 11184810); + this.drawString(fontRendererObj, toDraw[1][0], (this.width - totalWidth) / 2 + maxWidth[0], 90, 11184810); + this.drawString(fontRendererObj, toDraw[1][1], (this.width - totalWidth) / 2 + maxWidth[0], 104, 11184810); + this.drawString(fontRendererObj, toDraw[1][2], (this.width - totalWidth) / 2 + maxWidth[0], 118, 11184810); + super.drawScreen(par1, par2, par3); + } + + protected void actionPerformed(GuiButton par1GuiButton) { + if(par1GuiButton.id == 0) { + this.mc.displayGuiScreen(parent); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenRevokeSessionToken.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenRevokeSessionToken.java new file mode 100644 index 00000000..6d42d00c --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenRevokeSessionToken.java @@ -0,0 +1,146 @@ +package net.lax1dude.eaglercraft.v1_8.cookie; + +import java.io.IOException; +import java.util.Collections; + +import com.google.common.collect.Lists; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiSlot; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenRevokeSessionToken extends GuiScreen { + protected GuiScreen parentScreen; + private GuiScreenRevokeSessionToken.List list; + private GuiButton inspectButton; + private GuiButton revokeButton; + + public GuiScreenRevokeSessionToken(GuiScreen parent) { + this.parentScreen = parent; + } + + public void initGui() { + this.buttonList.clear(); + this.buttonList.add(this.inspectButton = new GuiButton(10, this.width / 2 - 154, this.height - 38, 100, 20, I18n.format("revokeSessionToken.inspect"))); + this.buttonList.add(this.revokeButton = new GuiButton(9, this.width / 2 - 50, this.height - 38, 100, 20, I18n.format("revokeSessionToken.revoke"))); + this.buttonList.add(new GuiButton(6, this.width / 2 + 54, this.height - 38, 100, 20, I18n.format("gui.done"))); + this.list = new GuiScreenRevokeSessionToken.List(this.mc); + this.list.registerScrollButtons(7, 8); + updateButtons(); + } + + public void handleMouseInput() throws IOException { + super.handleMouseInput(); + this.list.handleMouseInput(); + } + + public void handleTouchInput() throws IOException { + super.handleTouchInput(); + this.list.handleTouchInput(); + } + + protected void actionPerformed(GuiButton parGuiButton) { + if (parGuiButton.enabled) { + switch (parGuiButton.id) { + case 6: + this.mc.displayGuiScreen(this.parentScreen); + break; + case 9: + String s1 = list.getSelectedItem(); + if(s1 != null) { + ServerCookieDataStore.ServerCookie cookie = ServerCookieDataStore.loadCookie(s1); + if(cookie != null) { + this.mc.displayGuiScreen(new GuiScreenSendRevokeRequest(this, cookie)); + }else { + this.initGui(); + } + } + break; + case 10: + String s2 = list.getSelectedItem(); + if(s2 != null) { + ServerCookieDataStore.ServerCookie cookie = ServerCookieDataStore.loadCookie(s2); + if(cookie != null) { + this.mc.displayGuiScreen(new GuiScreenInspectSessionToken(this, cookie)); + }else { + this.initGui(); + } + } + break; + default: + this.list.actionPerformed(parGuiButton); + } + + } + } + + protected void updateButtons() { + inspectButton.enabled = revokeButton.enabled = list.getSelectedItem() != null; + } + + public void drawScreen(int i, int j, float f) { + this.list.drawScreen(i, j, f); + this.drawCenteredString(this.fontRendererObj, I18n.format("revokeSessionToken.title"), this.width / 2, 16, 16777215); + this.drawCenteredString(this.fontRendererObj, I18n.format("revokeSessionToken.note.0"), this.width / 2, this.height - 66, 8421504); + this.drawCenteredString(this.fontRendererObj, I18n.format("revokeSessionToken.note.1"), this.width / 2, this.height - 56, 8421504); + super.drawScreen(i, j, f); + } + + class List extends GuiSlot { + private final java.util.List cookieNames = Lists.newArrayList(); + + public List(Minecraft mcIn) { + super(mcIn, GuiScreenRevokeSessionToken.this.width, GuiScreenRevokeSessionToken.this.height, 32, GuiScreenRevokeSessionToken.this.height - 75 + 4, 18); + ServerCookieDataStore.flush(); + cookieNames.addAll(ServerCookieDataStore.getRevokableServers()); + Collections.sort(cookieNames); + } + + protected int getSize() { + return this.cookieNames.size(); + } + + protected void elementClicked(int i, boolean var2, int var3, int var4) { + selectedElement = i; + GuiScreenRevokeSessionToken.this.updateButtons(); + } + + protected boolean isSelected(int i) { + return selectedElement == i; + } + + protected String getSelectedItem() { + return selectedElement == -1 ? null : cookieNames.get(selectedElement); + } + + protected int getContentHeight() { + return this.getSize() * 18; + } + + protected void drawBackground() { + GuiScreenRevokeSessionToken.this.drawDefaultBackground(); + } + + protected void drawSlot(int i, int var2, int j, int var4, int var5, int var6) { + GuiScreenRevokeSessionToken.this.drawCenteredString(GuiScreenRevokeSessionToken.this.fontRendererObj, + this.cookieNames.get(i), this.width / 2, j + 1, 16777215); + } + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenSendRevokeRequest.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenSendRevokeRequest.java new file mode 100644 index 00000000..fb3b60d4 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/GuiScreenSendRevokeRequest.java @@ -0,0 +1,177 @@ +package net.lax1dude.eaglercraft.v1_8.cookie; + +import org.json.JSONObject; + +import net.lax1dude.eaglercraft.v1_8.cookie.ServerCookieDataStore.ServerCookie; +import net.lax1dude.eaglercraft.v1_8.internal.IServerQuery; +import net.lax1dude.eaglercraft.v1_8.internal.QueryResponse; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.minecraft.GuiScreenGenericErrorMessage; +import net.lax1dude.eaglercraft.v1_8.socket.ServerQueryDispatch; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenSendRevokeRequest extends GuiScreen { + + private static final Logger logger = LogManager.getLogger("SessionRevokeRequest"); + + private GuiScreen parent; + private ServerCookie cookie; + private String title; + private String message; + private int timer = 0; + private boolean cancelRequested = false; + private IServerQuery query = null; + private boolean hasSentPacket = false; + + public GuiScreenSendRevokeRequest(GuiScreen parent, ServerCookie cookie) { + this.parent = parent; + this.cookie = cookie; + this.title = I18n.format("revokeSendingScreen.title"); + this.message = I18n.format("revokeSendingScreen.message.opening", cookie.server); + } + + public void initGui() { + this.buttonList.clear(); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 96, I18n.format("gui.cancel"))); + } + + public void drawScreen(int par1, int par2, float par3) { + this.drawDefaultBackground(); + this.drawCenteredString(fontRendererObj, title, this.width / 2, 70, 11184810); + this.drawCenteredString(fontRendererObj, message, this.width / 2, 90, 16777215); + super.drawScreen(par1, par2, par3); + } + + public void updateScreen() { + ++timer; + if (timer > 1) { + if(query == null) { + logger.info("Attempting to revoke session tokens for: {}", cookie.server); + query = ServerQueryDispatch.sendServerQuery(cookie.server, "revoke_session_token"); + if(query == null) { + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeFailure.title", "revokeFailure.desc.connectionError", parent)); + return; + } + }else { + query.update(); + QueryResponse resp = query.getResponse(); + if(resp != null) { + if(resp.responseType.equalsIgnoreCase("revoke_session_token") && (hasSentPacket ? resp.isResponseJSON() : resp.isResponseString())) { + if(!hasSentPacket) { + String str = resp.getResponseString(); + if("ready".equalsIgnoreCase(str)) { + hasSentPacket = true; + message = I18n.format("revokeSendingScreen.message.sending"); + query.send(cookie.cookie); + return; + }else { + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeFailure.title", "revokeFailure.desc.clientError", parent)); + return; + } + }else { + JSONObject json = resp.getResponseJSON(); + String stat = json.optString("status"); + if("ok".equalsIgnoreCase(stat)) { + if(hasSentPacket) { + query.close(); + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeSuccess.title", "revokeSuccess.desc", parent)); + ServerCookieDataStore.clearCookie(cookie.server); + return; + }else { + query.close(); + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeFailure.title", "revokeFailure.desc.clientError", parent)); + return; + } + }else if("error".equalsIgnoreCase(stat)) { + int code = json.optInt("code", -1); + if(code == -1) { + query.close(); + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeFailure.title", "revokeFailure.desc.clientError", parent)); + return; + }else { + String key; + switch(code) { + case 1: + key = "revokeFailure.desc.notSupported"; + break; + case 2: + key = "revokeFailure.desc.notAllowed"; + break; + case 3: + key = "revokeFailure.desc.notFound"; + break; + case 4: + key = "revokeFailure.desc.serverError"; + break; + default: + key = "revokeFailure.desc.genericCode"; + break; + } + logger.error("Recieved error code {}! ({})", code, key); + query.close(); + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeFailure.title", key, parent)); + if(json.optBoolean("delete", false)) { + ServerCookieDataStore.clearCookie(cookie.server); + } + return; + } + }else { + logger.error("Recieved unknown status \"{}\"!", stat); + query.close(); + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeFailure.title", "revokeFailure.desc.clientError", parent)); + return; + } + } + }else { + query.close(); + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeFailure.title", "revokeFailure.desc.clientError", parent)); + return; + } + } + if(query.isClosed()) { + if(!hasSentPacket || query.responsesAvailable() == 0) { + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeFailure.title", "revokeFailure.desc.connectionError", parent)); + return; + } + }else { + if(timer > 400) { + query.close(); + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeFailure.title", "revokeFailure.desc.connectionError", parent)); + return; + } + } + if(cancelRequested) { + query.close(); + this.mc.displayGuiScreen(new GuiScreenGenericErrorMessage("revokeFailure.title", "revokeFailure.desc.cancelled", parent)); + return; + } + } + } + } + + protected void actionPerformed(GuiButton par1GuiButton) { + if(par1GuiButton.id == 0) { + cancelRequested = true; + par1GuiButton.enabled = false; + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/HardwareFingerprint.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/HardwareFingerprint.java new file mode 100644 index 00000000..8fd21769 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/HardwareFingerprint.java @@ -0,0 +1,294 @@ +package net.lax1dude.eaglercraft.v1_8.cookie; + +import net.lax1dude.eaglercraft.v1_8.internal.IFramebufferGL; +import net.lax1dude.eaglercraft.v1_8.internal.IProgramGL; +import net.lax1dude.eaglercraft.v1_8.internal.IShaderGL; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformAssets; +import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; +import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.opengl.DrawUtils; +import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; +import net.lax1dude.eaglercraft.v1_8.opengl.GLSLHeader; +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; +import net.lax1dude.eaglercraft.v1_8.opengl.ImageData; +import net.lax1dude.eaglercraft.v1_8.opengl.VSHInputLayoutParser; +import net.minecraft.client.renderer.texture.TextureUtil; + +import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL.*; +import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; +import static net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.ExtGLEnums.*; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +import com.google.common.collect.Lists; + +import net.lax1dude.eaglercraft.v1_8.Base64; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom; +import net.lax1dude.eaglercraft.v1_8.crypto.GeneralDigest; +import net.lax1dude.eaglercraft.v1_8.crypto.SHA256Digest; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class HardwareFingerprint { + + // This is used for generating encryption keys for storing cookies, + // its supposed to make session hijacking more difficult for skids + + private static final String fingerprintIMG = "/9j/4AAQSkZJRgABAQEBLAEsAAD//gAKZnVjayBvZmb/4gKwSUNDX1BST0ZJTEUAAQEAAAKgbGNtcwRAAABtbnRyUkdCIFhZWiAH6AAGABAAAgArACNhY3NwTVNGVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWxjbXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1kZXNjAAABIAAAAEBjcHJ0AAABYAAAADZ3dHB0AAABmAAAABRjaGFkAAABrAAAACxyWFlaAAAB2AAAABRiWFlaAAAB7AAAABRnWFlaAAACAAAAABRyVFJDAAACFAAAACBnVFJDAAACFAAAACBiVFJDAAACFAAAACBjaHJtAAACNAAAACRkbW5kAAACWAAAACRkbWRkAAACfAAAACRtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACQAAAAcAEcASQBNAFAAIABiAHUAaQBsAHQALQBpAG4AIABzAFIARwBCbWx1YwAAAAAAAAABAAAADGVuVVMAAAAaAAAAHABQAHUAYgBsAGkAYwAgAEQAbwBtAGEAaQBuAABYWVogAAAAAAAA9tYAAQAAAADTLXNmMzIAAAAAAAEMQgAABd7///MlAAAHkwAA/ZD///uh///9ogAAA9wAAMBuWFlaIAAAAAAAAG+gAAA49QAAA5BYWVogAAAAAAAAJJ8AAA+EAAC2xFhZWiAAAAAAAABilwAAt4cAABjZcGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACltjaHJtAAAAAAADAAAAAKPXAABUfAAATM0AAJmaAAAmZwAAD1xtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAEcASQBNAFBtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEL/2wBDAAoHBwkHBgoJCAkLCwoMDxkQDw4ODx4WFxIZJCAmJSMgIyIoLTkwKCo2KyIjMkQyNjs9QEBAJjBGS0U+Sjk/QD3/2wBDAQsLCw8NDx0QEB09KSMpPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT3/wgARCACAAIADAREAAhEBAxEB/8QAGwAAAgMBAQEAAAAAAAAAAAAAAQIABAUDBgf/xAAYAQEBAQEBAAAAAAAAAAAAAAAAAQIDBP/aAAwDAQACEAMQAAABx95IwaIRghDRCFCElZ+KQhpghGDRCFCQNEzsUjUQjBDRChCENQJn4pDXfNuSvBBUJYK52Q5ahIZ+KSxLtc9jAcbO8OpA0YENodZpdJWszsUm1z3alHnuBZurO+SA5+PVPTQ7zv0zd1MLpnGzSem49OQIPMeslCpA82ueVj05fcuWdtPCBPW8elGzlRIMFCElOPZoJe0+dx2PR8+mdYoSAFQgFHq3ZsWWa+fxZ01+O6lKgqiWhxRhBQpv6mlXgdS1Loct8kAtcjiWBRxQANHWd+vEw5M67HQghXGO4AEFLms+hrxFhrW49KliDhGAABAALes79eI1CbnHpVshzAQhCEIAmpa3nJCbPHpwsgBRBQBGICrlzd1PN1DW5bSBSgAcxElOpQLo3OtX/8QAJxAAAQQBBAEEAgMAAAAAAAAAAQACAxEQBBITICEUIjAxMkEjNFD/2gAIAQEAAQUC/wBNkZevThcDVwtXC1cAXAuAoxOCMbh2iZyPqsX8HG16lhMXTSMqHAZP6jpI/Y2DVcxCCYA9s8JgfiBtaUq+zm7hHAyLATFqo+TTIeSRUbirVq1uW5bluW4K0FCE/wAilCLmkcirV4tWrVq0HprlD9Y039iQ9JXmMtO5qLqV+MbqWmfuZ+gC4we3UPPnL27mxGkTQaOhWjdhp2Na7cre0h99HsTQT20n5JxtzPzkHksX0rV/BpT7gQcyYpEZvtHbjE8x5PlmT8ELP4ItOGuw3zD3vo32r9f/xAAeEQACAQUAAwAAAAAAAAAAAAAAEQEQIDAxUAIhQP/aAAgBAwEBPwHpoQhCEIQr4qsvjSD2TbEk3Rqr5kb+CN59D7s2RrN//8QAIhEAAQMEAgIDAAAAAAAAAAAAAQACEhARIDEDMBNAIUFQ/9oACAECAQE/Af0ybKSkVIqRU1NSUgrjIm1HuiF5SmulgSAg4UBQN8H050XMiuHA3c5P44fK4zcVBvU7o5odteFqAtg5hBuEZu2mNtUejdfXeAna9B2u/ZRHeEfe33jA77BT/8QAJxAAAQIFAwMFAQAAAAAAAAAAAQARAhAhMDEgQVESImEDUGJxgZH/2gAIAQEABj8C9zosme6yVlUMsamVLVQnzDzoMXNh0zVn0xYKY42M4R+2GK7RoPIrMC6RKD7uvOHTRPZYVUL6umTnW+5TbrurpcZVbHhQ/d6hvngByuuEvyJwnxcPzib8CJNZw3PRg4gf+y//xAAkEAEAAgIBBAIDAQEAAAAAAAABABEQITFBUWFxIIGR8PFQsf/aAAgBAQABPyH/AE39Nd4GbulPd+54X8z9jhK/zHpMWqj8wa1/Xyrenl9QAoUGKcXgwMuFQdx/6oldyOPg7hqsdI6zYXz0qBrFwl/2wF1CXvFKdw5bLfcMh3g29ywwt8SBWWRJaV6wnKc4WhppgIBy6nZwoJ054y0vnrKnqeSB7xXNhc9kKwrXjOMOkdOJK/AKwo5htQdfcXWNvbK1juVGBXYldhV7sLWYuBPTEDZLisKuxC0CfyWOOGAgysrkwFkGMqaGo2zzBjudcb4i71HwzXfZBgRiblS/toGM8GGMeJo64uE47XQR03hBILpNyObZvHXHSAXdENsE8YJu33jKM7M4l4Dg64eIABpTeIibDl65XlB8BcSoy5cOMM3bz+49wcAJq8v6VfJNx1OEvC6uauwL3tBj/9oADAMBAAIAAwAAABD9mRbSTt1t0bIAF+n8iJQX9mluqdyUVE09DAPykEK/iOdbRe8nvNsaLtpZ+CB9RFxmQADt+ChzpmW03P7QNkikzbp/AkyUoZrmRKm2JYrYU5LbJaLJU0pbLQ1Z+klOwjL/xAAeEQEAAgIDAQEBAAAAAAAAAAABABEQICEwMUFAUP/aAAgBAwEBPxD+mJlZWUlZXPaI7C3AXglaVKwhErQcXHALa/CWQVGMSsioYcS2oiQo8i3HDip81qVq5qHpHuWsT1unGHsceIW4ZyS+twtw9jK63R7GeZ+fsDj/xAAfEQEAAgIDAAMBAAAAAAAAAAABABEQISAwMUFQYXH/2gAIAQIBAT8Q+zGL8IrT+IHANyqXP2DYwu8hizh6kW6cKQOBbrC2QNYWr+Mvk1UTQxTWEGpQy7UGAVGyAKOPcKEUahh04uevG+gwW1DlXIxbNHDrIYsJs4dhipUlkoZXWYFE8MJeK6TgdhtqX4cul7PdxbyaXZ4x/8QAJRABAAICAgEEAwADAAAAAAAAAQARITFBUWEQcZGhgbHhwdHw/9oACAEBAAE/EBhCDCEGEGXBhCD6DCEIMIQYQYMGDBhB9CEGDCEGEIMIMNQYegwYTVAtL0TKZ4UEAFjdsNePmS1wj2jHgfiL6Z72lLL9xIJSLqhLcAc5fqG4MuDBiZquXoQIDCA4lXuZ8F9S/iZGJdSw8QggXzHMFjupfG/2OA2aA56fMIMJcZp3wf1+oMd63WI/LNqeSMC9yq1vqaTBxMWXS4ykS/CRRTcE4ekf3GNn/BHMGDmcKrS7yiomtzOywRtvMuubjKtmDTGl52MIthPKAjqXYTiPbtv22fEGZmkB7sxbQUuaKqADe03UvVR52BbYQGDFJOU325g2S35jtRa8wEObK4akp9T3RcfVj+G4QKyH0TwImLaIPZEBLwSagO5YbMQnnXmOKy4WW2ntLn+4sOXwn6ZgTlgVmYxYTuWbLuCByahNMWaIhBT1Oj5i5xfvASx7Z+p0AGTmFsIASbaLfMOXbcJXKHuVcWYsxgBXiFgqY8kuWM0suh49AhbwQiUp8wirdaTATjBHIf1CRqVeuf8AD5Jhihwm2wzQsuHUULmLm10cxwUDiWBGZUdzpxD2g2zyRYDxCZNGOo4gMaE+4pKuLVFMBpzoBddwA5izZ+osHLwRzzFqEIllOf8AcpK2Mrr0VI9MVANBlvzEGSUt6QOUF36AT00Udeg/S3jQx9qH5isgUZobxwlkCVPJi+o4bdR+pcCEhdwgdRYnprC1givIW1+R8So0Sq6vcCVL/wAi+DUo5ihYkuLWiOSkjcMSyxC0AlodEEfALlhHUubF/STqcT//2Q=="; + + private static final String shaderPrecision = "precision lowp int;\nprecision mediump float;\nprecision mediump sampler2D;\n"; + + private static byte[] fingerprint = null; + + public static final Logger logger = LogManager.getLogger("HardwareFingerprint"); + + public static byte[] getFingerprint() { + if(fingerprint == null) { + try { + fingerprint = generateFingerprint(); + }catch(Throwable t) { + fingerprint = new byte[0]; + } + if(fingerprint.length == 0) { + logger.error("Failed to calculate hardware fingerprint, server cookies will not be encrypted!"); + } + } + return fingerprint; + } + + private static byte[] generateFingerprint() { + ImageData img = PlatformAssets.loadImageFile(Base64.decodeBase64(fingerprintIMG), "image/jpeg"); + if(img == null) { + logger.error("Input image data is corrupt!"); + return new byte[0]; + } + + int[][] mipmapLevels = TextureUtil.generateMipmapData(7, 128, + new int[][] { img.pixels, null, null, null, null, null, null, null }); + + int helperTexture = GlStateManager.generateTexture(); + GlStateManager.bindTexture(helperTexture); + _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + TextureUtil.allocateTextureImpl(helperTexture, 7, 128, 128); + TextureUtil.uploadTextureMipmap(mipmapLevels, 128, 128, 0, 0, false, false); + if(checkAnisotropicFilteringSupport()) { + _wglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, 16.0f); + } + + IShaderGL vert; + List vertLayout; + if(DrawUtils.vshLocal != null) { + vert = DrawUtils.vshLocal; + vertLayout = DrawUtils.vshLocalLayout; + }else { + String vshLocalSrc = EagRuntime.getRequiredResourceString("/assets/eagler/glsl/local.vsh"); + vertLayout = VSHInputLayoutParser.getShaderInputs(vshLocalSrc); + vert = _wglCreateShader(GL_VERTEX_SHADER); + _wglShaderSource(vert, GLSLHeader.getVertexHeaderCompat(vshLocalSrc, DrawUtils.vertexShaderPrecision)); + _wglCompileShader(vert); + if(_wglGetShaderi(vert, GL_COMPILE_STATUS) != GL_TRUE) { + _wglDeleteShader(vert); + GlStateManager.deleteTexture(helperTexture); + return new byte[0]; + } + } + + IShaderGL frag = _wglCreateShader(GL_FRAGMENT_SHADER); + _wglShaderSource(frag, GLSLHeader.getFragmentHeaderCompat(EagRuntime.getRequiredResourceString("/assets/eagler/glsl/hw_fingerprint.fsh"), shaderPrecision)); + _wglCompileShader(frag); + if(_wglGetShaderi(frag, GL_COMPILE_STATUS) != GL_TRUE) { + _wglDeleteShader(vert); + _wglDeleteShader(frag); + GlStateManager.deleteTexture(helperTexture); + return new byte[0]; + } + + IProgramGL program = _wglCreateProgram(); + + _wglAttachShader(program, vert); + _wglAttachShader(program, frag); + + if(EaglercraftGPU.checkOpenGLESVersion() == 200) { + VSHInputLayoutParser.applyLayout(program, vertLayout); + } + + _wglLinkProgram(program); + _wglDetachShader(program, vert); + _wglDetachShader(program, frag); + if(DrawUtils.vshLocal == null) { + _wglDeleteShader(vert); + } + _wglDeleteShader(frag); + + if(_wglGetProgrami(program, GL_LINK_STATUS) != GL_TRUE) { + _wglDeleteProgram(program); + GlStateManager.deleteTexture(helperTexture); + return new byte[0]; + } + + EaglercraftGPU.bindGLShaderProgram(program); + _wglUniform1i(_wglGetUniformLocation(program, "u_inputTexture"), 0); + + float fovy = 90.0f; + float aspect = 1.0f; + float zNear = 0.01f; + float zFar = 100.0f; + FloatBuffer matrixUploadBuffer = EagRuntime.allocateFloatBuffer(16); + float toRad = 0.0174532925f; + float cotangent = (float) Math.cos(fovy * toRad * 0.5f) / (float) Math.sin(fovy * toRad * 0.5f); + + matrixUploadBuffer.put(cotangent / aspect); + matrixUploadBuffer.put(0.0f); + matrixUploadBuffer.put(0.0f); + matrixUploadBuffer.put(0.0f); + matrixUploadBuffer.put(0.0f); + matrixUploadBuffer.put(cotangent); + matrixUploadBuffer.put(0.0f); + matrixUploadBuffer.put(0.0f); + matrixUploadBuffer.put(0.0f); + matrixUploadBuffer.put(0.0f); + matrixUploadBuffer.put((zFar + zNear) / (zFar - zNear)); + matrixUploadBuffer.put(2.0f * zFar * zNear / (zFar - zNear)); + matrixUploadBuffer.put(0.0f); + matrixUploadBuffer.put(0.0f); + matrixUploadBuffer.put(-1.0f); + matrixUploadBuffer.put(0.0f); + + matrixUploadBuffer.flip(); + _wglUniformMatrix4fv(_wglGetUniformLocation(program, "u_textureMatrix"), false, matrixUploadBuffer); + EagRuntime.freeFloatBuffer(matrixUploadBuffer); + + int[] oldViewport = new int[4]; + EaglercraftGPU.glGetInteger(GL_VIEWPORT, oldViewport); + IFramebufferGL framebuffer = _wglCreateFramebuffer(); + int renderTexture = GlStateManager.generateTexture(); + GlStateManager.bindTexture(renderTexture); + _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + + int dataLength; + int type; + if(EaglercraftGPU.checkHDRFramebufferSupport(32)) { + dataLength = 256 * 256 * 4 * 4; + type = GL_FLOAT; + EaglercraftGPU.createFramebufferHDR32FTexture(GL_TEXTURE_2D, 0, 256, 256, GL_RGBA, false); + }else if(EaglercraftGPU.checkHDRFramebufferSupport(16)) { + dataLength = 256 * 256 * 4 * 2; + type = _GL_HALF_FLOAT; + EaglercraftGPU.createFramebufferHDR16FTexture(GL_TEXTURE_2D, 0, 256, 256, GL_RGBA, false); + }else { + dataLength = 256 * 256 * 4; + type = GL_UNSIGNED_BYTE; + EaglercraftGPU.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, (ByteBuffer)null); + } + + _wglBindFramebuffer(_GL_FRAMEBUFFER, framebuffer); + _wglFramebufferTexture2D(_GL_FRAMEBUFFER, _GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, EaglercraftGPU.getNativeTexture(renderTexture), 0); + _wglDrawBuffers(_GL_COLOR_ATTACHMENT0); + + GlStateManager.viewport(0, 0, 256, 256); + GlStateManager.disableBlend(); + GlStateManager.bindTexture(helperTexture); + + DrawUtils.drawStandardQuad2D(); + + _wglDeleteProgram(program); + GlStateManager.deleteTexture(helperTexture); + + ByteBuffer readBuffer = EagRuntime.allocateByteBuffer(dataLength); + EaglercraftGPU.glReadPixels(0, 0, 256, 256, GL_RGBA, type, readBuffer); + + _wglBindFramebuffer(_GL_FRAMEBUFFER, null); + _wglDeleteFramebuffer(framebuffer); + GlStateManager.deleteTexture(renderTexture); + GlStateManager.viewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]); + + SHA256Digest digest = new SHA256Digest(); + byte[] copyBuffer = new byte[1024]; + + byte[] b = ("eag" + EaglercraftGPU.glGetString(GL_VENDOR) + "; eag " + EaglercraftGPU.glGetString(GL_RENDERER)).getBytes(StandardCharsets.UTF_8); + digest.update(b, 0, b.length); + + digestInts(digest, _wglGetInteger(0x8869), _wglGetInteger(0x8DFB), _wglGetInteger(0x8B4C), _wglGetInteger(0x8DFC), copyBuffer); + digestInts(digest, _wglGetInteger(0x8DFD), _wglGetInteger(0x8872), _wglGetInteger(0x84E8), 69, copyBuffer); + digestInts(digest, _wglGetInteger(0x0D33), _wglGetInteger(0x851C), _wglGetInteger(0x8B4D), 69, copyBuffer); + + if(EaglercraftGPU.checkOpenGLESVersion() >= 300) { + digestInts(digest, _wglGetInteger(0x8B4A), _wglGetInteger(0x8A2B), _wglGetInteger(0x9122), _wglGetInteger(0x8B4B), copyBuffer); + digestInts(digest, _wglGetInteger(0x8C8A), _wglGetInteger(0x8C8B), _wglGetInteger(0x8C80), _wglGetInteger(0x8B49), copyBuffer); + digestInts(digest, _wglGetInteger(0x8A2D), _wglGetInteger(0x9125), _wglGetInteger(0x8904), _wglGetInteger(0x8905), copyBuffer); + digestInts(digest, _wglGetInteger(0x8824), _wglGetInteger(0x8073), _wglGetInteger(0x88FF), _wglGetInteger(0x84FD), copyBuffer); + digestInts(digest, _wglGetInteger(0x8CDF), _wglGetInteger(0x8A2F), _wglGetInteger(0x8A30), _wglGetInteger(0x8A34), copyBuffer); + digestInts(digest, _wglGetInteger(0x8A2E), _wglGetInteger(0x8A31), _wglGetInteger(0x8A33), _wglGetInteger(0x8D57), copyBuffer); + } + + try { + List exts = Lists.newArrayList(getAllExtensions()); + Collections.sort(exts); + EaglercraftRandom rand = new EaglercraftRandom(6942069420l + exts.size() * 69l + b.length); + for (int i = exts.size() - 1; i > 0; --i) { + int j = rand.nextInt(i + 1); + Collections.swap(exts, i, j); + } + b = String.join(":>", exts).getBytes(StandardCharsets.UTF_8); + digest.update(b, 0, b.length); + }catch(Throwable t) { + } + + int i; + while(readBuffer.hasRemaining()) { + i = Math.min(readBuffer.remaining(), copyBuffer.length); + readBuffer.get(copyBuffer, 0, i); + digest.update(copyBuffer, 0, i); + } + + EagRuntime.freeByteBuffer(readBuffer); + + byte[] hashOut = new byte[32]; + digest.doFinal(hashOut, 0); + + return hashOut; + } + + private static void digestInts(GeneralDigest digest, int i1, int i2, int i3, int i4, byte[] tmpBuffer) { + tmpBuffer[0] = (byte)(i1 >>> 24); + tmpBuffer[1] = (byte)(i1 >>> 16); + tmpBuffer[2] = (byte)(i1 >>> 8); + tmpBuffer[3] = (byte)(i1 & 0xFF); + tmpBuffer[4] = (byte)(i2 >>> 24); + tmpBuffer[5] = (byte)(i2 >>> 16); + tmpBuffer[6] = (byte)(i2 >>> 8); + tmpBuffer[7] = (byte)(i2 & 0xFF); + tmpBuffer[8] = (byte)(i3 >>> 24); + tmpBuffer[9] = (byte)(i3 >>> 16); + tmpBuffer[10] = (byte)(i3 >>> 8); + tmpBuffer[11] = (byte)(i3 & 0xFF); + tmpBuffer[12] = (byte)(i4 >>> 24); + tmpBuffer[13] = (byte)(i4 >>> 16); + tmpBuffer[14] = (byte)(i4 >>> 8); + tmpBuffer[15] = (byte)(i4 & 0xFF); + digest.update(tmpBuffer, 0, 16); + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/ServerCookieDataStore.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/ServerCookieDataStore.java new file mode 100644 index 00000000..705da432 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/cookie/ServerCookieDataStore.java @@ -0,0 +1,396 @@ +package net.lax1dude.eaglercraft.v1_8.cookie; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.EaglerInputStream; +import net.lax1dude.eaglercraft.v1_8.EaglerOutputStream; +import net.lax1dude.eaglercraft.v1_8.EaglerZLIB; +import net.lax1dude.eaglercraft.v1_8.crypto.AESLightEngine; +import net.lax1dude.eaglercraft.v1_8.crypto.SHA1Digest; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformApplication; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ServerCookieDataStore { + + private static final Logger logger = LogManager.getLogger("ServerCookieDataStore"); + + private static final Map dataStore = new HashMap<>(); + + public static final String localStorageKey = "c"; + + public static class ServerCookie { + + public final String server; + public final byte[] cookie; + public final long expires; + public final boolean revokeQuerySupported; + public final boolean saveCookieToDisk; + + public ServerCookie(String server, byte[] cookie, long expires, boolean revokeQuerySupported, boolean saveCookieToDisk) { + this.server = server; + this.cookie = cookie; + this.expires = expires; + this.revokeQuerySupported = revokeQuerySupported; + this.saveCookieToDisk = saveCookieToDisk; + } + + } + + public static void load() { + if(EagRuntime.getConfiguration().isEnableServerCookies()) { + loadData(HardwareFingerprint.getFingerprint()); + } + } + + public static ServerCookie loadCookie(String server) { + if(!EagRuntime.getConfiguration().isEnableServerCookies()) { + return null; + } + server = normalize(server); + ServerCookie cookie = dataStore.get(server); + if(cookie == null) { + return null; + } + long timestamp = System.currentTimeMillis(); + if(timestamp > cookie.expires) { + dataStore.remove(server); + saveData(HardwareFingerprint.getFingerprint()); + return null; + } + return cookie; + } + + public static void saveCookie(String server, long expires, byte[] data, boolean revokeQuerySupported, boolean saveCookieToDisk) { + if(!EagRuntime.getConfiguration().isEnableServerCookies()) { + return; + } + server = normalize(server); + if(expires > 604800l) { + clearCookie(server); + logger.error("Server \"{}\" tried to set a cookie for {} days! (The max is 7 days)", server, (expires / 604800l)); + return; + } + if(data.length > 255) { + clearCookie(server); + logger.error("Server \"{}\" tried to set a {} byte cookie! (The max length is 255 bytes)", server, data.length); + return; + } + if(expires < 0l || data.length == 0) { + clearCookie(server); + return; + } + long expiresRelative = System.currentTimeMillis() + expires * 1000l; + dataStore.put(server, new ServerCookie(server, data, expiresRelative, revokeQuerySupported, saveCookieToDisk)); + saveData(HardwareFingerprint.getFingerprint()); + } + + public static void clearCookie(String server) { + if(!EagRuntime.getConfiguration().isEnableServerCookies()) { + return; + } + if(dataStore.remove(normalize(server)) != null) { + saveData(HardwareFingerprint.getFingerprint()); + } + } + + public static void clearCookiesLow() { + dataStore.clear(); + } + + private static String normalize(String server) { + int j = server.indexOf('/'); + if(j != -1) { + int i = server.indexOf("://"); + if(i != -1) { + j = server.indexOf('/', i + 3); + if(j == -1) { + return server.toLowerCase(); + }else { + return server.substring(0, j).toLowerCase() + server.substring(j); + } + }else { + return server.substring(0, j).toLowerCase() + server.substring(j); + } + }else { + return server.toLowerCase(); + } + } + + public static Set getAllServers() { + return dataStore.keySet(); + } + + public static List getRevokableServers() { + List ret = new ArrayList<>(dataStore.size()); + for(ServerCookie c : dataStore.values()) { + if(c.revokeQuerySupported) { + ret.add(c.server); + } + } + return ret; + } + + public static int size() { + return dataStore.size(); + } + + public static int numRevokable() { + int i = 0; + for(ServerCookie c : dataStore.values()) { + if(c.revokeQuerySupported) { + ++i; + } + } + return i; + } + + public static void flush() { + Iterator itr = dataStore.values().iterator(); + boolean changed = false; + while(itr.hasNext()) { + long timestamp = System.currentTimeMillis(); + ServerCookie cookie = itr.next(); + if(timestamp > cookie.expires) { + itr.remove(); + changed = true; + } + } + if(changed) { + saveData(HardwareFingerprint.getFingerprint()); + } + } + + private static void loadData(byte[] key) { + dataStore.clear(); + byte[] cookiesTag = PlatformApplication.getLocalStorage(localStorageKey, false); + if(cookiesTag == null) { + return; + } + if(cookiesTag.length <= 25) { + PlatformApplication.setLocalStorage(localStorageKey, null, false); + return; + } + try { + byte[] decrypted; + int decryptedLen; + switch(cookiesTag[0]) { + case 2: + if(key == null || key.length == 0) { + throw new IOException("Data is encrypted!"); + } + decrypted = new byte[cookiesTag.length - 5]; + decryptedLen = (cookiesTag[1] << 24) | (cookiesTag[2] << 16) | (cookiesTag[3] << 8) | (cookiesTag[4] & 0xFF); + if(decryptedLen < 25) { + throw new IOException("too short!"); + } + AESLightEngine aes = new AESLightEngine(); + aes.init(false, key); + int bs = aes.getBlockSize(); + if(decrypted.length % bs != 0) { + throw new IOException("length not aligned to block size!"); + } + byte[] cbcHelper = new byte[] { (byte) 29, (byte) 163, (byte) 4, (byte) 20, (byte) 207, (byte) 26, + (byte) 140, (byte) 55, (byte) 246, (byte) 250, (byte) 141, (byte) 183, (byte) 153, (byte) 154, + (byte) 59, (byte) 4 }; + for(int i = 0; i < decryptedLen; i += bs) { + processBlockDecryptHelper(aes, cookiesTag, 5 + i, decrypted, i, bs, Math.min(decryptedLen - i, bs), cbcHelper); + } + if(decrypted[0] != (byte)0x69) { + throw new IOException("Data is corrupt!"); + } + break; + case 1: + if(key != null && key.length > 0) { + throw new IOException("Data isn't encrypted!"); + } + decrypted = cookiesTag; + decryptedLen = cookiesTag.length; + break; + default: + throw new IOException("Unknown type!"); + } + SHA1Digest digest = new SHA1Digest(); + digest.update(decrypted, 25, decryptedLen - 25); + byte[] digestOut = new byte[20]; + digest.doFinal(digestOut, 0); + for(int i = 0; i < 20; ++i) { + if(digestOut[i] != decrypted[5 + i]) { + throw new IOException("Invalid checksum!"); + } + } + int decompressedLen = (decrypted[1] << 24) | (decrypted[2] << 16) | (decrypted[3] << 8) | (decrypted[4] & 0xFF); + byte[] decompressed = new byte[decompressedLen]; + try (InputStream zstream = EaglerZLIB.newInflaterInputStream(new EaglerInputStream(decrypted, 25, decryptedLen - 25))) { + int i = 0, j; + while(i < decompressedLen && (j = zstream.read(decompressed, i, decompressedLen - i)) != -1) { + i += j; + } + if(i != decompressedLen) { + throw new IOException("Length does not match!"); + } + } + DataInputStream dis = new DataInputStream(new EaglerInputStream(decompressed)); + int readCount = dis.readInt(); + long time = System.currentTimeMillis(); + for(int i = 0; i < readCount; ++i) { + byte flags = dis.readByte(); + long expires = dis.readLong(); + int len = dis.readUnsignedShort(); + String server = dis.readUTF(); + if(len == 0) { + continue; + } + if(expires < time) { + dis.skipBytes(len); + continue; + } + byte[] cookieData = new byte[len]; + dis.readFully(cookieData); + server = normalize(server); + dataStore.put(server, new ServerCookie(server, cookieData, expires, (flags & 1) != 0, (flags & 2) != 0)); + } + if(dis.available() > 0) { + throw new IOException("Extra bytes remaining!"); + } + }catch (IOException e) { + dataStore.clear(); + PlatformApplication.setLocalStorage(localStorageKey, null, false); + return; + } + } + + private static void saveData(byte[] key) { + Iterator itr = dataStore.values().iterator(); + List toSave = new ArrayList<>(dataStore.size()); + while(itr.hasNext()) { + long timestamp = System.currentTimeMillis(); + ServerCookie cookie = itr.next(); + if(timestamp > cookie.expires || cookie.cookie.length > 255 || cookie.cookie.length == 0) { + itr.remove(); + }else if(cookie.saveCookieToDisk) { + toSave.add(cookie); + } + } + if(toSave.size() == 0) { + PlatformApplication.setLocalStorage(localStorageKey, null, false); + }else { + EaglerOutputStream bao = new EaglerOutputStream(1024); + bao.skipBytes(25); + int totalUncompressedLen; + try(DataOutputStream zstream = new DataOutputStream(EaglerZLIB.newDeflaterOutputStream(bao))) { + zstream.writeInt(dataStore.size()); + for(Entry etr : dataStore.entrySet()) { + ServerCookie cookie = etr.getValue(); + zstream.writeByte((cookie.revokeQuerySupported ? 1 : 0) | (cookie.saveCookieToDisk ? 2 : 0)); + zstream.writeLong(cookie.expires); + zstream.writeShort(cookie.cookie.length); + zstream.writeUTF(etr.getKey()); + zstream.write(cookie.cookie); + } + totalUncompressedLen = zstream.size(); + } catch (IOException e) { + logger.error("Failed to write cookies to local storage!"); + return; + } + byte[] toEncrypt = bao.toByteArray(); + SHA1Digest hash = new SHA1Digest(); + hash.update(toEncrypt, 25, toEncrypt.length - 25); + hash.doFinal(toEncrypt, 5); + toEncrypt[1] = (byte)(totalUncompressedLen >>> 24); + toEncrypt[2] = (byte)(totalUncompressedLen >>> 16); + toEncrypt[3] = (byte)(totalUncompressedLen >>> 8); + toEncrypt[4] = (byte)(totalUncompressedLen & 0xFF); + if(key != null && key.length > 0) { + toEncrypt[0] = (byte)0x69; + AESLightEngine aes = new AESLightEngine(); + aes.init(true, key); + int bs = aes.getBlockSize(); + int blockCount = (toEncrypt.length % bs) != 0 ? (toEncrypt.length / bs + 1) : (toEncrypt.length / bs); + byte[] encrypted = new byte[blockCount * bs + 5]; + encrypted[0] = (byte)2; + encrypted[1] = (byte)(toEncrypt.length >>> 24); + encrypted[2] = (byte)(toEncrypt.length >>> 16); + encrypted[3] = (byte)(toEncrypt.length >>> 8); + encrypted[4] = (byte)(toEncrypt.length & 0xFF); + byte[] cbcHelper = new byte[] { (byte) 29, (byte) 163, (byte) 4, (byte) 20, (byte) 207, (byte) 26, + (byte) 140, (byte) 55, (byte) 246, (byte) 250, (byte) 141, (byte) 183, (byte) 153, (byte) 154, + (byte) 59, (byte) 4 }; + for(int i = 0; i < toEncrypt.length; i += bs) { + processBlockEncryptHelper(aes, toEncrypt, i, encrypted, 5 + i, bs, cbcHelper); + } + PlatformApplication.setLocalStorage(localStorageKey, encrypted, false); + }else { + toEncrypt[0] = (byte)1; + PlatformApplication.setLocalStorage(localStorageKey, toEncrypt, false); + } + } + } + + private static void processBlockEncryptHelper(AESLightEngine aes, byte[] in, int inOff, byte[] out, int outOff, + int len, byte[] cbcHelper) { + int clampedBlockLength = Math.min(in.length - inOff, len); + if(clampedBlockLength == len) { + for(int i = 0; i < len; ++i) { + in[i + inOff] ^= cbcHelper[i]; + } + aes.processBlock(in, inOff, out, outOff); + System.arraycopy(out, outOff, cbcHelper, 0, len); + }else { + byte[] paddedBlock = new byte[len]; + System.arraycopy(in, inOff, paddedBlock, 0, clampedBlockLength); + byte padValue = (byte)(len - clampedBlockLength); + for(byte i = 0; i < padValue; ++i) { + paddedBlock[clampedBlockLength + i] = padValue; + } + for(int i = 0; i < len; ++i) { + paddedBlock[i] ^= cbcHelper[i]; + } + aes.processBlock(paddedBlock, 0, out, outOff); + } + } + + private static void processBlockDecryptHelper(AESLightEngine aes, byte[] in, int inOff, byte[] out, int outOff, + int paddedLen, int unpaddedLen, byte[] cbcHelper) throws IOException { + aes.processBlock(in, inOff, out, outOff); + for(int i = 0; i < paddedLen; ++i) { + out[i + outOff] ^= cbcHelper[i]; + } + if(unpaddedLen == paddedLen) { + System.arraycopy(in, inOff, cbcHelper, 0, paddedLen); + }else { + byte padValue = (byte)(paddedLen - unpaddedLen); + for(byte i = 0; i < padValue; ++i) { + if(out[outOff + unpaddedLen + i] != padValue) { + throw new IOException("Invalid padding!"); + } + } + } + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/crypto/AESLightEngine.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/crypto/AESLightEngine.java new file mode 100644 index 00000000..6938e30f --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/crypto/AESLightEngine.java @@ -0,0 +1,527 @@ +/* + * Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) + * + * 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 net.lax1dude.eaglercraft.v1_8.crypto; + +/** + * an implementation of the AES (Rijndael), from FIPS-197. + *

+ * For further details see: https://csrc.nist.gov/encryption/aes/. + * + * This implementation is based on optimizations from Dr. Brian Gladman's paper and C code at + * https://fp.gladman.plus.com/cryptography_technology/rijndael/ + * + * There are three levels of tradeoff of speed vs memory + * Because java has no preprocessor, they are written as three separate classes from which to choose + * + * The fastest uses 8Kbytes of static tables to precompute round calculations, 4 256 word tables for encryption + * and 4 for decryption. + * + * The middle performance version uses only one 256 word table for each, for a total of 2Kbytes, + * adding 12 rotate operations per round to compute the values contained in the other tables from + * the contents of the first + * + * The slowest version uses no static tables at all and computes the values + * in each round. + *

+ * This file contains the slowest performance version with no static tables + * for round precomputation, but it has the smallest foot print. + * + */ +public class AESLightEngine { + // The S box + private static final byte[] S = { (byte) 99, (byte) 124, (byte) 119, (byte) 123, (byte) 242, (byte) 107, (byte) 111, + (byte) 197, (byte) 48, (byte) 1, (byte) 103, (byte) 43, (byte) 254, (byte) 215, (byte) 171, (byte) 118, + (byte) 202, (byte) 130, (byte) 201, (byte) 125, (byte) 250, (byte) 89, (byte) 71, (byte) 240, (byte) 173, + (byte) 212, (byte) 162, (byte) 175, (byte) 156, (byte) 164, (byte) 114, (byte) 192, (byte) 183, (byte) 253, + (byte) 147, (byte) 38, (byte) 54, (byte) 63, (byte) 247, (byte) 204, (byte) 52, (byte) 165, (byte) 229, + (byte) 241, (byte) 113, (byte) 216, (byte) 49, (byte) 21, (byte) 4, (byte) 199, (byte) 35, (byte) 195, + (byte) 24, (byte) 150, (byte) 5, (byte) 154, (byte) 7, (byte) 18, (byte) 128, (byte) 226, (byte) 235, + (byte) 39, (byte) 178, (byte) 117, (byte) 9, (byte) 131, (byte) 44, (byte) 26, (byte) 27, (byte) 110, + (byte) 90, (byte) 160, (byte) 82, (byte) 59, (byte) 214, (byte) 179, (byte) 41, (byte) 227, (byte) 47, + (byte) 132, (byte) 83, (byte) 209, (byte) 0, (byte) 237, (byte) 32, (byte) 252, (byte) 177, (byte) 91, + (byte) 106, (byte) 203, (byte) 190, (byte) 57, (byte) 74, (byte) 76, (byte) 88, (byte) 207, (byte) 208, + (byte) 239, (byte) 170, (byte) 251, (byte) 67, (byte) 77, (byte) 51, (byte) 133, (byte) 69, (byte) 249, + (byte) 2, (byte) 127, (byte) 80, (byte) 60, (byte) 159, (byte) 168, (byte) 81, (byte) 163, (byte) 64, + (byte) 143, (byte) 146, (byte) 157, (byte) 56, (byte) 245, (byte) 188, (byte) 182, (byte) 218, (byte) 33, + (byte) 16, (byte) 255, (byte) 243, (byte) 210, (byte) 205, (byte) 12, (byte) 19, (byte) 236, (byte) 95, + (byte) 151, (byte) 68, (byte) 23, (byte) 196, (byte) 167, (byte) 126, (byte) 61, (byte) 100, (byte) 93, + (byte) 25, (byte) 115, (byte) 96, (byte) 129, (byte) 79, (byte) 220, (byte) 34, (byte) 42, (byte) 144, + (byte) 136, (byte) 70, (byte) 238, (byte) 184, (byte) 20, (byte) 222, (byte) 94, (byte) 11, (byte) 219, + (byte) 224, (byte) 50, (byte) 58, (byte) 10, (byte) 73, (byte) 6, (byte) 36, (byte) 92, (byte) 194, + (byte) 211, (byte) 172, (byte) 98, (byte) 145, (byte) 149, (byte) 228, (byte) 121, (byte) 231, (byte) 200, + (byte) 55, (byte) 109, (byte) 141, (byte) 213, (byte) 78, (byte) 169, (byte) 108, (byte) 86, (byte) 244, + (byte) 234, (byte) 101, (byte) 122, (byte) 174, (byte) 8, (byte) 186, (byte) 120, (byte) 37, (byte) 46, + (byte) 28, (byte) 166, (byte) 180, (byte) 198, (byte) 232, (byte) 221, (byte) 116, (byte) 31, (byte) 75, + (byte) 189, (byte) 139, (byte) 138, (byte) 112, (byte) 62, (byte) 181, (byte) 102, (byte) 72, (byte) 3, + (byte) 246, (byte) 14, (byte) 97, (byte) 53, (byte) 87, (byte) 185, (byte) 134, (byte) 193, (byte) 29, + (byte) 158, (byte) 225, (byte) 248, (byte) 152, (byte) 17, (byte) 105, (byte) 217, (byte) 142, (byte) 148, + (byte) 155, (byte) 30, (byte) 135, (byte) 233, (byte) 206, (byte) 85, (byte) 40, (byte) 223, (byte) 140, + (byte) 161, (byte) 137, (byte) 13, (byte) 191, (byte) 230, (byte) 66, (byte) 104, (byte) 65, (byte) 153, + (byte) 45, (byte) 15, (byte) 176, (byte) 84, (byte) 187, (byte) 22, }; + + // The inverse S-box + private static final byte[] Si = { (byte) 82, (byte) 9, (byte) 106, (byte) 213, (byte) 48, (byte) 54, (byte) 165, + (byte) 56, (byte) 191, (byte) 64, (byte) 163, (byte) 158, (byte) 129, (byte) 243, (byte) 215, (byte) 251, + (byte) 124, (byte) 227, (byte) 57, (byte) 130, (byte) 155, (byte) 47, (byte) 255, (byte) 135, (byte) 52, + (byte) 142, (byte) 67, (byte) 68, (byte) 196, (byte) 222, (byte) 233, (byte) 203, (byte) 84, (byte) 123, + (byte) 148, (byte) 50, (byte) 166, (byte) 194, (byte) 35, (byte) 61, (byte) 238, (byte) 76, (byte) 149, + (byte) 11, (byte) 66, (byte) 250, (byte) 195, (byte) 78, (byte) 8, (byte) 46, (byte) 161, (byte) 102, + (byte) 40, (byte) 217, (byte) 36, (byte) 178, (byte) 118, (byte) 91, (byte) 162, (byte) 73, (byte) 109, + (byte) 139, (byte) 209, (byte) 37, (byte) 114, (byte) 248, (byte) 246, (byte) 100, (byte) 134, (byte) 104, + (byte) 152, (byte) 22, (byte) 212, (byte) 164, (byte) 92, (byte) 204, (byte) 93, (byte) 101, (byte) 182, + (byte) 146, (byte) 108, (byte) 112, (byte) 72, (byte) 80, (byte) 253, (byte) 237, (byte) 185, (byte) 218, + (byte) 94, (byte) 21, (byte) 70, (byte) 87, (byte) 167, (byte) 141, (byte) 157, (byte) 132, (byte) 144, + (byte) 216, (byte) 171, (byte) 0, (byte) 140, (byte) 188, (byte) 211, (byte) 10, (byte) 247, (byte) 228, + (byte) 88, (byte) 5, (byte) 184, (byte) 179, (byte) 69, (byte) 6, (byte) 208, (byte) 44, (byte) 30, + (byte) 143, (byte) 202, (byte) 63, (byte) 15, (byte) 2, (byte) 193, (byte) 175, (byte) 189, (byte) 3, + (byte) 1, (byte) 19, (byte) 138, (byte) 107, (byte) 58, (byte) 145, (byte) 17, (byte) 65, (byte) 79, + (byte) 103, (byte) 220, (byte) 234, (byte) 151, (byte) 242, (byte) 207, (byte) 206, (byte) 240, (byte) 180, + (byte) 230, (byte) 115, (byte) 150, (byte) 172, (byte) 116, (byte) 34, (byte) 231, (byte) 173, (byte) 53, + (byte) 133, (byte) 226, (byte) 249, (byte) 55, (byte) 232, (byte) 28, (byte) 117, (byte) 223, (byte) 110, + (byte) 71, (byte) 241, (byte) 26, (byte) 113, (byte) 29, (byte) 41, (byte) 197, (byte) 137, (byte) 111, + (byte) 183, (byte) 98, (byte) 14, (byte) 170, (byte) 24, (byte) 190, (byte) 27, (byte) 252, (byte) 86, + (byte) 62, (byte) 75, (byte) 198, (byte) 210, (byte) 121, (byte) 32, (byte) 154, (byte) 219, (byte) 192, + (byte) 254, (byte) 120, (byte) 205, (byte) 90, (byte) 244, (byte) 31, (byte) 221, (byte) 168, (byte) 51, + (byte) 136, (byte) 7, (byte) 199, (byte) 49, (byte) 177, (byte) 18, (byte) 16, (byte) 89, (byte) 39, + (byte) 128, (byte) 236, (byte) 95, (byte) 96, (byte) 81, (byte) 127, (byte) 169, (byte) 25, (byte) 181, + (byte) 74, (byte) 13, (byte) 45, (byte) 229, (byte) 122, (byte) 159, (byte) 147, (byte) 201, (byte) 156, + (byte) 239, (byte) 160, (byte) 224, (byte) 59, (byte) 77, (byte) 174, (byte) 42, (byte) 245, (byte) 176, + (byte) 200, (byte) 235, (byte) 187, (byte) 60, (byte) 131, (byte) 83, (byte) 153, (byte) 97, (byte) 23, + (byte) 43, (byte) 4, (byte) 126, (byte) 186, (byte) 119, (byte) 214, (byte) 38, (byte) 225, (byte) 105, + (byte) 20, (byte) 99, (byte) 85, (byte) 33, (byte) 12, (byte) 125, }; + + // vector used in calculating key schedule (powers of x in GF(256)) + private static final int[] rcon = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, + 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 }; + + private static int shift(int r, int shift) { + return (r >>> shift) | (r << -shift); + } + + /* multiply four bytes in GF(2^8) by 'x' {02} in parallel */ + + private static final int m1 = 0x80808080; + private static final int m2 = 0x7f7f7f7f; + private static final int m3 = 0x0000001b; + private static final int m4 = 0xC0C0C0C0; + private static final int m5 = 0x3f3f3f3f; + + private static int FFmulX(int x) { + return (((x & m2) << 1) ^ (((x & m1) >>> 7) * m3)); + } + + private static int FFmulX2(int x) { + int t0 = (x & m5) << 2; + int t1 = (x & m4); + t1 ^= (t1 >>> 1); + return t0 ^ (t1 >>> 2) ^ (t1 >>> 5); + } + + /* + The following defines provide alternative definitions of FFmulX that might + give improved performance if a fast 32-bit multiply is not available. + + private int FFmulX(int x) { int u = x & m1; u |= (u >> 1); return ((x & m2) << 1) ^ ((u >>> 3) | (u >>> 6)); } + private static final int m4 = 0x1b1b1b1b; + private int FFmulX(int x) { int u = x & m1; return ((x & m2) << 1) ^ ((u - (u >>> 7)) & m4); } + + */ + + private static int mcol(int x) { + int t0, t1; + t0 = shift(x, 8); + t1 = x ^ t0; + return shift(t1, 16) ^ t0 ^ FFmulX(t1); + } + + private static int inv_mcol(int x) { + int t0, t1; + t0 = x; + t1 = t0 ^ shift(t0, 8); + t0 ^= FFmulX(t1); + t1 ^= FFmulX2(t0); + t0 ^= t1 ^ shift(t1, 16); + return t0; + } + + private static int subWord(int x) { + return (S[x & 255] & 255 | ((S[(x >> 8) & 255] & 255) << 8) | ((S[(x >> 16) & 255] & 255) << 16) + | S[(x >> 24) & 255] << 24); + } + + private static int littleEndianToInt(byte[] bs, int off) { + int n = bs[off] & 0xff; + n |= (bs[++off] & 0xff) << 8; + n |= (bs[++off] & 0xff) << 16; + n |= bs[++off] << 24; + return n; + } + + public static void intToLittleEndian(int n, byte[] bs, int off) { + bs[off] = (byte) (n); + bs[++off] = (byte) (n >>> 8); + bs[++off] = (byte) (n >>> 16); + bs[++off] = (byte) (n >>> 24); + } + + /** + * Calculate the necessary round keys + * The number of calculations depends on key size and block size + * AES specified a fixed block size of 128 bits and key sizes 128/192/256 bits + * This code is written assuming those are the only possible values + */ + private int[][] generateWorkingKey(byte[] key, boolean forEncryption) { + int keyLen = key.length; + if (keyLen < 16 || keyLen > 32 || (keyLen & 7) != 0) { + throw new IllegalArgumentException("Key length not 128/192/256 bits."); + } + + int KC = keyLen >>> 2; + ROUNDS = KC + 6; // This is not always true for the generalized Rijndael that allows larger block + // sizes + int[][] W = new int[ROUNDS + 1][4]; // 4 words in a block + + switch (KC) { + case 4: { + int col0 = littleEndianToInt(key, 0); + W[0][0] = col0; + int col1 = littleEndianToInt(key, 4); + W[0][1] = col1; + int col2 = littleEndianToInt(key, 8); + W[0][2] = col2; + int col3 = littleEndianToInt(key, 12); + W[0][3] = col3; + + for (int i = 1; i <= 10; ++i) { + int colx = subWord(shift(col3, 8)) ^ rcon[i - 1]; + col0 ^= colx; + W[i][0] = col0; + col1 ^= col0; + W[i][1] = col1; + col2 ^= col1; + W[i][2] = col2; + col3 ^= col2; + W[i][3] = col3; + } + + break; + } + case 6: { + int col0 = littleEndianToInt(key, 0); + W[0][0] = col0; + int col1 = littleEndianToInt(key, 4); + W[0][1] = col1; + int col2 = littleEndianToInt(key, 8); + W[0][2] = col2; + int col3 = littleEndianToInt(key, 12); + W[0][3] = col3; + + int col4 = littleEndianToInt(key, 16); + int col5 = littleEndianToInt(key, 20); + + int i = 1, rcon = 1, colx; + for (;;) { + W[i][0] = col4; + W[i][1] = col5; + colx = subWord(shift(col5, 8)) ^ rcon; + rcon <<= 1; + col0 ^= colx; + W[i][2] = col0; + col1 ^= col0; + W[i][3] = col1; + + col2 ^= col1; + W[i + 1][0] = col2; + col3 ^= col2; + W[i + 1][1] = col3; + col4 ^= col3; + W[i + 1][2] = col4; + col5 ^= col4; + W[i + 1][3] = col5; + + colx = subWord(shift(col5, 8)) ^ rcon; + rcon <<= 1; + col0 ^= colx; + W[i + 2][0] = col0; + col1 ^= col0; + W[i + 2][1] = col1; + col2 ^= col1; + W[i + 2][2] = col2; + col3 ^= col2; + W[i + 2][3] = col3; + + if ((i += 3) >= 13) { + break; + } + + col4 ^= col3; + col5 ^= col4; + } + + break; + } + case 8: { + int col0 = littleEndianToInt(key, 0); + W[0][0] = col0; + int col1 = littleEndianToInt(key, 4); + W[0][1] = col1; + int col2 = littleEndianToInt(key, 8); + W[0][2] = col2; + int col3 = littleEndianToInt(key, 12); + W[0][3] = col3; + + int col4 = littleEndianToInt(key, 16); + W[1][0] = col4; + int col5 = littleEndianToInt(key, 20); + W[1][1] = col5; + int col6 = littleEndianToInt(key, 24); + W[1][2] = col6; + int col7 = littleEndianToInt(key, 28); + W[1][3] = col7; + + int i = 2, rcon = 1, colx; + for (;;) { + colx = subWord(shift(col7, 8)) ^ rcon; + rcon <<= 1; + col0 ^= colx; + W[i][0] = col0; + col1 ^= col0; + W[i][1] = col1; + col2 ^= col1; + W[i][2] = col2; + col3 ^= col2; + W[i][3] = col3; + ++i; + + if (i >= 15) { + break; + } + + colx = subWord(col3); + col4 ^= colx; + W[i][0] = col4; + col5 ^= col4; + W[i][1] = col5; + col6 ^= col5; + W[i][2] = col6; + col7 ^= col6; + W[i][3] = col7; + ++i; + } + + break; + } + default: { + throw new IllegalStateException("Should never get here"); + } + } + + if (!forEncryption) { + for (int j = 1; j < ROUNDS; j++) { + for (int i = 0; i < 4; i++) { + W[j][i] = inv_mcol(W[j][i]); + } + } + } + + return W; + } + + private int ROUNDS; + private int[][] WorkingKey = null; + private boolean forEncryption; + + private static final int BLOCK_SIZE = 16; + + /** + * default constructor - 128 bit block size. + */ + public AESLightEngine() { + + } + + /** + * initialise an AES cipher. + * + * @param forEncryption whether or not we are for encryption. + * @param params the parameters required to set up the cipher. + * @exception IllegalArgumentException if the params argument is + * inappropriate. + */ + public void init(boolean forEncryption, byte[] key) { + WorkingKey = generateWorkingKey(key, forEncryption); + this.forEncryption = forEncryption; + return; + } + + public String getAlgorithmName() { + return "AES"; + } + + public int getBlockSize() { + return BLOCK_SIZE; + } + + public int processBlock(byte[] in, int inOff, byte[] out, int outOff) { + if (WorkingKey == null) { + throw new IllegalStateException("AES engine not initialised"); + } + + if (inOff > (in.length - BLOCK_SIZE)) { + throw new IndexOutOfBoundsException("input buffer too short"); + } + + if (outOff > (out.length - BLOCK_SIZE)) { + throw new IndexOutOfBoundsException("output buffer too short"); + } + + if (forEncryption) { + encryptBlock(in, inOff, out, outOff, WorkingKey); + } else { + decryptBlock(in, inOff, out, outOff, WorkingKey); + } + + return BLOCK_SIZE; + } + + public void reset() { + } + + private void encryptBlock(byte[] in, int inOff, byte[] out, int outOff, int[][] KW) { + int C0 = littleEndianToInt(in, inOff + 0); + int C1 = littleEndianToInt(in, inOff + 4); + int C2 = littleEndianToInt(in, inOff + 8); + int C3 = littleEndianToInt(in, inOff + 12); + + int t0 = C0 ^ KW[0][0]; + int t1 = C1 ^ KW[0][1]; + int t2 = C2 ^ KW[0][2]; + + int r = 1, r0, r1, r2, r3 = C3 ^ KW[0][3]; + while (r < ROUNDS - 1) { + r0 = mcol((S[t0 & 255] & 255) ^ ((S[(t1 >> 8) & 255] & 255) << 8) ^ ((S[(t2 >> 16) & 255] & 255) << 16) + ^ (S[(r3 >> 24) & 255] << 24)) ^ KW[r][0]; + r1 = mcol((S[t1 & 255] & 255) ^ ((S[(t2 >> 8) & 255] & 255) << 8) ^ ((S[(r3 >> 16) & 255] & 255) << 16) + ^ (S[(t0 >> 24) & 255] << 24)) ^ KW[r][1]; + r2 = mcol((S[t2 & 255] & 255) ^ ((S[(r3 >> 8) & 255] & 255) << 8) ^ ((S[(t0 >> 16) & 255] & 255) << 16) + ^ (S[(t1 >> 24) & 255] << 24)) ^ KW[r][2]; + r3 = mcol((S[r3 & 255] & 255) ^ ((S[(t0 >> 8) & 255] & 255) << 8) ^ ((S[(t1 >> 16) & 255] & 255) << 16) + ^ (S[(t2 >> 24) & 255] << 24)) ^ KW[r++][3]; + t0 = mcol((S[r0 & 255] & 255) ^ ((S[(r1 >> 8) & 255] & 255) << 8) ^ ((S[(r2 >> 16) & 255] & 255) << 16) + ^ (S[(r3 >> 24) & 255] << 24)) ^ KW[r][0]; + t1 = mcol((S[r1 & 255] & 255) ^ ((S[(r2 >> 8) & 255] & 255) << 8) ^ ((S[(r3 >> 16) & 255] & 255) << 16) + ^ (S[(r0 >> 24) & 255] << 24)) ^ KW[r][1]; + t2 = mcol((S[r2 & 255] & 255) ^ ((S[(r3 >> 8) & 255] & 255) << 8) ^ ((S[(r0 >> 16) & 255] & 255) << 16) + ^ (S[(r1 >> 24) & 255] << 24)) ^ KW[r][2]; + r3 = mcol((S[r3 & 255] & 255) ^ ((S[(r0 >> 8) & 255] & 255) << 8) ^ ((S[(r1 >> 16) & 255] & 255) << 16) + ^ (S[(r2 >> 24) & 255] << 24)) ^ KW[r++][3]; + } + + r0 = mcol((S[t0 & 255] & 255) ^ ((S[(t1 >> 8) & 255] & 255) << 8) ^ ((S[(t2 >> 16) & 255] & 255) << 16) + ^ (S[(r3 >> 24) & 255] << 24)) ^ KW[r][0]; + r1 = mcol((S[t1 & 255] & 255) ^ ((S[(t2 >> 8) & 255] & 255) << 8) ^ ((S[(r3 >> 16) & 255] & 255) << 16) + ^ (S[(t0 >> 24) & 255] << 24)) ^ KW[r][1]; + r2 = mcol((S[t2 & 255] & 255) ^ ((S[(r3 >> 8) & 255] & 255) << 8) ^ ((S[(t0 >> 16) & 255] & 255) << 16) + ^ (S[(t1 >> 24) & 255] << 24)) ^ KW[r][2]; + r3 = mcol((S[r3 & 255] & 255) ^ ((S[(t0 >> 8) & 255] & 255) << 8) ^ ((S[(t1 >> 16) & 255] & 255) << 16) + ^ (S[(t2 >> 24) & 255] << 24)) ^ KW[r++][3]; + + // the final round is a simple function of S + + C0 = (S[r0 & 255] & 255) ^ ((S[(r1 >> 8) & 255] & 255) << 8) ^ ((S[(r2 >> 16) & 255] & 255) << 16) + ^ (S[(r3 >> 24) & 255] << 24) ^ KW[r][0]; + C1 = (S[r1 & 255] & 255) ^ ((S[(r2 >> 8) & 255] & 255) << 8) ^ ((S[(r3 >> 16) & 255] & 255) << 16) + ^ (S[(r0 >> 24) & 255] << 24) ^ KW[r][1]; + C2 = (S[r2 & 255] & 255) ^ ((S[(r3 >> 8) & 255] & 255) << 8) ^ ((S[(r0 >> 16) & 255] & 255) << 16) + ^ (S[(r1 >> 24) & 255] << 24) ^ KW[r][2]; + C3 = (S[r3 & 255] & 255) ^ ((S[(r0 >> 8) & 255] & 255) << 8) ^ ((S[(r1 >> 16) & 255] & 255) << 16) + ^ (S[(r2 >> 24) & 255] << 24) ^ KW[r][3]; + + intToLittleEndian(C0, out, outOff + 0); + intToLittleEndian(C1, out, outOff + 4); + intToLittleEndian(C2, out, outOff + 8); + intToLittleEndian(C3, out, outOff + 12); + } + + private void decryptBlock(byte[] in, int inOff, byte[] out, int outOff, int[][] KW) { + int C0 = littleEndianToInt(in, inOff + 0); + int C1 = littleEndianToInt(in, inOff + 4); + int C2 = littleEndianToInt(in, inOff + 8); + int C3 = littleEndianToInt(in, inOff + 12); + + int t0 = C0 ^ KW[ROUNDS][0]; + int t1 = C1 ^ KW[ROUNDS][1]; + int t2 = C2 ^ KW[ROUNDS][2]; + + int r = ROUNDS - 1, r0, r1, r2, r3 = C3 ^ KW[ROUNDS][3]; + while (r > 1) { + r0 = inv_mcol((Si[t0 & 255] & 255) ^ ((Si[(r3 >> 8) & 255] & 255) << 8) + ^ ((Si[(t2 >> 16) & 255] & 255) << 16) ^ (Si[(t1 >> 24) & 255] << 24)) ^ KW[r][0]; + r1 = inv_mcol((Si[t1 & 255] & 255) ^ ((Si[(t0 >> 8) & 255] & 255) << 8) + ^ ((Si[(r3 >> 16) & 255] & 255) << 16) ^ (Si[(t2 >> 24) & 255] << 24)) ^ KW[r][1]; + r2 = inv_mcol((Si[t2 & 255] & 255) ^ ((Si[(t1 >> 8) & 255] & 255) << 8) + ^ ((Si[(t0 >> 16) & 255] & 255) << 16) ^ (Si[(r3 >> 24) & 255] << 24)) ^ KW[r][2]; + r3 = inv_mcol((Si[r3 & 255] & 255) ^ ((Si[(t2 >> 8) & 255] & 255) << 8) + ^ ((Si[(t1 >> 16) & 255] & 255) << 16) ^ (Si[(t0 >> 24) & 255] << 24)) ^ KW[r--][3]; + t0 = inv_mcol((Si[r0 & 255] & 255) ^ ((Si[(r3 >> 8) & 255] & 255) << 8) + ^ ((Si[(r2 >> 16) & 255] & 255) << 16) ^ (Si[(r1 >> 24) & 255] << 24)) ^ KW[r][0]; + t1 = inv_mcol((Si[r1 & 255] & 255) ^ ((Si[(r0 >> 8) & 255] & 255) << 8) + ^ ((Si[(r3 >> 16) & 255] & 255) << 16) ^ (Si[(r2 >> 24) & 255] << 24)) ^ KW[r][1]; + t2 = inv_mcol((Si[r2 & 255] & 255) ^ ((Si[(r1 >> 8) & 255] & 255) << 8) + ^ ((Si[(r0 >> 16) & 255] & 255) << 16) ^ (Si[(r3 >> 24) & 255] << 24)) ^ KW[r][2]; + r3 = inv_mcol((Si[r3 & 255] & 255) ^ ((Si[(r2 >> 8) & 255] & 255) << 8) + ^ ((Si[(r1 >> 16) & 255] & 255) << 16) ^ (Si[(r0 >> 24) & 255] << 24)) ^ KW[r--][3]; + } + + r0 = inv_mcol((Si[t0 & 255] & 255) ^ ((Si[(r3 >> 8) & 255] & 255) << 8) ^ ((Si[(t2 >> 16) & 255] & 255) << 16) + ^ (Si[(t1 >> 24) & 255] << 24)) ^ KW[r][0]; + r1 = inv_mcol((Si[t1 & 255] & 255) ^ ((Si[(t0 >> 8) & 255] & 255) << 8) ^ ((Si[(r3 >> 16) & 255] & 255) << 16) + ^ (Si[(t2 >> 24) & 255] << 24)) ^ KW[r][1]; + r2 = inv_mcol((Si[t2 & 255] & 255) ^ ((Si[(t1 >> 8) & 255] & 255) << 8) ^ ((Si[(t0 >> 16) & 255] & 255) << 16) + ^ (Si[(r3 >> 24) & 255] << 24)) ^ KW[r][2]; + r3 = inv_mcol((Si[r3 & 255] & 255) ^ ((Si[(t2 >> 8) & 255] & 255) << 8) ^ ((Si[(t1 >> 16) & 255] & 255) << 16) + ^ (Si[(t0 >> 24) & 255] << 24)) ^ KW[r][3]; + + // the final round's table is a simple function of Si + + C0 = (Si[r0 & 255] & 255) ^ ((Si[(r3 >> 8) & 255] & 255) << 8) ^ ((Si[(r2 >> 16) & 255] & 255) << 16) + ^ (Si[(r1 >> 24) & 255] << 24) ^ KW[0][0]; + C1 = (Si[r1 & 255] & 255) ^ ((Si[(r0 >> 8) & 255] & 255) << 8) ^ ((Si[(r3 >> 16) & 255] & 255) << 16) + ^ (Si[(r2 >> 24) & 255] << 24) ^ KW[0][1]; + C2 = (Si[r2 & 255] & 255) ^ ((Si[(r1 >> 8) & 255] & 255) << 8) ^ ((Si[(r0 >> 16) & 255] & 255) << 16) + ^ (Si[(r3 >> 24) & 255] << 24) ^ KW[0][2]; + C3 = (Si[r3 & 255] & 255) ^ ((Si[(r2 >> 8) & 255] & 255) << 8) ^ ((Si[(r1 >> 16) & 255] & 255) << 16) + ^ (Si[(r0 >> 24) & 255] << 24) ^ KW[0][3]; + + intToLittleEndian(C0, out, outOff + 0); + intToLittleEndian(C1, out, outOff + 4); + intToLittleEndian(C2, out, outOff + 8); + intToLittleEndian(C3, out, outOff + 12); + } + + private int bitsOfSecurity() { + if (WorkingKey == null) { + return 256; + } + return (WorkingKey.length - 7) << 5; + } +} \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/futures/ListenableFutureTask.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/futures/ListenableFutureTask.java index bbbb57c2..eb914064 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/futures/ListenableFutureTask.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/futures/ListenableFutureTask.java @@ -22,7 +22,7 @@ import java.util.concurrent.Executor; */ public class ListenableFutureTask extends FutureTask implements ListenableFuture { - private final List listeners = new ArrayList(); + private final List listeners = new ArrayList<>(); public ListenableFutureTask(Callable callable) { super(callable); @@ -54,7 +54,7 @@ public class ListenableFutureTask extends FutureTask implements Listenable } public static ListenableFutureTask create(Callable callableToSchedule) { - return new ListenableFutureTask(callableToSchedule); + return new ListenableFutureTask<>(callableToSchedule); } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/AbstractWebSocketClient.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/AbstractWebSocketClient.java new file mode 100644 index 00000000..ff588197 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/AbstractWebSocketClient.java @@ -0,0 +1,227 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public abstract class AbstractWebSocketClient implements IWebSocketClient { + + protected volatile int availableStringFrames = 0; + protected volatile int availableBinaryFrames = 0; + protected final List recievedPacketBuffer = new LinkedList<>(); + protected String currentURI; + + protected AbstractWebSocketClient(String currentURI) { + this.currentURI = currentURI; + } + + protected void addRecievedFrame(IWebSocketFrame frame) { + boolean str = frame.isString(); + synchronized(recievedPacketBuffer) { + recievedPacketBuffer.add(frame); + if(str) { + ++availableStringFrames; + }else { + ++availableBinaryFrames; + } + } + } + + @Override + public int availableFrames() { + synchronized(recievedPacketBuffer) { + return availableStringFrames + availableBinaryFrames; + } + } + + @Override + public IWebSocketFrame getNextFrame() { + synchronized(recievedPacketBuffer) { + if(!recievedPacketBuffer.isEmpty()) { + IWebSocketFrame frame = recievedPacketBuffer.remove(0); + if(frame.isString()) { + --availableStringFrames; + }else { + --availableBinaryFrames; + } + return frame; + }else { + return null; + } + } + } + + @Override + public List getNextFrames() { + synchronized(recievedPacketBuffer) { + if(!recievedPacketBuffer.isEmpty()) { + List ret = new ArrayList<>(recievedPacketBuffer); + recievedPacketBuffer.clear(); + availableStringFrames = 0; + availableBinaryFrames = 0; + return ret; + }else { + return null; + } + } + } + + @Override + public void clearFrames() { + synchronized(recievedPacketBuffer) { + recievedPacketBuffer.clear(); + } + } + + @Override + public int availableStringFrames() { + synchronized(recievedPacketBuffer) { + return availableStringFrames; + } + } + + @Override + public IWebSocketFrame getNextStringFrame() { + synchronized(recievedPacketBuffer) { + if(availableStringFrames > 0) { + Iterator itr = recievedPacketBuffer.iterator(); + while(itr.hasNext()) { + IWebSocketFrame r = itr.next(); + if(r.isString()) { + itr.remove(); + --availableStringFrames; + return r; + } + } + availableStringFrames = 0; + return null; + }else { + return null; + } + } + } + + @Override + public List getNextStringFrames() { + synchronized(recievedPacketBuffer) { + if(availableStringFrames > 0) { + List ret = new ArrayList<>(availableStringFrames); + Iterator itr = recievedPacketBuffer.iterator(); + while(itr.hasNext()) { + IWebSocketFrame r = itr.next(); + if(r.isString()) { + itr.remove(); + ret.add(r); + } + } + availableStringFrames = 0; + return ret; + }else { + return null; + } + } + } + + @Override + public void clearStringFrames() { + synchronized(recievedPacketBuffer) { + if(availableStringFrames > 0) { + Iterator itr = recievedPacketBuffer.iterator(); + while(itr.hasNext()) { + IWebSocketFrame r = itr.next(); + if(r.isString()) { + itr.remove(); + } + } + availableStringFrames = 0; + } + } + } + + @Override + public int availableBinaryFrames() { + synchronized(recievedPacketBuffer) { + return availableBinaryFrames; + } + } + + @Override + public IWebSocketFrame getNextBinaryFrame() { + synchronized(recievedPacketBuffer) { + if(availableBinaryFrames > 0) { + Iterator itr = recievedPacketBuffer.iterator(); + while(itr.hasNext()) { + IWebSocketFrame r = itr.next(); + if(!r.isString()) { + itr.remove(); + --availableBinaryFrames; + return r; + } + } + availableBinaryFrames = 0; + return null; + }else { + return null; + } + } + } + + @Override + public List getNextBinaryFrames() { + synchronized(recievedPacketBuffer) { + if(availableBinaryFrames > 0) { + List ret = new ArrayList<>(availableBinaryFrames); + Iterator itr = recievedPacketBuffer.iterator(); + while(itr.hasNext()) { + IWebSocketFrame r = itr.next(); + if(!r.isString()) { + itr.remove(); + ret.add(r); + } + } + availableBinaryFrames = 0; + return ret; + }else { + return null; + } + } + } + + @Override + public void clearBinaryFrames() { + synchronized(recievedPacketBuffer) { + if(availableBinaryFrames > 0) { + Iterator itr = recievedPacketBuffer.iterator(); + while(itr.hasNext()) { + IWebSocketFrame r = itr.next(); + if(!r.isString()) { + itr.remove(); + } + } + availableBinaryFrames = 0; + } + } + } + + @Override + public String getCurrentURI() { + return currentURI; + } + +} diff --git a/sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformBufferFunctions.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EaglerMissingResourceException.java similarity index 63% rename from sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformBufferFunctions.java rename to sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EaglerMissingResourceException.java index c3b65936..a4791d18 100644 --- a/sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformBufferFunctions.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EaglerMissingResourceException.java @@ -1,10 +1,7 @@ package net.lax1dude.eaglercraft.v1_8.internal; -import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; -import net.lax1dude.eaglercraft.v1_8.internal.buffer.IntBuffer; - /** - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -18,17 +15,21 @@ import net.lax1dude.eaglercraft.v1_8.internal.buffer.IntBuffer; * POSSIBILITY OF SUCH DAMAGE. * */ -public class PlatformBufferFunctions { - - public static void put(ByteBuffer newBuffer, ByteBuffer flip) { - newBuffer.put(flip); +public class EaglerMissingResourceException extends RuntimeException { + + public EaglerMissingResourceException() { } - public static void put(IntBuffer intBuffer, int index, int[] data) { - int p = intBuffer.position(); - intBuffer.position(index); - intBuffer.put(data); - intBuffer.position(p); + public EaglerMissingResourceException(String message, Throwable cause) { + super(message, cause); } - + + public EaglerMissingResourceException(String message) { + super(message); + } + + public EaglerMissingResourceException(Throwable cause) { + super(cause); + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumFireKeyboardEvent.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumFireKeyboardEvent.java new file mode 100644 index 00000000..b74e605e --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumFireKeyboardEvent.java @@ -0,0 +1,20 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public enum EnumFireKeyboardEvent { + KEY_DOWN, KEY_UP, KEY_REPEAT; +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumFireMouseEvent.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumFireMouseEvent.java new file mode 100644 index 00000000..5e29c32e --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumFireMouseEvent.java @@ -0,0 +1,20 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public enum EnumFireMouseEvent { + MOUSE_DOWN, MOUSE_UP, MOUSE_MOVE, MOUSE_WHEEL; +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformANGLE.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformANGLE.java index 051b5f53..43fae4df 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformANGLE.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformANGLE.java @@ -18,6 +18,7 @@ package net.lax1dude.eaglercraft.v1_8.internal; public enum EnumPlatformANGLE { DEFAULT(225281 /* GLFW_ANGLE_PLATFORM_TYPE_NONE */, "default", "Default"), + D3D9(225284 /* GLFW_ANGLE_PLATFORM_TYPE_D3D9 */, "d3d9", "Direct3D9"), D3D11(225285 /* GLFW_ANGLE_PLATFORM_TYPE_D3D11 */, "d3d11", "Direct3D11"), OPENGL(225282 /* GLFW_ANGLE_PLATFORM_TYPE_OPENGL */, "opengl", "OpenGL"), OPENGLES(225283 /* GLFW_ANGLE_PLATFORM_TYPE_OPENGLES */, "opengles", "OpenGL ES"), @@ -41,6 +42,8 @@ public enum EnumPlatformANGLE { public static EnumPlatformANGLE fromId(String id) { if(id.equals("d3d11") || id.equals("d3d") || id.equals("dx11")) { return D3D11; + }else if(id.equals("d3d9") || id.equals("dx9")) { + return D3D9; }else if(id.equals("opengl")) { return OPENGL; }else if(id.equals("opengles")) { @@ -58,6 +61,8 @@ public enum EnumPlatformANGLE { str = str.toLowerCase(); if(str.contains("direct3d11") || str.contains("d3d11")) { return D3D11; + }else if(str.contains("direct3d9") || str.contains("d3d9")) { + return D3D9; }else if(str.contains("opengl es")) { return OPENGLES; }else if(str.contains("opengl")) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformAgent.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformAgent.java index 1ea4f444..dc0171e3 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformAgent.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformAgent.java @@ -35,12 +35,15 @@ public enum EnumPlatformAgent { } public static EnumPlatformAgent getFromUA(String ua) { + if(ua == null) { + return UNKNOWN; + } ua = " " + ua.toLowerCase(); if(ua.contains(" edg/")) { return EDGE; }else if(ua.contains(" opr/")) { return OPERA; - }else if(ua.contains(" chrome/")) { + }else if(ua.contains(" chrome/") || ua.contains(" chromium/")) { return CHROME; }else if(ua.contains(" firefox/")) { return FIREFOX; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformOS.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformOS.java index b42f7321..bfa1eff3 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformOS.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumPlatformOS.java @@ -42,6 +42,9 @@ public enum EnumPlatformOS { } public static EnumPlatformOS getFromJVM(String osNameProperty) { + if(osNameProperty == null) { + return OTHER; + } osNameProperty = osNameProperty.toLowerCase(); if(osNameProperty.contains("chrome")) { return CHROMEBOOK_LINUX; @@ -57,6 +60,9 @@ public enum EnumPlatformOS { } public static EnumPlatformOS getFromUA(String ua) { + if(ua == null) { + return OTHER; + } ua = " " + ua.toLowerCase(); if(ua.contains(" cros")) { return CHROMEBOOK_LINUX; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket05ClientSuccess.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumTouchEvent.java similarity index 55% rename from sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket05ClientSuccess.java rename to sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumTouchEvent.java index 240e9644..a94c6537 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket05ClientSuccess.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumTouchEvent.java @@ -1,11 +1,7 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +package net.lax1dude.eaglercraft.v1_8.internal; /** - * Copyright (c) 2022 lax1dude. All Rights Reserved. + * Copyright (c) 2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -19,27 +15,29 @@ import java.io.IOException; * POSSIBILITY OF SUCH DAMAGE. * */ -public class IPacket05ClientSuccess extends IPacket { +public enum EnumTouchEvent { + TOUCHSTART(0), TOUCHMOVE(1), TOUCHEND(2); - public String clientId; + public final int id; - public IPacket05ClientSuccess() { + private EnumTouchEvent(int id) { + this.id = id; } - public IPacket05ClientSuccess(String clientId) { - this.clientId = clientId; - } - - public void read(DataInputStream input) throws IOException { - clientId = readASCII8(input); - } - - public void write(DataOutputStream output) throws IOException { - writeASCII8(output, clientId); + public static EnumTouchEvent getById(int id) { + if(id >= 0 && id < lookup.length) { + return lookup[id]; + }else { + return null; + } } - public int packetLength() { - return 1 + clientId.length(); - } + private static final EnumTouchEvent[] lookup = new EnumTouchEvent[3]; + static { + EnumTouchEvent[] v = values(); + for(int i = 0; i < v.length; ++i) { + lookup[v[i].id] = v[i]; + } + } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumWebViewContentMode.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumWebViewContentMode.java new file mode 100644 index 00000000..75ef2a4e --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/EnumWebViewContentMode.java @@ -0,0 +1,20 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public enum EnumWebViewContentMode { + URL_BASED, BLOB_BASED; +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/GLObjectMap.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/GLObjectMap.java index 2c5acef2..680c1e09 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/GLObjectMap.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/GLObjectMap.java @@ -71,4 +71,11 @@ public class GLObjectMap { values = new Object[size]; System.arraycopy(oldValues, 0, values, 0, oldSize); } + + public void clear() { + if(allocatedObjects == 0) return; + values = new Object[size]; + insertIndex = 0; + allocatedObjects = 0; + } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/GamepadConstants.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/GamepadConstants.java new file mode 100644 index 00000000..fbc04a55 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/GamepadConstants.java @@ -0,0 +1,134 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GamepadConstants { + + private static final String[] buttonNames = new String[24]; + private static final String[] axisNames = new String[4]; + private static final int[] eaglerButtonsToGLFW = new int[24]; + private static final int[] glfwButtonsToEagler = new int[24]; + + public static final int GAMEPAD_NONE = -1; + public static final int GAMEPAD_A = 0; + public static final int GAMEPAD_B = 1; + public static final int GAMEPAD_X = 2; + public static final int GAMEPAD_Y = 3; + public static final int GAMEPAD_LEFT_BUTTON = 4; + public static final int GAMEPAD_RIGHT_BUTTON = 5; + public static final int GAMEPAD_LEFT_TRIGGER = 6; + public static final int GAMEPAD_RIGHT_TRIGGER = 7; + public static final int GAMEPAD_BACK = 8; + public static final int GAMEPAD_START = 9; + public static final int GAMEPAD_LEFT_STICK_BUTTON = 10; + public static final int GAMEPAD_RIGHT_STICK_BUTTON = 11; + public static final int GAMEPAD_DPAD_UP = 12; + public static final int GAMEPAD_DPAD_DOWN = 13; + public static final int GAMEPAD_DPAD_LEFT = 14; + public static final int GAMEPAD_DPAD_RIGHT = 15; + public static final int GAMEPAD_GUIDE = 16; + + public static final int GAMEPAD_AXIS_NONE = -1; + public static final int GAMEPAD_AXIS_LEFT_STICK_X = 0; + public static final int GAMEPAD_AXIS_LEFT_STICK_Y = 1; + public static final int GAMEPAD_AXIS_RIGHT_STICK_X = 2; + public static final int GAMEPAD_AXIS_RIGHT_STICK_Y = 3; + + private static final int GLFW_GAMEPAD_BUTTON_A = 0, GLFW_GAMEPAD_BUTTON_B = 1, GLFW_GAMEPAD_BUTTON_X = 2, + GLFW_GAMEPAD_BUTTON_Y = 3, GLFW_GAMEPAD_BUTTON_LEFT_BUMPER = 4, GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER = 5, + GLFW_GAMEPAD_BUTTON_BACK = 6, GLFW_GAMEPAD_BUTTON_START = 7, GLFW_GAMEPAD_BUTTON_GUIDE = 8, + GLFW_GAMEPAD_BUTTON_LEFT_THUMB = 9, GLFW_GAMEPAD_BUTTON_RIGHT_THUMB = 10, GLFW_GAMEPAD_BUTTON_DPAD_UP = 11, + GLFW_GAMEPAD_BUTTON_DPAD_RIGHT = 12, GLFW_GAMEPAD_BUTTON_DPAD_DOWN = 13, GLFW_GAMEPAD_BUTTON_DPAD_LEFT = 14; + + private static void registerBtn(int eaglerBtn, int glfwBtn, String name) { + if(eaglerButtonsToGLFW[eaglerBtn] != 0) throw new IllegalArgumentException("Duplicate eaglerButtonsToGLFW entry: " + eaglerBtn + " -> " + glfwBtn); + if(glfwBtn != -1 && glfwButtonsToEagler[glfwBtn] != 0) throw new IllegalArgumentException("Duplicate glfwButtonsToEAGLER entry: " + glfwBtn + " -> " + eaglerBtn); + eaglerButtonsToGLFW[eaglerBtn] = glfwBtn; + if(glfwBtn != -1) glfwButtonsToEagler[glfwBtn] = eaglerBtn; + if(buttonNames[eaglerBtn] != null) throw new IllegalArgumentException("Duplicate buttonNames entry: " + eaglerBtn); + buttonNames[eaglerBtn] = name; + } + + private static void registerAxis(int eaglerAxis, String name) { + if(axisNames[eaglerAxis] != null) throw new IllegalArgumentException("Duplicate axisNames entry: " + eaglerAxis); + axisNames[eaglerAxis] = name; + } + + static { + registerBtn(GAMEPAD_A, GLFW_GAMEPAD_BUTTON_A, "A"); + registerBtn(GAMEPAD_B, GLFW_GAMEPAD_BUTTON_B, "B"); + registerBtn(GAMEPAD_X, GLFW_GAMEPAD_BUTTON_X, "X"); + registerBtn(GAMEPAD_Y, GLFW_GAMEPAD_BUTTON_Y, "Y"); + registerBtn(GAMEPAD_LEFT_BUTTON, GLFW_GAMEPAD_BUTTON_LEFT_BUMPER, "Left Button"); + registerBtn(GAMEPAD_LEFT_TRIGGER, -1, "Left Trigger"); + registerBtn(GAMEPAD_RIGHT_BUTTON, GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER, "Right Button"); + registerBtn(GAMEPAD_RIGHT_TRIGGER, -1, "Right Trigger"); + registerBtn(GAMEPAD_BACK, GLFW_GAMEPAD_BUTTON_BACK, "Back"); + registerBtn(GAMEPAD_START, GLFW_GAMEPAD_BUTTON_START, "Start"); + registerBtn(GAMEPAD_LEFT_STICK_BUTTON, GLFW_GAMEPAD_BUTTON_LEFT_THUMB, "L. Stick Button"); + registerBtn(GAMEPAD_RIGHT_STICK_BUTTON, GLFW_GAMEPAD_BUTTON_RIGHT_THUMB, "R. Stick Button"); + registerBtn(GAMEPAD_DPAD_UP, GLFW_GAMEPAD_BUTTON_DPAD_UP, "D-Pad Up"); + registerBtn(GAMEPAD_DPAD_DOWN, GLFW_GAMEPAD_BUTTON_DPAD_DOWN, "D-Pad Down"); + registerBtn(GAMEPAD_DPAD_LEFT, GLFW_GAMEPAD_BUTTON_DPAD_LEFT, "D-Pad Left"); + registerBtn(GAMEPAD_DPAD_RIGHT, GLFW_GAMEPAD_BUTTON_DPAD_RIGHT, "D-Pad Right"); + registerBtn(GAMEPAD_GUIDE, GLFW_GAMEPAD_BUTTON_GUIDE, "Guide"); + registerAxis(GAMEPAD_AXIS_LEFT_STICK_X, "Left Stick X"); + registerAxis(GAMEPAD_AXIS_LEFT_STICK_Y, "Left Stick Y"); + registerAxis(GAMEPAD_AXIS_RIGHT_STICK_X, "Right Stick X"); + registerAxis(GAMEPAD_AXIS_RIGHT_STICK_Y, "Right Stick Y"); + } + + public static int getEaglerButtonFromBrowser(int button) { + return button; + } + + public static int getBrowserButtonFromEagler(int button) { + return button; + } + + public static int getEaglerButtonFromGLFW(int button) { + if(button >= 0 && button < glfwButtonsToEagler.length) { + return glfwButtonsToEagler[button]; + }else { + return -1; + } + } + + public static int getGLFWButtonFromEagler(int button) { + if(button >= 0 && button < eaglerButtonsToGLFW.length) { + return eaglerButtonsToGLFW[button]; + }else { + return -1; + } + } + + public static String getButtonName(int button) { + if(button >= 0 && button < buttonNames.length) { + return buttonNames[button]; + }else { + return "Button " + button; + } + } + + public static String getAxisName(int button) { + if(button >= 0 && button < axisNames.length) { + return axisNames[button]; + }else { + return "Axis " + button; + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IClientConfigAdapter.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IClientConfigAdapter.java index f28f78a4..fa6f350e 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IClientConfigAdapter.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IClientConfigAdapter.java @@ -26,10 +26,12 @@ public interface IClientConfigAdapter { public final String name; public final String addr; + public final boolean hideAddress; - public DefaultServer(String name, String addr) { + public DefaultServer(String name, String addr, boolean hideAddress) { this.name = name; this.addr = addr; + this.hideAddress = hideAddress; } } @@ -76,6 +78,24 @@ public interface IClientConfigAdapter { boolean isEnableMinceraft(); + boolean isEnableServerCookies(); + + boolean isAllowServerRedirects(); + + boolean isOpenDebugConsoleOnLaunch(); + + boolean isForceWebViewSupport(); + + boolean isEnableWebViewCSP(); + + boolean isAllowBootMenu(); + + boolean isForceProfanityFilter(); + + boolean isEaglerNoDelay(); + + boolean isRamdiskMode(); + IClientConfigAdapterHooks getHooks(); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IClientConfigAdapterHooks.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IClientConfigAdapterHooks.java index e6d0a545..888e36f5 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IClientConfigAdapterHooks.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IClientConfigAdapterHooks.java @@ -25,4 +25,6 @@ public interface IClientConfigAdapterHooks { void callCrashReportHook(String crashReport, Consumer customMessageCB); + void callScreenChangedHook(String screenName, int scaledWidth, int scaledHeight, int realWidth, int realHeight, int scaleFactor); + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IEaglerFilesystem.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IEaglerFilesystem.java new file mode 100644 index 00000000..8ec0faf4 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IEaglerFilesystem.java @@ -0,0 +1,46 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public interface IEaglerFilesystem { + + String getFilesystemName(); + + String getInternalDBName(); + + boolean isRamdisk(); + + boolean eaglerDelete(String pathName); + + ByteBuffer eaglerRead(String pathName); + + void eaglerWrite(String pathName, ByteBuffer data); + + boolean eaglerExists(String pathName); + + boolean eaglerMove(String pathNameOld, String pathNameNew); + + int eaglerCopy(String pathNameOld, String pathNameNew); + + int eaglerSize(String pathName); + + void eaglerIterate(String pathName, VFSFilenameIterator itr, boolean recursive); + + void closeHandle(); + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IServerQuery.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IServerQuery.java index ba97b26d..fc331c75 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IServerQuery.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IServerQuery.java @@ -2,6 +2,8 @@ package net.lax1dude.eaglercraft.v1_8.internal; import org.json.JSONObject; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; + /** * Copyright (c) 2022 lax1dude. All Rights Reserved. * @@ -42,6 +44,8 @@ public interface IServerQuery { } + void update(); + void send(String str); default void send(JSONObject json) { @@ -73,8 +77,8 @@ public interface IServerQuery { EnumServerRateLimit getRateLimit(); default boolean awaitResponseAvailable(long timeout) { - long start = System.currentTimeMillis(); - while(isOpen() && responsesAvailable() <= 0 && (timeout <= 0l || System.currentTimeMillis() - start < timeout)) { + long start = EagRuntime.steadyTimeMillis(); + while(isOpen() && responsesAvailable() <= 0 && (timeout <= 0l || EagRuntime.steadyTimeMillis() - start < timeout)) { try { Thread.sleep(0l, 250000); } catch (InterruptedException e) { @@ -88,8 +92,8 @@ public interface IServerQuery { } default boolean awaitResponseBinaryAvailable(long timeout) { - long start = System.currentTimeMillis(); - while(isOpen() && binaryResponsesAvailable() <= 0 && (timeout <= 0l || System.currentTimeMillis() - start < timeout)) { + long start = EagRuntime.steadyTimeMillis(); + while(isOpen() && binaryResponsesAvailable() <= 0 && (timeout <= 0l || EagRuntime.steadyTimeMillis() - start < timeout)) { try { Thread.sleep(0l, 250000); } catch (InterruptedException e) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket07LocalWorlds.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IWebSocketClient.java similarity index 50% rename from sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket07LocalWorlds.java rename to sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IWebSocketClient.java index 923e1798..85aad5aa 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket07LocalWorlds.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IWebSocketClient.java @@ -1,12 +1,9 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; +package net.lax1dude.eaglercraft.v1_8.internal; -import java.io.DataInputStream; -import java.io.IOException; -import java.util.ArrayList; import java.util.List; /** - * Copyright (c) 2022 lax1dude. All Rights Reserved. + * Copyright (c) 2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -20,31 +17,46 @@ import java.util.List; * POSSIBILITY OF SUCH DAMAGE. * */ -public class IPacket07LocalWorlds extends IPacket { - - public static class LocalWorld { - - public final String worldName; - public final String worldCode; - - public LocalWorld(String worldName, String worldCode) { - this.worldName = worldName; - this.worldCode = worldCode; - } - - } - - public final List worldsList; - - public IPacket07LocalWorlds() { - this.worldsList = new ArrayList(); - } +public interface IWebSocketClient { + + EnumEaglerConnectionState getState(); + + boolean connectBlocking(int timeoutMS); + + boolean isOpen(); + + boolean isClosed(); + + void close(); + + int availableFrames(); + + IWebSocketFrame getNextFrame(); + + List getNextFrames(); + + void clearFrames(); + + int availableStringFrames(); + + IWebSocketFrame getNextStringFrame(); + + List getNextStringFrames(); + + void clearStringFrames(); + + int availableBinaryFrames(); + + IWebSocketFrame getNextBinaryFrame(); + + List getNextBinaryFrames(); + + void clearBinaryFrames(); + + void send(String str); + + void send(byte[] bytes); + + String getCurrentURI(); - public void read(DataInputStream input) throws IOException { - int l = input.read(); - for(int i = 0; i < l; ++i) { - worldsList.add(new LocalWorld(readASCII8(input), readASCII8(input))); - } - } - } diff --git a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformBufferFunctions.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IWebSocketFrame.java similarity index 62% rename from sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformBufferFunctions.java rename to sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IWebSocketFrame.java index 6cab7cc5..7ecec0f1 100644 --- a/sources/lwjgl/java/net/lax1dude/eaglercraft/v1_8/internal/PlatformBufferFunctions.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/IWebSocketFrame.java @@ -1,10 +1,9 @@ package net.lax1dude.eaglercraft.v1_8.internal; -import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; -import net.lax1dude.eaglercraft.v1_8.internal.buffer.IntBuffer; +import java.io.InputStream; /** - * Copyright (c) 2022-2023 lax1dude, ayunami2000. All Rights Reserved. + * Copyright (c) 2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -18,17 +17,18 @@ import net.lax1dude.eaglercraft.v1_8.internal.buffer.IntBuffer; * POSSIBILITY OF SUCH DAMAGE. * */ -public class PlatformBufferFunctions { +public interface IWebSocketFrame { - public static void put(ByteBuffer newBuffer, ByteBuffer flip) { - newBuffer.put(flip); - } + boolean isString(); + + String getString(); + + byte[] getByteArray(); + + InputStream getInputStream(); + + int getLength(); + + long getTimestamp(); - public static void put(IntBuffer intBuffer, int index, int[] data) { - int p = intBuffer.position(); - intBuffer.position(index); - intBuffer.put(data); - intBuffer.position(p); - } - } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/QueryResponse.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/QueryResponse.java index 3f95a0a6..f069acbe 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/QueryResponse.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/QueryResponse.java @@ -2,6 +2,8 @@ package net.lax1dude.eaglercraft.v1_8.internal; import org.json.JSONObject; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; + /** * Copyright (c) 2022 lax1dude. All Rights Reserved. * @@ -37,7 +39,7 @@ public class QueryResponse { this.serverBrand = obj.getString("brand"); this.serverName = obj.getString("name"); this.serverTime = obj.getLong("time"); - this.clientTime = System.currentTimeMillis(); + this.clientTime = EagRuntime.steadyTimeMillis(); this.serverCracked = obj.optBoolean("cracked", false); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/RamdiskFilesystemImpl.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/RamdiskFilesystemImpl.java new file mode 100644 index 00000000..9302a899 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/RamdiskFilesystemImpl.java @@ -0,0 +1,131 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +import java.util.Map; +import java.util.TreeMap; + +import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class RamdiskFilesystemImpl implements IEaglerFilesystem { + + protected final String filesystemName; + protected final Map filesystemMap = new TreeMap<>(); + + public RamdiskFilesystemImpl(String filesystemName) { + this.filesystemName = filesystemName; + } + + @Override + public String getFilesystemName() { + return filesystemName; + } + + @Override + public String getInternalDBName() { + return "ramdisk:" + filesystemName; + } + + @Override + public boolean isRamdisk() { + return true; + } + + @Override + public boolean eaglerDelete(String pathName) { + return filesystemMap.remove(pathName) != null; + } + + @Override + public ByteBuffer eaglerRead(String pathName) { + byte[] data = filesystemMap.get(pathName); + if(data != null) { + ByteBuffer buf = PlatformRuntime.castPrimitiveByteArray(data); + if(buf == null) { + buf = PlatformRuntime.allocateByteBuffer(data.length); + buf.put(data); + buf.flip(); + } + return buf; + }else { + return null; + } + } + + @Override + public void eaglerWrite(String pathName, ByteBuffer data) { + byte[] arr = PlatformRuntime.castNativeByteBuffer(data); + if(arr == null) { + arr = new byte[data.remaining()]; + int i = data.position(); + data.get(arr); + data.position(i); + } + filesystemMap.put(pathName, arr); + } + + @Override + public boolean eaglerExists(String pathName) { + return filesystemMap.containsKey(pathName); + } + + @Override + public boolean eaglerMove(String pathNameOld, String pathNameNew) { + byte[] dat = filesystemMap.remove(pathNameOld); + if(dat != null) { + filesystemMap.put(pathNameNew, dat); + return true; + } + return false; + } + + @Override + public int eaglerCopy(String pathNameOld, String pathNameNew) { + byte[] dat = filesystemMap.get(pathNameOld); + if(dat != null) { + filesystemMap.put(pathNameNew, dat); + return dat.length; + } + return -1; + } + + @Override + public int eaglerSize(String pathName) { + byte[] dat = filesystemMap.get(pathName); + return dat != null ? dat.length : -1; + } + + @Override + public void eaglerIterate(String pathName, VFSFilenameIterator itr, boolean recursive) { + if(!recursive) { + eaglerIterate(pathName, new VFSFilenameIteratorNonRecursive(itr, + VFSFilenameIteratorNonRecursive.countSlashes(pathName) + 1), true); + }else { + boolean b = pathName.length() == 0; + for(String key : filesystemMap.keySet()) { + if(b || key.startsWith(pathName)) { + itr.next(key); + } + } + } + } + + @Override + public void closeHandle() { + filesystemMap.clear(); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/ScreenRecordParameters.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/ScreenRecordParameters.java new file mode 100644 index 00000000..0c4d25ba --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/ScreenRecordParameters.java @@ -0,0 +1,37 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +import net.lax1dude.eaglercraft.v1_8.recording.EnumScreenRecordingCodec; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ScreenRecordParameters { + + public final EnumScreenRecordingCodec codec; + public final int resolutionDivisior; + public final int videoBitsPerSecond; + public final int audioBitsPerSecond; + public final int captureFrameRate; + + public ScreenRecordParameters(EnumScreenRecordingCodec codec, int resolutionDivisior, int videoBitsPerSecond, + int audioBitsPerSecond, int captureFrameRate) { + this.codec = codec; + this.resolutionDivisior = resolutionDivisior; + this.videoBitsPerSecond = videoBitsPerSecond; + this.audioBitsPerSecond = audioBitsPerSecond; + this.captureFrameRate = captureFrameRate; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket69Pong.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/VFSFilenameIteratorNonRecursive.java similarity index 53% rename from sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket69Pong.java rename to sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/VFSFilenameIteratorNonRecursive.java index 589ca92a..d10a3afb 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket69Pong.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/VFSFilenameIteratorNonRecursive.java @@ -1,10 +1,7 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -import java.io.DataInputStream; -import java.io.IOException; +package net.lax1dude.eaglercraft.v1_8.internal; /** - * Copyright (c) 2022 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -18,28 +15,33 @@ import java.io.IOException; * POSSIBILITY OF SUCH DAMAGE. * */ -public class IPacket69Pong extends IPacket { +public class VFSFilenameIteratorNonRecursive implements VFSFilenameIterator { - public int protcolVersion; - public String comment; - public String brand; - - public IPacket69Pong(int protcolVersion, String comment, String brand) { - if(comment.length() > 255) { - comment = comment.substring(0, 256); + private final VFSFilenameIterator child; + private final int pathCount; + + public VFSFilenameIteratorNonRecursive(VFSFilenameIterator child, int pathCount) { + this.child = child; + this.pathCount = pathCount; + } + + @Override + public void next(String entry) { + int i = countSlashes(entry); + if(i == pathCount) { + child.next(entry); } - this.protcolVersion = protcolVersion; - this.comment = comment; - this.brand = brand; } - public IPacket69Pong() { + public static int countSlashes(String str) { + if(str.length() == 0) return -1; + int j = 0; + for(int i = 0, l = str.length(); i < l; ++i) { + if(str.charAt(i) == '/') { + ++j; + } + } + return j; } - - public void read(DataInputStream output) throws IOException { - protcolVersion = output.read(); - comment = readASCII8(output); - brand = readASCII8(output); - } - + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/WebViewOptions.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/WebViewOptions.java new file mode 100644 index 00000000..6015c73f --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/WebViewOptions.java @@ -0,0 +1,67 @@ +package net.lax1dude.eaglercraft.v1_8.internal; + +import java.net.URI; +import java.nio.charset.StandardCharsets; + +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class WebViewOptions { + + public EnumWebViewContentMode contentMode = EnumWebViewContentMode.BLOB_BASED; + public String fallbackTitle = "WebView"; + public boolean scriptEnabled = false; + public boolean strictCSPEnable = true; + public boolean serverMessageAPIEnabled = false; + public URI url = null; + public byte[] blob = null; + public EaglercraftUUID permissionsOriginUUID = null; + + public WebViewOptions() { + } + + public WebViewOptions(boolean script, boolean serverMessageAPIEnabled, boolean strictCSPEnable, URI url) { + this.contentMode = EnumWebViewContentMode.URL_BASED; + this.scriptEnabled = script; + this.strictCSPEnable = strictCSPEnable; + this.serverMessageAPIEnabled = serverMessageAPIEnabled; + this.url = url; + this.permissionsOriginUUID = getURLOriginUUID(url); + } + + public WebViewOptions(boolean script, boolean serverMessageAPIEnabled, boolean strictCSPEnable, byte[] data, EaglercraftUUID permissionsOriginUUID) { + this.contentMode = EnumWebViewContentMode.BLOB_BASED; + this.scriptEnabled = script; + this.strictCSPEnable = strictCSPEnable; + this.serverMessageAPIEnabled = serverMessageAPIEnabled; + this.blob = data; + this.permissionsOriginUUID = permissionsOriginUUID; + } + + public static EaglercraftUUID getURLOriginUUID(URI url) { + return EaglercraftUUID.nameUUIDFromBytes(("URLOrigin:" + url.toString()).getBytes(StandardCharsets.UTF_8)); + } + + public static EaglercraftUUID getEmbedOriginUUID(byte[] sha256) { + byte[] vigg = "BlobOrigin:".getBytes(StandardCharsets.UTF_8); + byte[] eagler = new byte[sha256.length + vigg.length]; + System.arraycopy(vigg, 0, eagler, 0, vigg.length); + System.arraycopy(sha256, 0, eagler, vigg.length, sha256.length); + return EaglercraftUUID.nameUUIDFromBytes(eagler); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/Buffer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/Buffer.java index 0aff7fc0..ee27b232 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/Buffer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/Buffer.java @@ -41,14 +41,14 @@ public interface Buffer { boolean hasRemaining(); - boolean isReadOnly(); - boolean hasArray(); Object array(); - int arrayOffset(); - boolean isDirect(); + static IndexOutOfBoundsException makeIOOBE(int idx) { + return new IndexOutOfBoundsException("Index out of range: " + idx); + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/ByteBuffer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/ByteBuffer.java index b301853c..4ec39b18 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/ByteBuffer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/ByteBuffer.java @@ -18,12 +18,8 @@ package net.lax1dude.eaglercraft.v1_8.internal.buffer; */ public interface ByteBuffer extends Buffer { - ByteBuffer slice(); - ByteBuffer duplicate(); - ByteBuffer asReadOnlyBuffer(); - byte get(); ByteBuffer put(byte b); @@ -42,10 +38,6 @@ public interface ByteBuffer extends Buffer { ByteBuffer put(byte[] src); - int arrayOffset(); - - ByteBuffer compact(); - char getChar(); ByteBuffer putChar(char value); @@ -54,7 +46,7 @@ public interface ByteBuffer extends Buffer { ByteBuffer putChar(int index, char value); - public abstract short getShort(); + short getShort(); ByteBuffer putShort(short value); @@ -106,4 +98,6 @@ public interface ByteBuffer extends Buffer { ByteBuffer position(int newPosition); + byte[] array(); + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerBufferInputStream.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerBufferInputStream.java index 383f83e8..d6fd8ee9 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerBufferInputStream.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/EaglerBufferInputStream.java @@ -3,7 +3,6 @@ package net.lax1dude.eaglercraft.v1_8.internal.buffer; import java.io.IOException; import java.io.InputStream; - /** * Copyright (c) 2022 lax1dude. All Rights Reserved. * diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/FloatBuffer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/FloatBuffer.java index 73656cb9..ecaf25ff 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/FloatBuffer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/FloatBuffer.java @@ -17,12 +17,8 @@ package net.lax1dude.eaglercraft.v1_8.internal.buffer; */ public interface FloatBuffer extends Buffer { - FloatBuffer slice(); - FloatBuffer duplicate(); - FloatBuffer asReadOnlyBuffer(); - float get(); FloatBuffer put(float b); @@ -45,10 +41,6 @@ public interface FloatBuffer extends Buffer { FloatBuffer put(float[] src); - int getArrayOffset(); - - FloatBuffer compact(); - boolean isDirect(); FloatBuffer mark(); @@ -64,6 +56,8 @@ public interface FloatBuffer extends Buffer { FloatBuffer limit(int newLimit); FloatBuffer position(int newPosition); - + + float[] array(); + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/IntBuffer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/IntBuffer.java index 27b33607..0d96de55 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/IntBuffer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/IntBuffer.java @@ -17,12 +17,8 @@ package net.lax1dude.eaglercraft.v1_8.internal.buffer; */ public interface IntBuffer extends Buffer { - IntBuffer slice(); - IntBuffer duplicate(); - IntBuffer asReadOnlyBuffer(); - int get(); IntBuffer put(int b); @@ -45,10 +41,6 @@ public interface IntBuffer extends Buffer { IntBuffer put(int[] src); - int getArrayOffset(); - - IntBuffer compact(); - boolean isDirect(); IntBuffer mark(); @@ -64,6 +56,8 @@ public interface IntBuffer extends Buffer { IntBuffer limit(int newLimit); IntBuffer position(int newPosition); - + + int[] array(); + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/ShortBuffer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/ShortBuffer.java index b66bad62..56e54a54 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/ShortBuffer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/buffer/ShortBuffer.java @@ -17,12 +17,8 @@ package net.lax1dude.eaglercraft.v1_8.internal.buffer; */ public interface ShortBuffer extends Buffer { - ShortBuffer slice(); - ShortBuffer duplicate(); - ShortBuffer asReadOnlyBuffer(); - short get(); ShortBuffer put(short b); @@ -45,10 +41,6 @@ public interface ShortBuffer extends Buffer { ShortBuffer put(short[] src); - int getArrayOffset(); - - ShortBuffer compact(); - boolean isDirect(); ShortBuffer mark(); @@ -64,5 +56,7 @@ public interface ShortBuffer extends Buffer { ShortBuffer limit(int newLimit); ShortBuffer position(int newPosition); - + + short[] array(); + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFSFilenameIteratorImpl.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFSFilenameIteratorImpl.java index 0311f158..6bfec45d 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFSFilenameIteratorImpl.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFSFilenameIteratorImpl.java @@ -1,5 +1,6 @@ package net.lax1dude.eaglercraft.v1_8.internal.vfs2; +import net.lax1dude.eaglercraft.v1_8.internal.IEaglerFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.VFSFilenameIterator; /** @@ -19,15 +20,17 @@ import net.lax1dude.eaglercraft.v1_8.internal.VFSFilenameIterator; */ class VFSFilenameIteratorImpl implements VFSFilenameIterator { + protected IEaglerFilesystem fs; protected VFSIterator2 itr; - VFSFilenameIteratorImpl(VFSIterator2 itr) { + VFSFilenameIteratorImpl(IEaglerFilesystem fs, VFSIterator2 itr) { + this.fs = fs; this.itr = itr; } @Override public void next(String entry) { - itr.next(new VFile2(entry)); + itr.next(VFile2.create(fs, entry)); } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFSListFilesIteratorImpl.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFSListFilesIteratorImpl.java index 81d44766..0e937743 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFSListFilesIteratorImpl.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFSListFilesIteratorImpl.java @@ -2,6 +2,7 @@ package net.lax1dude.eaglercraft.v1_8.internal.vfs2; import java.util.List; +import net.lax1dude.eaglercraft.v1_8.internal.IEaglerFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.VFSFilenameIterator; /** @@ -21,15 +22,17 @@ import net.lax1dude.eaglercraft.v1_8.internal.VFSFilenameIterator; */ class VFSListFilesIteratorImpl implements VFSFilenameIterator { + protected IEaglerFilesystem fs; protected List list; - VFSListFilesIteratorImpl(List list) { + VFSListFilesIteratorImpl(IEaglerFilesystem fs, List list) { + this.fs = fs; this.list = list; } @Override public void next(String entry) { - list.add(new VFile2(entry)); + list.add(VFile2.create(fs, entry)); } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFile2.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFile2.java index 88c0ecf8..f1114238 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFile2.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFile2.java @@ -5,9 +5,10 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; import net.lax1dude.eaglercraft.v1_8.EagUtils; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformFilesystem; +import net.lax1dude.eaglercraft.v1_8.internal.IEaglerFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; @@ -31,6 +32,12 @@ public class VFile2 { public static final String pathSeperator = "/"; public static final String[] altPathSeperator = new String[] { "\\" }; + static IEaglerFilesystem primaryFilesystem = null; + + public static void setPrimaryFilesystem(IEaglerFilesystem fs) { + primaryFilesystem = fs; + } + public static String normalizePath(String p) { for(int i = 0; i < altPathSeperator.length; ++i) { p = p.replace(altPathSeperator[i], pathSeperator); @@ -53,9 +60,11 @@ public class VFile2 { } protected String path; + protected IEaglerFilesystem myFilesystem; + protected Supplier myFilesystemProvider; public static String createPath(Object... p) { - ArrayList r = new ArrayList(); + ArrayList r = new ArrayList<>(); for(int i = 0; i < p.length; ++i) { if(p[i] == null) { continue; @@ -94,13 +103,45 @@ public class VFile2 { } } - public VFile2(Object... p) { - this.path = createPath(p); + public static VFile2 create(IEaglerFilesystem fs, Object... path) { + return new VFile2(createPath(path), fs); + } + + public static VFile2 create(Supplier fs, Object... path) { + return new VFile2(createPath(path), fs); + } + + public VFile2(Object... path) { + this(createPath(path), primaryFilesystem); + } + + private VFile2(String path, IEaglerFilesystem fs) { + this.path = path; + this.myFilesystem = fs; + } + + private VFile2(String path, Supplier fs) { + this.path = path; + this.myFilesystemProvider = fs; + } + + protected IEaglerFilesystem getFS() { + if(myFilesystem == null) { + if(myFilesystemProvider != null) { + myFilesystem = myFilesystemProvider.get(); + }else { + myFilesystem = primaryFilesystem; + } + if(myFilesystem == null) { + throw new IllegalStateException("The filesystem has not been initialized yet!"); + } + } + return myFilesystem; } public InputStream getInputStream() { assertNotRelative(); - return new VFileInputStream(PlatformFilesystem.eaglerRead(path)); + return new VFileInputStream(getFS().eaglerRead(path)); } public OutputStream getOutputStream() { @@ -121,7 +162,7 @@ public class VFile2 { } public boolean canRead() { - return !isRelative() && PlatformFilesystem.eaglerExists(path); + return !isRelative() && getFS().eaglerExists(path); } public String getPath() { @@ -160,15 +201,15 @@ public class VFile2 { } public boolean exists() { - return !isRelative() && PlatformFilesystem.eaglerExists(path); + return !isRelative() && getFS().eaglerExists(path); } public boolean delete() { - return !isRelative() && PlatformFilesystem.eaglerDelete(path); + return !isRelative() && getFS().eaglerDelete(path); } public boolean renameTo(String p) { - if(!isRelative() && PlatformFilesystem.eaglerMove(path, p)) { + if(!isRelative() && getFS().eaglerMove(path, p)) { return true; } return false; @@ -179,7 +220,7 @@ public class VFile2 { } public int length() { - return isRelative() ? -1 : PlatformFilesystem.eaglerSize(path); + return isRelative() ? -1 : getFS().eaglerSize(path); } public byte[] getAllBytes() { @@ -187,7 +228,7 @@ public class VFile2 { if(!exists()) { return null; } - ByteBuffer readBuffer = PlatformFilesystem.eaglerRead(path); + ByteBuffer readBuffer = getFS().eaglerRead(path); byte[] copyBuffer = PlatformRuntime.castNativeByteBuffer(readBuffer); if(copyBuffer != null) { return copyBuffer; @@ -225,14 +266,14 @@ public class VFile2 { assertNotRelative(); ByteBuffer copyBuffer = PlatformRuntime.castPrimitiveByteArray(bytes); if(copyBuffer != null) { - PlatformFilesystem.eaglerWrite(path, copyBuffer); + getFS().eaglerWrite(path, copyBuffer); return; } copyBuffer = PlatformRuntime.allocateByteBuffer(bytes.length); try { copyBuffer.put(bytes); copyBuffer.flip(); - PlatformFilesystem.eaglerWrite(path, copyBuffer); + getFS().eaglerWrite(path, copyBuffer); }finally { PlatformRuntime.freeByteBuffer(copyBuffer); } @@ -240,24 +281,30 @@ public class VFile2 { public void iterateFiles(VFSIterator2 itr, boolean recursive) { assertNotRelative(); - PlatformFilesystem.eaglerIterate(path, new VFSFilenameIteratorImpl(itr), recursive); + IEaglerFilesystem fs = getFS(); + fs.eaglerIterate(path, new VFSFilenameIteratorImpl(fs, itr), recursive); } public List listFilenames(boolean recursive) { - List ret = new ArrayList(); - PlatformFilesystem.eaglerIterate(path, new VFSListFilenamesIteratorImpl(ret), recursive); + List ret = new ArrayList<>(); + getFS().eaglerIterate(path, new VFSListFilenamesIteratorImpl(ret), recursive); return ret; } public List listFiles(boolean recursive) { - List ret = new ArrayList(); - PlatformFilesystem.eaglerIterate(path, new VFSListFilesIteratorImpl(ret), recursive); + List ret = new ArrayList<>(); + IEaglerFilesystem fs = getFS(); + fs.eaglerIterate(path, new VFSListFilesIteratorImpl(fs, ret), recursive); return ret; } public static int copyFile(VFile2 src, VFile2 dst) { src.assertNotRelative(); dst.assertNotRelative(); - return PlatformFilesystem.eaglerCopy(src.path, dst.path); + IEaglerFilesystem sfs = src.getFS(); + if(sfs != dst.getFS()) { + throw new UnsupportedOperationException("Cannot copy file between filesystems!"); + } + return sfs.eaglerCopy(src.path, dst.path); } } \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFileOutputStream.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFileOutputStream.java index 4333bfd4..8be71750 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFileOutputStream.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/internal/vfs2/VFileOutputStream.java @@ -3,7 +3,6 @@ package net.lax1dude.eaglercraft.v1_8.internal.vfs2; import java.io.IOException; import net.lax1dude.eaglercraft.v1_8.EaglerOutputStream; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformFilesystem; import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; @@ -41,7 +40,7 @@ class VFileOutputStream extends EaglerOutputStream { copyBuffer.put(buf, 0, count); copyBuffer.flip(); try { - PlatformFilesystem.eaglerWrite(vfsFile.path, copyBuffer); + vfsFile.getFS().eaglerWrite(vfsFile.path, copyBuffer); }catch(Throwable t) { throw new IOException("Could not write stream contents to file!", t); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/json/JSONTypeProvider.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/json/JSONTypeProvider.java index 8549dff0..3d0dd8be 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/json/JSONTypeProvider.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/json/JSONTypeProvider.java @@ -54,10 +54,10 @@ import net.minecraft.world.gen.ChunkProviderSettings; */ public class JSONTypeProvider { - private static final Map,JSONTypeSerializer> serializers = new HashMap(); - private static final Map,JSONTypeDeserializer> deserializers = new HashMap(); + private static final Map,JSONTypeSerializer> serializers = new HashMap<>(); + private static final Map,JSONTypeDeserializer> deserializers = new HashMap<>(); - private static final List parsers = new ArrayList(); + private static final List parsers = new ArrayList<>(); public static J serialize(Object object) throws JSONException { JSONTypeSerializer ser = (JSONTypeSerializer) serializers.get(object.getClass()); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/json/impl/SoundMapDeserializer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/json/impl/SoundMapDeserializer.java index ccbeca47..7e21dec9 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/json/impl/SoundMapDeserializer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/json/impl/SoundMapDeserializer.java @@ -30,7 +30,7 @@ public class SoundMapDeserializer implements JSONTypeDeserializer soundsMap = new HashMap(); + Map soundsMap = new HashMap<>(); for(String str : json.keySet()) { soundsMap.put(str, JSONTypeProvider.deserialize(json.getJSONObject(str), SoundList.class)); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/log4j/LogManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/log4j/LogManager.java index 3cc11e12..2d295d36 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/log4j/LogManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/log4j/LogManager.java @@ -20,7 +20,7 @@ import java.util.Map; */ public class LogManager { - private static final Map loggerInstances = new HashMap(); + private static final Map loggerInstances = new HashMap<>(); public static final Object logLock = new Object(); public static Level logLevel = Level.DEBUG; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/log4j/Logger.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/log4j/Logger.java index 137ac39e..f734f35f 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/log4j/Logger.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/log4j/Logger.java @@ -101,7 +101,7 @@ public class Logger { log(Level.FATAL, msg); } - private static final SimpleDateFormat fmt = EagRuntime.fixDateFormat(new SimpleDateFormat("hh:mm:ss+SSS")); + private static final SimpleDateFormat fmt = new SimpleDateFormat("hh:mm:ss+SSS"); private static final Date dateInstance = new Date(); public void log(Level level, String msg) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/ChunkUpdateManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/ChunkUpdateManager.java index 7ea085ee..579a29dd 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/ChunkUpdateManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/ChunkUpdateManager.java @@ -5,6 +5,7 @@ import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; import java.util.LinkedList; import java.util.List; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; @@ -35,7 +36,7 @@ public class ChunkUpdateManager { private int chunkUpdatesQueuedLast = 0; private long chunkUpdatesTotalLastUpdate = 0l; - private final List queue = new LinkedList(); + private final List queue = new LinkedList<>(); public ChunkUpdateManager() { worldVertexUploader = new WorldVertexBufferUploader(); @@ -108,8 +109,8 @@ public class ChunkUpdateManager { return false; }else { boolean flag = false; - long millis = System.currentTimeMillis(); - List droppedUpdates = new LinkedList(); + long millis = EagRuntime.steadyTimeMillis(); + List droppedUpdates = new LinkedList<>(); while(!queue.isEmpty()) { ChunkCompileTaskGenerator generator = queue.remove(0); @@ -125,7 +126,7 @@ public class ChunkUpdateManager { ++chunkUpdatesTotal; - if(timeout < System.nanoTime()) { + if(timeout < EagRuntime.nanoTime()) { break; } } @@ -176,7 +177,7 @@ public class ChunkUpdateManager { if (chunkcompiletaskgenerator == null) { return true; } - chunkcompiletaskgenerator.goddamnFuckingTimeout = System.currentTimeMillis(); + chunkcompiletaskgenerator.goddamnFuckingTimeout = EagRuntime.steadyTimeMillis(); if(queue.size() < 100) { chunkcompiletaskgenerator.addFinishRunnable(new Runnable() { @Override @@ -219,7 +220,7 @@ public class ChunkUpdateManager { } public String getDebugInfo() { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - chunkUpdatesTotalLastUpdate > 500l) { chunkUpdatesTotalLastUpdate = millis; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerFolderResourcePack.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerFolderResourcePack.java index 5a5ab3e0..e8ccb4f1 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerFolderResourcePack.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerFolderResourcePack.java @@ -105,7 +105,7 @@ public class EaglerFolderResourcePack extends AbstractResourcePack { } try { JSONArray json = (new JSONObject(str)).getJSONArray("resourcePacks"); - List ret = new ArrayList(json.length()); + List ret = new ArrayList<>(json.length()); for(int i = 0, l = json.length(); i < l; ++i) { JSONObject jp = json.getJSONObject(i); String folderName = jp.getString("folder"); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerFontRenderer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerFontRenderer.java index 61a52490..0e0f561b 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerFontRenderer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerFontRenderer.java @@ -30,6 +30,15 @@ public class EaglerFontRenderer extends FontRenderer { private final int[] temporaryCodepointArray = new int[6553]; + public static FontRenderer createSupportedFontRenderer(GameSettings gameSettingsIn, ResourceLocation location, + TextureManager textureManagerIn, boolean unicode) { + if(EaglercraftGPU.checkInstancingCapable()) { + return new EaglerFontRenderer(gameSettingsIn, location, textureManagerIn, unicode); + }else { + return new FontRenderer(gameSettingsIn, location, textureManagerIn, unicode); + } + } + public EaglerFontRenderer(GameSettings gameSettingsIn, ResourceLocation location, TextureManager textureManagerIn, boolean unicode) { super(gameSettingsIn, location, textureManagerIn, unicode); @@ -116,15 +125,16 @@ public class EaglerFontRenderer extends FontRenderer { ++i; } else { int j = temporaryCodepointArray[i]; + if(j > 255) continue; if (this.randomStyle && j != -1) { int k = this.getCharWidth(c0); - String chars = "\u00c0\u00c1\u00c2\u00c8\u00ca\u00cb\u00cd\u00d3\u00d4\u00d5\u00da\u00df\u00e3\u00f5\u011f\u0130\u0131\u0152\u0153\u015e\u015f\u0174\u0175\u017e\u0207\u0000\u0000\u0000\u0000\u0000\u0000\u0000 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0000\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00f8\u00a3\u00d8\u00d7\u0192\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u00ae\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580\u03b1\u03b2\u0393\u03c0\u03a3\u03c3\u03bc\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u2205\u2208\u2229\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u00b7\u221a\u207f\u00b2\u25a0\u0000"; + char[] chars = FontRenderer.codepointLookup; char c1; while (true) { - j = this.fontRandom.nextInt(chars.length()); - c1 = chars.charAt(j); + j = this.fontRandom.nextInt(chars.length); + c1 = chars[j]; if (k == this.getCharWidth(c1)) { break; } @@ -220,8 +230,7 @@ public class EaglerFontRenderer extends FontRenderer { private boolean decodeASCIICodepointsAndValidate(String str) { for(int i = 0, l = str.length(); i < l; ++i) { - int j = "\u00c0\u00c1\u00c2\u00c8\u00ca\u00cb\u00cd\u00d3\u00d4\u00d5\u00da\u00df\u00e3\u00f5\u011f\u0130\u0131\u0152\u0153\u015e\u015f\u0174\u0175\u017e\u0207\u0000\u0000\u0000\u0000\u0000\u0000\u0000 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0000\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00f8\u00a3\u00d8\u00d7\u0192\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u00ae\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580\u03b1\u03b2\u0393\u03c0\u03a3\u03c3\u03bc\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u2205\u2208\u2229\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u00b7\u221a\u207f\u00b2\u25a0\u0000\u00a7" - .indexOf(str.charAt(i)); + int j = FontMappingHelper.lookupChar(str.charAt(i), true); if(j != -1) { temporaryCodepointArray[i] = j; }else { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerTextureAtlasSprite.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerTextureAtlasSprite.java index ef9f92ae..125c6b6b 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerTextureAtlasSprite.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EaglerTextureAtlasSprite.java @@ -1,7 +1,6 @@ package net.lax1dude.eaglercraft.v1_8.minecraft; import java.io.IOException; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.Callable; @@ -229,10 +228,10 @@ public class EaglerTextureAtlasSprite { int l = i; this.height = this.width; if (meta.getFrameCount() > 0) { - Iterator iterator = meta.getFrameIndexSet().iterator(); + Iterator iterator = meta.getFrameIndexSet().iterator(); while (iterator.hasNext()) { - int i1 = ((Integer) iterator.next()).intValue(); + int i1 = iterator.next().intValue(); if (i1 >= j1) { throw new RuntimeException("invalid frameindex " + i1); } @@ -243,7 +242,7 @@ public class EaglerTextureAtlasSprite { this.animationMetadata = meta; } else { - ArrayList arraylist = Lists.newArrayList(); + List arraylist = Lists.newArrayList(); for (int l1 = 0; l1 < j1; ++l1) { this.framesTextureData.add(getFrameTextureData(aint, k1, l, l1)); @@ -258,10 +257,10 @@ public class EaglerTextureAtlasSprite { } public void generateMipmaps(int level) { - ArrayList arraylist = Lists.newArrayList(); + List arraylist = Lists.newArrayList(); for (int i = 0; i < this.framesTextureData.size(); ++i) { - final int[][] aint = (int[][]) this.framesTextureData.get(i); + final int[][] aint = this.framesTextureData.get(i); if (aint != null) { try { arraylist.add(TextureUtil.generateMipmapData(level, this.width, aint)); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EnumInputEvent.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EnumInputEvent.java new file mode 100644 index 00000000..419e5366 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/EnumInputEvent.java @@ -0,0 +1,20 @@ +package net.lax1dude.eaglercraft.v1_8.minecraft; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public enum EnumInputEvent { + CLIPBOARD_COPY, CLIPBOARD_PASTE; +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/FontMappingHelper.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/FontMappingHelper.java new file mode 100644 index 00000000..15e2553a --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/FontMappingHelper.java @@ -0,0 +1,525 @@ +package net.lax1dude.eaglercraft.v1_8.minecraft; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class FontMappingHelper { + + public static int lookupChar(char c, boolean incSel) { + switch(c) { + case 167: + return incSel ? 256 : -1; + case 192: + return 0; + case 193: + return 1; + case 194: + return 2; + case 200: + return 3; + case 202: + return 4; + case 203: + return 5; + case 205: + return 6; + case 211: + return 7; + case 212: + return 8; + case 213: + return 9; + case 218: + return 10; + case 223: + return 11; + case 227: + return 12; + case 245: + return 13; + case 287: + return 14; + case 304: + return 15; + case 305: + return 16; + case 338: + return 17; + case 339: + return 18; + case 350: + return 19; + case 351: + return 20; + case 372: + return 21; + case 373: + return 22; + case 382: + return 23; + case 519: + return 24; + case 0: + return 25; + case 32: + return 32; + case 33: + return 33; + case 34: + return 34; + case 35: + return 35; + case 36: + return 36; + case 37: + return 37; + case 38: + return 38; + case 39: + return 39; + case 40: + return 40; + case 41: + return 41; + case 42: + return 42; + case 43: + return 43; + case 44: + return 44; + case 45: + return 45; + case 46: + return 46; + case 47: + return 47; + case 48: + return 48; + case 49: + return 49; + case 50: + return 50; + case 51: + return 51; + case 52: + return 52; + case 53: + return 53; + case 54: + return 54; + case 55: + return 55; + case 56: + return 56; + case 57: + return 57; + case 58: + return 58; + case 59: + return 59; + case 60: + return 60; + case 61: + return 61; + case 62: + return 62; + case 63: + return 63; + case 64: + return 64; + case 65: + return 65; + case 66: + return 66; + case 67: + return 67; + case 68: + return 68; + case 69: + return 69; + case 70: + return 70; + case 71: + return 71; + case 72: + return 72; + case 73: + return 73; + case 74: + return 74; + case 75: + return 75; + case 76: + return 76; + case 77: + return 77; + case 78: + return 78; + case 79: + return 79; + case 80: + return 80; + case 81: + return 81; + case 82: + return 82; + case 83: + return 83; + case 84: + return 84; + case 85: + return 85; + case 86: + return 86; + case 87: + return 87; + case 88: + return 88; + case 89: + return 89; + case 90: + return 90; + case 91: + return 91; + case 92: + return 92; + case 93: + return 93; + case 94: + return 94; + case 95: + return 95; + case 96: + return 96; + case 97: + return 97; + case 98: + return 98; + case 99: + return 99; + case 100: + return 100; + case 101: + return 101; + case 102: + return 102; + case 103: + return 103; + case 104: + return 104; + case 105: + return 105; + case 106: + return 106; + case 107: + return 107; + case 108: + return 108; + case 109: + return 109; + case 110: + return 110; + case 111: + return 111; + case 112: + return 112; + case 113: + return 113; + case 114: + return 114; + case 115: + return 115; + case 116: + return 116; + case 117: + return 117; + case 118: + return 118; + case 119: + return 119; + case 120: + return 120; + case 121: + return 121; + case 122: + return 122; + case 123: + return 123; + case 124: + return 124; + case 125: + return 125; + case 126: + return 126; + case 199: + return 128; + case 252: + return 129; + case 233: + return 130; + case 226: + return 131; + case 228: + return 132; + case 224: + return 133; + case 229: + return 134; + case 231: + return 135; + case 234: + return 136; + case 235: + return 137; + case 232: + return 138; + case 239: + return 139; + case 238: + return 140; + case 236: + return 141; + case 196: + return 142; + case 197: + return 143; + case 201: + return 144; + case 230: + return 145; + case 198: + return 146; + case 244: + return 147; + case 246: + return 148; + case 242: + return 149; + case 251: + return 150; + case 249: + return 151; + case 255: + return 152; + case 214: + return 153; + case 220: + return 154; + case 248: + return 155; + case 163: + return 156; + case 216: + return 157; + case 215: + return 158; + case 402: + return 159; + case 225: + return 160; + case 237: + return 161; + case 243: + return 162; + case 250: + return 163; + case 241: + return 164; + case 209: + return 165; + case 170: + return 166; + case 186: + return 167; + case 191: + return 168; + case 174: + return 169; + case 172: + return 170; + case 189: + return 171; + case 188: + return 172; + case 161: + return 173; + case 171: + return 174; + case 187: + return 175; + case 9617: + return 176; + case 9618: + return 177; + case 9619: + return 178; + case 9474: + return 179; + case 9508: + return 180; + case 9569: + return 181; + case 9570: + return 182; + case 9558: + return 183; + case 9557: + return 184; + case 9571: + return 185; + case 9553: + return 186; + case 9559: + return 187; + case 9565: + return 188; + case 9564: + return 189; + case 9563: + return 190; + case 9488: + return 191; + case 9492: + return 192; + case 9524: + return 193; + case 9516: + return 194; + case 9500: + return 195; + case 9472: + return 196; + case 9532: + return 197; + case 9566: + return 198; + case 9567: + return 199; + case 9562: + return 200; + case 9556: + return 201; + case 9577: + return 202; + case 9574: + return 203; + case 9568: + return 204; + case 9552: + return 205; + case 9580: + return 206; + case 9575: + return 207; + case 9576: + return 208; + case 9572: + return 209; + case 9573: + return 210; + case 9561: + return 211; + case 9560: + return 212; + case 9554: + return 213; + case 9555: + return 214; + case 9579: + return 215; + case 9578: + return 216; + case 9496: + return 217; + case 9484: + return 218; + case 9608: + return 219; + case 9604: + return 220; + case 9612: + return 221; + case 9616: + return 222; + case 9600: + return 223; + case 945: + return 224; + case 946: + return 225; + case 915: + return 226; + case 960: + return 227; + case 931: + return 228; + case 963: + return 229; + case 956: + return 230; + case 964: + return 231; + case 934: + return 232; + case 920: + return 233; + case 937: + return 234; + case 948: + return 235; + case 8734: + return 236; + case 8709: + return 237; + case 8712: + return 238; + case 8745: + return 239; + case 8801: + return 240; + case 177: + return 241; + case 8805: + return 242; + case 8804: + return 243; + case 8992: + return 244; + case 8993: + return 245; + case 247: + return 246; + case 8776: + return 247; + case 176: + return 248; + case 8729: + return 249; + case 183: + return 250; + case 8730: + return 251; + case 8319: + return 252; + case 178: + return 253; + case 9632: + return 254; + default: + return -1; + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiButtonWithStupidIcons.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiButtonWithStupidIcons.java new file mode 100644 index 00000000..80108669 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiButtonWithStupidIcons.java @@ -0,0 +1,132 @@ +package net.lax1dude.eaglercraft.v1_8.minecraft; + +import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; + +import net.lax1dude.eaglercraft.v1_8.Mouse; +import net.lax1dude.eaglercraft.v1_8.internal.EnumCursorType; +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.ResourceLocation; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiButtonWithStupidIcons extends GuiButton { + + protected ResourceLocation leftIcon; + protected float leftIconAspect; + protected ResourceLocation rightIcon; + protected float rightIconAspect; + + public GuiButtonWithStupidIcons(int buttonId, int x, int y, int widthIn, int heightIn, String buttonText) { + super(buttonId, x, y, widthIn, heightIn, buttonText); + } + + public GuiButtonWithStupidIcons(int buttonId, int x, int y, String buttonText) { + super(buttonId, x, y, buttonText); + } + + public GuiButtonWithStupidIcons(int buttonId, int x, int y, int widthIn, int heightIn, String buttonText, + ResourceLocation leftIcon, float leftIconAspect, ResourceLocation rightIcon, float rightIconAspect) { + super(buttonId, x, y, widthIn, heightIn, buttonText); + this.leftIcon = leftIcon; + this.leftIconAspect = leftIconAspect; + this.rightIcon = rightIcon; + this.rightIconAspect = rightIconAspect; + } + + public GuiButtonWithStupidIcons(int buttonId, int x, int y, String buttonText, ResourceLocation leftIcon, + float leftIconAspect, ResourceLocation rightIcon, float rightIconAspect) { + super(buttonId, x, y, buttonText); + this.leftIcon = leftIcon; + this.leftIconAspect = leftIconAspect; + this.rightIcon = rightIcon; + this.rightIconAspect = rightIconAspect; + } + + public ResourceLocation getLeftIcon() { + return leftIcon; + } + + public ResourceLocation getRightIcon() { + return rightIcon; + } + + public void setLeftIcon(ResourceLocation leftIcon, float aspectRatio) { + this.leftIcon = leftIcon; + this.leftIconAspect = aspectRatio; + } + + public void setRightIcon(ResourceLocation rightIcon, float aspectRatio) { + this.rightIcon = rightIcon; + this.rightIconAspect = aspectRatio; + } + + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + if (this.visible) { + FontRenderer fontrenderer = mc.fontRendererObj; + mc.getTextureManager().bindTexture(buttonTextures); + GlStateManager.color(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; + if (this.enabled && this.hovered) { + Mouse.showCursor(EnumCursorType.HAND); + } + int i = this.getHoverState(this.hovered); + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, 1, 0); + GlStateManager.blendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + this.drawTexturedModalRect(this.xPosition, this.yPosition, 0, 46 + i * 20, this.width / 2, this.height); + this.drawTexturedModalRect(this.xPosition + this.width / 2, this.yPosition, 200 - this.width / 2, + 46 + i * 20, this.width / 2, this.height); + this.mouseDragged(mc, mouseX, mouseY); + int j = 14737632; + if (!this.enabled) { + j = 10526880; + } else if (this.hovered) { + j = 16777120; + } + + int strWidth = fontrenderer.getStringWidth(displayString); + int strWidthAdj = strWidth - (leftIcon != null ? (int) (16 * leftIconAspect) : 0) + + (rightIcon != null ? (int) (16 * rightIconAspect) : 0); + this.drawString(fontrenderer, this.displayString, this.xPosition + (this.width - strWidthAdj) / 2, + this.yPosition + (this.height - 8) / 2, j); + if(leftIcon != null) { + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); + mc.getTextureManager().bindTexture(leftIcon); + GlStateManager.pushMatrix(); + GlStateManager.translate(this.xPosition + (this.width - strWidthAdj) / 2 - 3 - 16 * leftIconAspect, this.yPosition + 2, 0.0f); + float f = 16.0f / 256.0f; + GlStateManager.scale(f * leftIconAspect, f, f); + this.drawTexturedModalRect(0, 0, 0, 0, 256, 256); + GlStateManager.popMatrix(); + } + if(rightIcon != null) { + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); + mc.getTextureManager().bindTexture(rightIcon); + GlStateManager.pushMatrix(); + GlStateManager.translate(this.xPosition + (this.width - strWidthAdj) / 2 + strWidth + 3, this.yPosition + 2, 0.0f); + float f = 16.0f / 256.0f; + GlStateManager.scale(f * rightIconAspect, f, f); + this.drawTexturedModalRect(0, 0, 0, 0, 256, 256); + GlStateManager.popMatrix(); + } + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiScreenGenericErrorMessage.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiScreenGenericErrorMessage.java index f6fb104c..6e8fa027 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiScreenGenericErrorMessage.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiScreenGenericErrorMessage.java @@ -1,5 +1,7 @@ package net.lax1dude.eaglercraft.v1_8.minecraft; +import org.apache.commons.lang3.StringUtils; + import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.resources.I18n; @@ -26,8 +28,8 @@ public class GuiScreenGenericErrorMessage extends GuiScreen { private GuiScreen cont; public GuiScreenGenericErrorMessage(String str1, String str2, GuiScreen cont) { - this.str1 = I18n.format(str1); - this.str2 = I18n.format(str2); + this.str1 = StringUtils.isAllEmpty(str1) ? "" : I18n.format(str1); + this.str2 = StringUtils.isAllEmpty(str2) ? "" : I18n.format(str2); this.cont = cont; } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiScreenVisualViewport.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiScreenVisualViewport.java new file mode 100644 index 00000000..3a0e5aed --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/GuiScreenVisualViewport.java @@ -0,0 +1,144 @@ +package net.lax1dude.eaglercraft.v1_8.minecraft; + +import net.lax1dude.eaglercraft.v1_8.Display; +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenVisualViewport extends GuiScreen { + + protected int offsetX; + protected int offsetY; + + @Override + public final void setWorldAndResolution(Minecraft mc, int width, int height) { + Display.wasVisualViewportResized(); // clear state + offsetX = Display.getVisualViewportX() * width / mc.displayWidth; + offsetY = Display.getVisualViewportY() * height / mc.displayHeight; + setWorldAndResolution0(mc, Display.getVisualViewportW() * width / mc.displayWidth, + Display.getVisualViewportH() * height / mc.displayHeight); + } + + protected void setWorldAndResolution0(Minecraft mc, int width, int height) { + super.setWorldAndResolution(mc, width, height); + } + + @Override + public final void updateScreen() { + if(Display.wasVisualViewportResized()) { + setWorldAndResolution(mc, mc.scaledResolution.getScaledWidth(), mc.scaledResolution.getScaledHeight()); + } + updateScreen0(); + } + + protected void updateScreen0() { + super.updateScreen(); + } + + @Override + public final void drawScreen(int i, int j, float var3) { + i -= offsetX; + j -= offsetY; + GlStateManager.pushMatrix(); + GlStateManager.translate(offsetX, offsetY, 0.0f); + drawScreen0(i, j, var3); + GlStateManager.popMatrix(); + } + + protected void drawScreen0(int i, int j, float var3) { + super.drawScreen(i, j, var3); + } + + @Override + protected final void mouseClicked(int parInt1, int parInt2, int parInt3) { + parInt1 -= offsetX; + parInt2 -= offsetY; + mouseClicked0(parInt1, parInt2, parInt3); + } + + protected void mouseClicked0(int parInt1, int parInt2, int parInt3) { + super.mouseClicked(parInt1, parInt2, parInt3); + } + + @Override + protected final void mouseReleased(int i, int j, int k) { + i -= offsetX; + j -= offsetY; + mouseReleased0(i, j, k); + } + + protected void mouseReleased0(int i, int j, int k) { + super.mouseReleased(i, j, k); + } + + @Override + protected final void mouseClickMove(int var1, int var2, int var3, long var4) { + var1 -= offsetX; + var2 -= offsetY; + mouseClickMove0(var1, var2, var3, var4); + } + + protected void mouseClickMove0(int var1, int var2, int var3, long var4) { + super.mouseClickMove(var1, var2, var3, var4); + } + + @Override + protected final void touchEndMove(int parInt1, int parInt2, int parInt3) { + parInt1 -= offsetX; + parInt2 -= offsetY; + touchEndMove0(parInt1, parInt2, parInt3); + } + + protected void touchEndMove0(int parInt1, int parInt2, int parInt3) { + super.touchEndMove(parInt1, parInt2, parInt3); + } + + @Override + protected final void touchMoved(int parInt1, int parInt2, int parInt3) { + parInt1 -= offsetX; + parInt2 -= offsetY; + touchMoved0(parInt1, parInt2, parInt3); + } + + protected void touchMoved0(int parInt1, int parInt2, int parInt3) { + super.touchMoved(parInt1, parInt2, parInt3); + } + + @Override + protected final void touchStarted(int parInt1, int parInt2, int parInt3) { + parInt1 -= offsetX; + parInt2 -= offsetY; + touchStarted0(parInt1, parInt2, parInt3); + } + + protected void touchStarted0(int parInt1, int parInt2, int parInt3) { + super.touchStarted(parInt1, parInt2, parInt3); + } + + @Override + protected void touchTapped(int parInt1, int parInt2, int parInt3) { + parInt1 -= offsetX; + parInt2 -= offsetY; + touchTapped0(parInt1, parInt2, parInt3); + } + + protected void touchTapped0(int parInt1, int parInt2, int parInt3) { + super.touchTapped(parInt1, parInt2, parInt3); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/TextureAnimationCache.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/TextureAnimationCache.java index fbf84e60..b20d3fc0 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/TextureAnimationCache.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/minecraft/TextureAnimationCache.java @@ -54,8 +54,8 @@ public class TextureAnimationCache { for(int i = 0; i < cacheTextures.length; ++i) { cacheTextures[i] = GlStateManager.generateTexture(); GlStateManager.bindTexture(cacheTextures[i]); - EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); } @@ -123,6 +123,8 @@ public class TextureAnimationCache { if(cacheTextures == null) { throw new IllegalStateException("Cannot copy from uninitialized TextureAnimationCache"); } + GlStateManager.disableBlend(); + GlStateManager.disableAlpha(); GlStateManager.bindTexture(cacheTextures[level]); TextureCopyUtil.srcSize(width >> level, (height >> level) * frameCount); TextureCopyUtil.blitTextureUsingViewports(0, h * animationFrame, dx, dy, w, h); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/CachedNotifBadgeTexture.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/CachedNotifBadgeTexture.java new file mode 100644 index 00000000..904e10ec --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/CachedNotifBadgeTexture.java @@ -0,0 +1,46 @@ +package net.lax1dude.eaglercraft.v1_8.notifications; + +import java.util.List; + +import net.minecraft.util.IChatComponent; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class CachedNotifBadgeTexture { + + public final int glTexture; + public final int scaleFactor; + public final int width; + public final int height; + public final List cursorEvents; + public final IChatComponent rootClickEvent; + public final boolean hasClickEvents; + public final boolean hasHoverEvents; + + protected CachedNotifBadgeTexture(int glTexture, int scaleFactor, int width, int height, + List cursorEvents, IChatComponent rootClickEvent, boolean hasClickEvents, + boolean hasHoverEvents) { + this.glTexture = glTexture; + this.scaleFactor = scaleFactor; + this.width = width; + this.height = height; + this.cursorEvents = cursorEvents; + this.rootClickEvent = rootClickEvent; + this.hasClickEvents = hasClickEvents; + this.hasHoverEvents = hasHoverEvents; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ClickEventZone.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ClickEventZone.java new file mode 100644 index 00000000..939a61f3 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ClickEventZone.java @@ -0,0 +1,41 @@ +package net.lax1dude.eaglercraft.v1_8.notifications; + +import net.minecraft.util.IChatComponent; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ClickEventZone { + + public final int posX; + public final int posY; + public final int width; + public final int height; + public final IChatComponent chatComponent; + public final boolean hasHoverEvent; + public final boolean hasClickEvent; + + public ClickEventZone(int posX, int posY, int width, int height, IChatComponent chatComponent, + boolean hasHoverEvent, boolean hasClickEvent) { + this.posX = posX; + this.posY = posY; + this.width = width; + this.height = height; + this.chatComponent = chatComponent; + this.hasHoverEvent = hasHoverEvent; + this.hasClickEvent = hasClickEvent; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiButtonNotifBell.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiButtonNotifBell.java new file mode 100644 index 00000000..fc72d217 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiButtonNotifBell.java @@ -0,0 +1,69 @@ +package net.lax1dude.eaglercraft.v1_8.notifications; + +import net.lax1dude.eaglercraft.v1_8.Mouse; +import net.lax1dude.eaglercraft.v1_8.internal.EnumCursorType; +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.ResourceLocation; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiButtonNotifBell extends GuiButton { + + private static final ResourceLocation eaglerTextures = new ResourceLocation("eagler:gui/eagler_gui.png"); + + private int unread = 0; + + public GuiButtonNotifBell(int buttonID, int xPos, int yPos) { + super(buttonID, xPos, yPos, 20, 20, ""); + } + + public void setUnread(int num) { + unread = num; + } + + public void drawButton(Minecraft minecraft, int i, int j) { + if (this.visible) { + minecraft.getTextureManager().bindTexture(eaglerTextures); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + boolean flag = i >= this.xPosition && j >= this.yPosition && i < this.xPosition + this.width + && j < this.yPosition + this.height; + int k = 0; + int c = 14737632; + if (flag) { + k += this.height; + c = 16777120; + Mouse.showCursor(EnumCursorType.HAND); + } + + drawTexturedModalRect(xPosition, yPosition, unread > 0 ? 116 : 136, k, width, height); + + if(unread > 0) { + GlStateManager.pushMatrix(); + GlStateManager.translate(xPosition + 15.5f, yPosition + 11.0f, 0.0f); + if(unread >= 10) { + GlStateManager.translate(0.0f, 1.0f, 0.0f); + GlStateManager.scale(0.5f, 0.5f, 0.5f); + }else { + GlStateManager.scale(0.75f, 0.75f, 0.75f); + } + drawCenteredString(minecraft.fontRendererObj, Integer.toString(unread), 0, 0, c); + GlStateManager.popMatrix(); + } + } + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiScreenNotifications.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiScreenNotifications.java new file mode 100644 index 00000000..80876fb4 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiScreenNotifications.java @@ -0,0 +1,172 @@ +package net.lax1dude.eaglercraft.v1_8.notifications; + +import java.io.IOException; +import java.util.List; + +import com.google.common.base.Predicate; +import com.google.common.collect.Collections2; + +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketNotifBadgeShowV4EAG.EnumBadgePriority; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenNotifications extends GuiScreen { + + private static final String[] priorityLangKeys = new String[] { + "notifications.priority.low", + "notifications.priority.normal", + "notifications.priority.higher", + "notifications.priority.highest" + }; + + private static final int[] priorityOrder = new int[] { + 0, 3, 2, 1 + }; + + GuiScreen parent; + int selected; + GuiSlotNotifications slots; + GuiButton clearAllButton; + GuiButton priorityButton; + int showPriority = 0; + EnumBadgePriority selectedMaxPriority = EnumBadgePriority.LOW; + int lastUpdate = -1; + + public GuiScreenNotifications(GuiScreen parent) { + this.parent = parent; + } + + public void initGui() { + selected = -1; + buttonList.clear(); + buttonList.add(new GuiButton(0, this.width / 2 + 54, this.height - 32, 100, 20, I18n.format("gui.done"))); + buttonList.add(clearAllButton = new GuiButton(1, this.width / 2 - 154, this.height - 32, 100, 20, + I18n.format("notifications.clearAll"))); + int i = priorityOrder[showPriority]; + buttonList.add(priorityButton = new GuiButton(2, this.width / 2 - 50, this.height - 32, 100, 20, + I18n.format("notifications.priority", I18n.format(priorityLangKeys[i])))); + selectedMaxPriority = EnumBadgePriority.getByID(i); + slots = new GuiSlotNotifications(this); + lastUpdate = -69420; + updateList(); + updateButtons(); + } + + void updateButtons() { + clearAllButton.enabled = !slots.currentDisplayNotifs.isEmpty(); + } + + void updateList() { + if(mc.thePlayer == null) return; + ServerNotificationManager mgr = mc.thePlayer.sendQueue.getNotifManager(); + int verHash = showPriority | (mgr.getNotifListUpdateCount() << 2); + if(verHash != lastUpdate) { + lastUpdate = verHash; + EaglercraftUUID selectedUUID = null; + List lst = slots.currentDisplayNotifs; + int oldSelectedId = selected; + if(oldSelectedId >= 0 && oldSelectedId < lst.size()) { + selectedUUID = lst.get(oldSelectedId).badge.badgeUUID; + } + lst.clear(); + lst.addAll(Collections2.transform(Collections2.filter(mgr.getNotifLongHistory(), new Predicate() { + @Override + public boolean apply(NotificationBadge input) { + return input.priority.priority >= priorityOrder[showPriority]; + } + }), GuiSlotNotifications.NotifBadgeSlot::new)); + selected = -1; + if(selectedUUID != null) { + for(int i = 0, l = lst.size(); i < l; ++i) { + if(selectedUUID.equals(lst.get(i).badge.badgeUUID)) { + selected = i; + break; + } + } + } + if(selected != -1) { + if(oldSelectedId != selected) { + slots.scrollBy((selected - oldSelectedId) * slots.getSlotHeight()); + } + } + updateButtons(); + } + } + + public void updateScreen() { + if(mc.thePlayer == null) { + mc.displayGuiScreen(parent); + return; + } + updateList(); + } + + static Minecraft getMinecraft(GuiScreenNotifications screen) { + return screen.mc; + } + + public void actionPerformed(GuiButton btn) { + switch(btn.id) { + case 0: + mc.displayGuiScreen(parent); + break; + case 1: + if(mc.thePlayer != null) { + ServerNotificationManager mgr = mc.thePlayer.sendQueue.getNotifManager(); + mgr.removeAllNotifFromActiveList(mgr.getNotifLongHistory()); + clearAllButton.enabled = false; + } + break; + case 2: + showPriority = (showPriority + 1) & 3; + int i = priorityOrder[showPriority]; + priorityButton.displayString = I18n.format("notifications.priority", I18n.format(priorityLangKeys[i])); + selectedMaxPriority = EnumBadgePriority.getByID(i); + updateList(); + break; + default: + break; + } + } + + public void drawScreen(int par1, int par2, float par3) { + if(mc.thePlayer == null) return; + slots.drawScreen(par1, par2, par3); + this.drawCenteredString(fontRendererObj, I18n.format("notifications.title"), this.width / 2, 16, 16777215); + super.drawScreen(par1, par2, par3); + } + + public void handleMouseInput() throws IOException { + super.handleMouseInput(); + slots.handleMouseInput(); + } + + public void handleTouchInput() throws IOException { + super.handleTouchInput(); + slots.handleTouchInput(); + } + + public void onGuiClosed() { + if(mc.thePlayer != null) { + mc.thePlayer.sendQueue.getNotifManager().commitUnreadFlag(); + } + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiSlotNotifications.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiSlotNotifications.java new file mode 100644 index 00000000..e7b4ef3c --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/GuiSlotNotifications.java @@ -0,0 +1,338 @@ +package net.lax1dude.eaglercraft.v1_8.notifications; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketNotifBadgeShowV4EAG.EnumBadgePriority; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.gui.GuiSlot; +import net.minecraft.client.gui.GuiUtilRenderComponents; +import net.minecraft.event.ClickEvent; +import net.minecraft.event.HoverEvent; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.IChatComponent; +import net.minecraft.util.MathHelper; +import net.minecraft.util.ResourceLocation; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiSlotNotifications extends GuiSlot { + + private static final ResourceLocation eaglerGui = new ResourceLocation("eagler:gui/eagler_gui.png"); + private static final ResourceLocation largeNotifBk = new ResourceLocation("eagler:gui/notif_bk_large.png"); + + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("hh:mm a"); + + final GuiScreenNotifications parent; + final List currentDisplayNotifs; + + int mouseX; + int mouseY; + + protected static class NotifBadgeSlot { + + protected final NotificationBadge badge; + protected final List cursorEvents = new ArrayList<>(); + protected int currentScreenX = -69420; + protected int currentScreenY = -69420; + + protected NotifBadgeSlot(NotificationBadge badge) { + this.badge = badge; + } + + } + + public GuiSlotNotifications(GuiScreenNotifications parent) { + super(GuiScreenNotifications.getMinecraft(parent), parent.width, parent.height, 32, parent.height - 44, 68); + this.parent = parent; + this.currentDisplayNotifs = new ArrayList<>(); + } + + @Override + protected int getSize() { + return currentDisplayNotifs.size(); + } + + @Override + protected void elementClicked(int id, boolean doubleClk, int xx, int yy) { + if(selectedElement != id) return; //workaround for vanilla bs + if(id < currentDisplayNotifs.size()) { + NotifBadgeSlot slot = currentDisplayNotifs.get(id); + if(slot.currentScreenY != -69420) { + int w = getListWidth(); + int localX = xx - slot.currentScreenX; + int localY = yy - slot.currentScreenY; + if(localX >= w - 22 && localX < w - 5 && localY >= 5 && localY < 21) { + slot.badge.removeNotif(); + mc.getSoundHandler().playSound(PositionedSoundRecord.create(new ResourceLocation("gui.button.press"), 1.0F)); + return; + } + IChatComponent cmp = slot.badge.bodyComponent; + if(cmp != null) { + if(doubleClk) { + if (cmp.getChatStyle().getChatClickEvent() != null + && cmp.getChatStyle().getChatClickEvent().getAction().shouldAllowInChat()) { + if(parent.handleComponentClick(cmp)) { + mc.getSoundHandler().playSound(PositionedSoundRecord.create(new ResourceLocation("gui.button.press"), 1.0F)); + return; + } + } + }else { + if(parent.selected != id) { + parent.selected = id; + }else { + List cursorEvents = slot.cursorEvents; + if(cursorEvents != null && !cursorEvents.isEmpty()) { + for(int j = 0, m = cursorEvents.size(); j < m; ++j) { + ClickEventZone evt = cursorEvents.get(j); + if(evt.hasClickEvent) { + int offsetPosX = slot.currentScreenX + evt.posX; + int offsetPosY = slot.currentScreenY + evt.posY; + if(xx >= offsetPosX && yy >= offsetPosY && xx < offsetPosX + evt.width && yy < offsetPosY + evt.height) { + if(parent.handleComponentClick(evt.chatComponent)) { + mc.getSoundHandler().playSound(PositionedSoundRecord.create(new ResourceLocation("gui.button.press"), 1.0F)); + return; + } + } + } + } + } + } + } + } + } + } + } + + @Override + protected boolean isSelected(int var1) { + return var1 == parent.selected; + } + + @Override + protected void drawBackground() { + parent.drawBackground(0); + } + + @Override + protected void drawSlot(int id, int xx, int yy, int width, int height, int ii) { + if(id < currentDisplayNotifs.size()) { + NotifBadgeSlot slot = currentDisplayNotifs.get(id); + slot.currentScreenX = xx; + slot.currentScreenY = yy; + NotificationBadge bd = slot.badge; + if(yy + 32 > this.top && yy + 32 < this.bottom) { + bd.markRead(); + } + GlStateManager.pushMatrix(); + GlStateManager.translate(xx, yy, 0.0f); + mc.getTextureManager().bindTexture(largeNotifBk); + int badgeWidth = getListWidth() - 4; + int badgeHeight = getSlotHeight() - 4; + float r = ((bd.backgroundColor >> 16) & 0xFF) * 0.00392156f; + float g = ((bd.backgroundColor >> 8) & 0xFF) * 0.00392156f; + float b = (bd.backgroundColor & 0xFF) * 0.00392156f; + if(parent.selected != id) { + r *= 0.85f; + g *= 0.85f; + b *= 0.85f; + } + GlStateManager.color(r, g, b, 1.0f); + parent.drawTexturedModalRect(0, 0, 0, bd.unreadFlagRender ? 64 : 0, badgeWidth - 32, 64); + parent.drawTexturedModalRect(badgeWidth - 32, 0, 224, bd.unreadFlagRender ? 64 : 0, 32, 64); + mc.getTextureManager().bindTexture(eaglerGui); + if(bd.priority == EnumBadgePriority.LOW) { + parent.drawTexturedModalRect(badgeWidth - 21, badgeHeight - 21, 192, 176, 16, 16); + } + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); + + switch(bd.priority) { + default: + break; + case NORMAL: + parent.drawTexturedModalRect(badgeWidth - 21, badgeHeight - 21, 208, 176, 16, 16); + break; + case HIGHER: + parent.drawTexturedModalRect(badgeWidth - 21, badgeHeight - 21, 224, 176, 16, 16); + break; + case HIGHEST: + parent.drawTexturedModalRect(badgeWidth - 21, badgeHeight - 21, 240, 176, 16, 16); + break; + } + + int bodyYOffset = 16; + + int leftPadding = 6; + int rightPadding = 26; + + int mainIconSW = 32; + boolean mainIconEn = bd.mainIcon != null && bd.mainIcon.isValid(); + if(mainIconEn) { + int iw = bd.mainIcon.texture.getWidth(); + int ih = bd.mainIcon.texture.getHeight(); + float iaspect = (float)iw / (float)ih; + mainIconSW = (int)(32 * iaspect); + leftPadding += Math.min(mainIconSW, 64) + 3; + } + + int textZoneWidth = badgeWidth - leftPadding - rightPadding; + + if(mainIconEn) { + mc.getTextureManager().bindTexture(bd.mainIcon.resource); + ServerNotificationRenderer.drawTexturedRect(6, bodyYOffset, mainIconSW, 32); + } + + boolean titleIconEn = bd.titleIcon != null && bd.titleIcon.isValid(); + if(titleIconEn) { + mc.getTextureManager().bindTexture(bd.titleIcon.resource); + ServerNotificationRenderer.drawTexturedRect(6, 5, 8, 8); + } + + String titleText = ""; + IChatComponent titleComponent = bd.getTitleProfanityFilter(); + if(titleComponent != null) { + titleText = titleComponent.getFormattedText(); + } + + titleText += EnumChatFormatting.GRAY + (titleText.length() > 0 ? " @ " : "@ ") + + (bd.unreadFlagRender ? EnumChatFormatting.YELLOW : EnumChatFormatting.GRAY) + + formatAge(bd.serverTimestamp); + + GlStateManager.pushMatrix(); + GlStateManager.translate(6 + (titleIconEn ? 10 : 0), 6, 0.0f); + GlStateManager.scale(0.75f, 0.75f, 0.75f); + mc.fontRendererObj.drawStringWithShadow(titleText, 0, 0, bd.titleTxtColor); + GlStateManager.popMatrix(); + + String sourceText = null; + IChatComponent sourceComponent = bd.getSourceProfanityFilter(); + if(sourceComponent != null) { + sourceText = sourceComponent.getFormattedText(); + if(sourceText.length() == 0) { + sourceText = null; + } + } + + List bodyLines = null; + float bodyFontSize = (sourceText != null || titleIconEn) ? 0.75f : 1.0f; + IChatComponent bodyComponent = bd.getBodyProfanityFilter(); + if(bodyComponent != null) { + bodyLines = GuiUtilRenderComponents.func_178908_a(bodyComponent, (int) (textZoneWidth / bodyFontSize), + mc.fontRendererObj, true, true); + + int maxHeight = badgeHeight - (sourceText != null ? 32 : 22); + int maxLines = MathHelper.floor_float(maxHeight / (9 * bodyFontSize)); + if(bodyLines.size() > maxLines) { + bodyLines = bodyLines.subList(0, maxLines); + IChatComponent cmp = bodyLines.get(maxLines - 1); + List siblings = cmp.getSiblings(); + IChatComponent dots = new ChatComponentText("..."); + if(siblings != null && siblings.size() > 0) { + dots.setChatStyle(siblings.get(siblings.size() - 1).getChatStyle()); + } + cmp.appendSibling(dots); + } + } + + slot.cursorEvents.clear(); + if(bodyLines != null && !bodyLines.isEmpty()) { + GlStateManager.pushMatrix(); + GlStateManager.translate(leftPadding, bodyYOffset, 0.0f); + int l = bodyLines.size(); + GlStateManager.scale(bodyFontSize, bodyFontSize, bodyFontSize); + IChatComponent toolTip = null; + for(int i = 0; i < l; ++i) { + int startXLocal = 0; + int startXReal = leftPadding; + for(IChatComponent comp : bodyLines.get(i)) { + int w = mc.fontRendererObj.drawStringWithShadow( + comp.getChatStyle().getFormattingCode() + comp.getUnformattedTextForChat(), startXLocal, + i * 9, bd.bodyTxtColor) - startXLocal; + ClickEvent clickEvent = comp.getChatStyle().getChatClickEvent(); + HoverEvent hoverEvent = toolTip == null ? comp.getChatStyle().getChatHoverEvent() : null; + if(clickEvent != null && !clickEvent.getAction().shouldAllowInChat()) { + clickEvent = null; + } + if(hoverEvent != null && !hoverEvent.getAction().shouldAllowInChat()) { + hoverEvent = null; + } + if(clickEvent != null) { + slot.cursorEvents.add(new ClickEventZone(startXReal + (int) (startXLocal * bodyFontSize), + bodyYOffset + (int) (i * 9 * bodyFontSize), (int) (w * bodyFontSize), + (int) (9 * bodyFontSize), comp, clickEvent != null, hoverEvent != null)); + } + if(hoverEvent != null) { + int px = xx + startXReal + (int) (startXLocal * bodyFontSize); + int py = yy + bodyYOffset + (int) (i * 9 * bodyFontSize); + if (mouseX >= px && mouseX < px + (int) (w * bodyFontSize) && mouseY >= py + && mouseY < py + (int) (9 * bodyFontSize)) { + toolTip = comp; + } + } + startXLocal += w; + } + } + GlStateManager.popMatrix(); + if(toolTip != null) { + parent.handleComponentHover(toolTip, mouseX - xx, mouseY - yy); + } + } + + if(sourceText != null) { + GlStateManager.pushMatrix(); + GlStateManager.translate(badgeWidth - 21, badgeHeight - 5, 0.0f); + GlStateManager.scale(0.75f, 0.75f, 0.75f); + mc.fontRendererObj.drawStringWithShadow(sourceText, -mc.fontRendererObj.getStringWidth(sourceText) - 4, -10, bd.sourceTxtColor); + GlStateManager.popMatrix(); + } + + GlStateManager.popMatrix(); + } + } + + private String formatAge(long serverTimestamp) { + long cur = System.currentTimeMillis(); + long daysAgo = Math.round((cur - serverTimestamp) / 86400000.0); + String ret = dateFormat.format(new Date(serverTimestamp)); + if(daysAgo > 0l) { + ret += " (" + daysAgo + (daysAgo == 1l ? " day" : " days") + " ago)"; + }else if(daysAgo < 0l) { + ret += " (in " + -daysAgo + (daysAgo == -1l ? " day" : " days") + ")"; + } + return ret; + } + + @Override + public int getListWidth() { + return 224; + } + + @Override + public void drawScreen(int mouseXIn, int mouseYIn, float parFloat1) { + mouseX = mouseXIn; + mouseY = mouseYIn; + for(int i = 0, l = currentDisplayNotifs.size(); i < l; ++i) { + NotifBadgeSlot slot = currentDisplayNotifs.get(i); + slot.currentScreenX = -69420; + slot.currentScreenY = -69420; + } + super.drawScreen(mouseXIn, mouseYIn, parFloat1); + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/NotificationBadge.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/NotificationBadge.java new file mode 100644 index 00000000..6f17cb0b --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/NotificationBadge.java @@ -0,0 +1,171 @@ +package net.lax1dude.eaglercraft.v1_8.notifications; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; +import net.lax1dude.eaglercraft.v1_8.profanity_filter.ProfanityFilter; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketNotifBadgeShowV4EAG.EnumBadgePriority; +import net.minecraft.client.Minecraft; +import net.minecraft.util.IChatComponent; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class NotificationBadge { + + public final ServerNotificationManager mgr; + public final EaglercraftUUID badgeUUID; + public final IChatComponent bodyComponent; + protected IChatComponent bodyComponentProfanityFilter; + public final IChatComponent titleComponent; + protected IChatComponent titleComponentProfanityFilter; + public final IChatComponent sourceComponent; + protected IChatComponent sourceComponentProfanityFilter; + public final long clientTimestamp; + public final long serverTimestamp; + public final boolean silent; + public final EnumBadgePriority priority; + public final NotificationIcon mainIcon; + public final NotificationIcon titleIcon; + public final int hideAfterSec; + public final int expireAfterSec; + public final int backgroundColor; + public final int bodyTxtColor; + public final int titleTxtColor; + public final int sourceTxtColor; + + protected CachedNotifBadgeTexture currentCacheGLTexture = null; + protected int currentCacheScaleFac = -1; + protected boolean currentCacheXButton = false; + protected boolean currentCacheProfanityFilter = false; + protected long hideAtMillis = -1l; + protected boolean unreadFlag = true; + protected boolean unreadFlagRender = true; + + protected NotificationBadge(ServerNotificationManager mgr, EaglercraftUUID badgeUUID, IChatComponent bodyComponent, + IChatComponent titleComponent, IChatComponent sourceComponent, long clientTimestamp, long serverTimestamp, + boolean silent, EnumBadgePriority priority, NotificationIcon mainIcon, NotificationIcon titleIcon, + int hideAfterSec, int expireAfterSec, int backgroundColor, int bodyTxtColor, int titleTxtColor, + int sourceTxtColor) { + this.mgr = mgr; + this.badgeUUID = badgeUUID; + this.bodyComponent = bodyComponent; + this.titleComponent = titleComponent; + this.sourceComponent = sourceComponent; + this.clientTimestamp = clientTimestamp; + this.serverTimestamp = serverTimestamp; + this.silent = silent; + this.priority = priority; + this.mainIcon = mainIcon; + this.titleIcon = titleIcon; + this.hideAfterSec = hideAfterSec; + this.expireAfterSec = expireAfterSec; + this.backgroundColor = backgroundColor; + this.bodyTxtColor = bodyTxtColor; + this.titleTxtColor = titleTxtColor; + this.sourceTxtColor = sourceTxtColor; + } + + protected void incrIconRefcounts() { + if(mainIcon != null) { + mainIcon.retain(); + } + if(titleIcon != null) { + titleIcon.retain(); + } + } + + protected void decrIconRefcounts() { + deleteGLTexture(); + if(mainIcon != null) { + mainIcon.release(); + } + if(titleIcon != null) { + titleIcon.release(); + } + } + + protected CachedNotifBadgeTexture getGLTexture(ServerNotificationRenderer renderer, int scaleFactor, boolean showXButton) { + boolean profanityFilter = Minecraft.getMinecraft().isEnableProfanityFilter(); + if(currentCacheGLTexture == null || currentCacheScaleFac != scaleFactor || currentCacheXButton != showXButton || currentCacheProfanityFilter != profanityFilter) { + deleteGLTexture(); + currentCacheGLTexture = renderer.renderBadge(this, scaleFactor, showXButton); + currentCacheScaleFac = scaleFactor; + currentCacheXButton = showXButton; + currentCacheProfanityFilter = profanityFilter; + } + return currentCacheGLTexture; + } + + protected void deleteGLTexture() { + if(currentCacheGLTexture != null) { + GlStateManager.deleteTexture(currentCacheGLTexture.glTexture); + currentCacheGLTexture = null; + } + } + + public void hideNotif() { + if(hideAtMillis == -1l) { + markRead(); + unreadFlagRender = false; + hideAtMillis = EagRuntime.steadyTimeMillis(); + } + } + + public void removeNotif() { + mgr.removeNotifFromActiveList(badgeUUID); + } + + public void markRead() { + if(unreadFlag) { + unreadFlag = false; + --mgr.unreadCounter; + } + } + + public IChatComponent getBodyProfanityFilter() { + if(Minecraft.getMinecraft().isEnableProfanityFilter()) { + if(bodyComponentProfanityFilter == null && bodyComponent != null) { + bodyComponentProfanityFilter = ProfanityFilter.getInstance().profanityFilterChatComponent(bodyComponent); + } + return bodyComponentProfanityFilter; + }else { + return bodyComponent; + } + } + + public IChatComponent getTitleProfanityFilter() { + if(Minecraft.getMinecraft().isEnableProfanityFilter()) { + if(titleComponentProfanityFilter == null && titleComponent != null) { + titleComponentProfanityFilter = ProfanityFilter.getInstance().profanityFilterChatComponent(titleComponent); + } + return titleComponentProfanityFilter; + }else { + return titleComponent; + } + } + + public IChatComponent getSourceProfanityFilter() { + if(Minecraft.getMinecraft().isEnableProfanityFilter()) { + if(sourceComponentProfanityFilter == null && sourceComponent != null) { + sourceComponentProfanityFilter = ProfanityFilter.getInstance().profanityFilterChatComponent(sourceComponent); + } + return sourceComponentProfanityFilter; + }else { + return sourceComponent; + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/NotificationIcon.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/NotificationIcon.java new file mode 100644 index 00000000..e67df09c --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/NotificationIcon.java @@ -0,0 +1,51 @@ +package net.lax1dude.eaglercraft.v1_8.notifications; + +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +import net.lax1dude.eaglercraft.v1_8.profile.EaglerSkinTexture; +import net.minecraft.util.ResourceLocation; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class NotificationIcon { + + private static int notifIconTmpId = 0; + + protected int refCount = 0; + protected boolean serverRegistered = true; + + public final EaglercraftUUID iconUUID; + public final EaglerSkinTexture texture; + public final ResourceLocation resource; + + protected NotificationIcon(EaglercraftUUID iconUUID, EaglerSkinTexture texture) { + this.iconUUID = iconUUID; + this.texture = texture; + this.resource = new ResourceLocation("eagler:gui/server/notifs/tex_" + notifIconTmpId++); + } + + public void retain() { + ++refCount; + } + + public void release() { + --refCount; + } + + public boolean isValid() { + return serverRegistered || refCount > 0; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ServerNotificationManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ServerNotificationManager.java new file mode 100644 index 00000000..27b51590 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ServerNotificationManager.java @@ -0,0 +1,277 @@ +package net.lax1dude.eaglercraft.v1_8.notifications; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.opengl.ImageData; +import net.lax1dude.eaglercraft.v1_8.profile.EaglerSkinTexture; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketNotifBadgeHideV4EAG; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketNotifBadgeShowV4EAG; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketNotifIconsRegisterV4EAG; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketNotifIconsReleaseV4EAG; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.util.PacketImageData; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.util.IChatComponent; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ServerNotificationManager { + + private static final Logger logger = LogManager.getLogger("ServerNotificationManager"); + + private final Map activeIcons = new HashMap<>(); + private final Map activeNotifications = new HashMap<>(); + private List sortedNotifList = new ArrayList<>(0); + private List sortedDisplayNotifList = new ArrayList<>(0); + private int updateCounter = 0; + private long lastCleanup = EagRuntime.steadyTimeMillis(); + private final TextureManager textureMgr; + protected int unreadCounter = 0; + + public ServerNotificationManager() { + this.textureMgr = Minecraft.getMinecraft().getTextureManager(); + } + + public void processPacketAddIcons(SPacketNotifIconsRegisterV4EAG packet) { + for(SPacketNotifIconsRegisterV4EAG.CreateIcon icn : packet.iconsToCreate) { + if(icn.uuidMost == 0 && icn.uuidLeast == 0) { + logger.error("Skipping notification icon with UUID 0!"); + continue; + } + EaglercraftUUID uuid = new EaglercraftUUID(icn.uuidMost, icn.uuidLeast); + PacketImageData imageData = icn.imageData; + NotificationIcon existing = activeIcons.get(uuid); + if(existing != null) { + if (existing.texture.getWidth() != imageData.width + || existing.texture.getHeight() != imageData.height) { + logger.error("Error: server tried to change the dimensions of icon {}!", uuid); + }else if(!Arrays.equals(existing.texture.getData(), imageData.rgba)) { + existing.texture.copyPixelsIn(ImageData.swapRB(imageData.rgba)); + } + existing.serverRegistered = true; + continue; + } + NotificationIcon newIcon = new NotificationIcon(uuid, + new EaglerSkinTexture(ImageData.swapRB(imageData.rgba), imageData.width, imageData.height)); + textureMgr.loadTexture(newIcon.resource, newIcon.texture); + activeIcons.put(uuid, newIcon); + } + } + + public void processPacketRemIcons(SPacketNotifIconsReleaseV4EAG packet) { + for(SPacketNotifIconsReleaseV4EAG.DestroyIcon icn : packet.iconsToDestroy) { + NotificationIcon existing = activeIcons.get(new EaglercraftUUID(icn.uuidMost, icn.uuidLeast)); + if(existing != null) { + existing.serverRegistered = false; + } + } + } + + public void processPacketShowBadge(SPacketNotifBadgeShowV4EAG packet) { + EaglercraftUUID newUuid = new EaglercraftUUID(packet.badgeUUIDMost, packet.badgeUUIDLeast); + NotificationBadge existing = activeNotifications.get(newUuid); + if(existing != null) { + logger.error("Duplicate notification UUID {}, all notifications should have unique UUIDs!", newUuid); + return; + } + NotificationBadge newBadge = new NotificationBadge(this, newUuid, + !StringUtils.isAllBlank(packet.bodyComponent) ? IChatComponent.Serializer.jsonToComponent(packet.bodyComponent) : null, + !StringUtils.isAllBlank(packet.titleComponent) ? IChatComponent.Serializer.jsonToComponent(packet.titleComponent) : null, + !StringUtils.isAllBlank(packet.sourceComponent) ? IChatComponent.Serializer.jsonToComponent(packet.sourceComponent) : null, + EagRuntime.steadyTimeMillis(), packet.originalTimestampSec * 1000l, packet.silent, packet.priority, + getIcon(packet.mainIconUUIDMost, packet.mainIconUUIDLeast), + getIcon(packet.titleIconUUIDMost, packet.titleIconUUIDLeast), packet.hideAfterSec, packet.expireAfterSec, + packet.backgroundColor, packet.bodyTxtColor, packet.titleTxtColor, packet.sourceTxtColor); + ++unreadCounter; + addNotifToActiveList(newBadge); + } + + private NotificationIcon getIcon(long uuidMost, long uuidLeast) { + if(uuidMost == 0l && uuidLeast == 0l) { + return null; + } + return activeIcons.get(new EaglercraftUUID(uuidMost, uuidLeast)); + } + + public void processPacketHideBadge(SPacketNotifBadgeHideV4EAG packet) { + removeNotifFromActiveList(new EaglercraftUUID(packet.badgeUUIDLeast, packet.badgeUUIDMost)); + } + + public int getNotifListUpdateCount() { + return updateCounter; + } + + public List getNotifBadgesToDisplay() { + return sortedDisplayNotifList; + } + + public List getNotifLongHistory() { + return sortedNotifList; + } + + protected void addNotifToActiveList(NotificationBadge badge) { + NotificationBadge exists = activeNotifications.put(badge.badgeUUID, badge); + if(exists != null) { + exists.decrIconRefcounts(); + } + badge.incrIconRefcounts(); + resortLists(); + } + + protected void removeNotifFromActiveList(EaglercraftUUID badge) { + NotificationBadge exists = activeNotifications.remove(badge); + if(exists != null) { + exists.decrIconRefcounts(); + resortLists(); + } + } + + protected void removeAllNotifFromActiveList(Collection badges) { + boolean resort = false; + for(NotificationBadge badge : badges) { + NotificationBadge exists = activeNotifications.remove(badge.badgeUUID); + if(exists != null) { + exists.decrIconRefcounts(); + resort = true; + } + } + if(resort) { + resortLists(); + } + } + + protected static final Comparator clientAgeComparator = (a, b) -> { + return (int)(b.clientTimestamp - a.clientTimestamp); + }; + + private void resortLists() { + updateCounter++; + int ll = activeNotifications.size(); + if(!sortedNotifList.isEmpty()) sortedNotifList = new ArrayList<>(ll); + if(!sortedDisplayNotifList.isEmpty()) sortedDisplayNotifList = new ArrayList<>(Math.min(ll, 4)); + if(ll > 0) { + sortedNotifList.addAll(activeNotifications.values()); + Collections.sort(sortedNotifList, clientAgeComparator); + long millis = EagRuntime.steadyTimeMillis(); + for(int i = 0, l = sortedNotifList.size(); i < l; ++i) { + NotificationBadge bd = sortedNotifList.get(i); + if(millis - bd.clientTimestamp < (long)(bd.hideAfterSec * 1000)) { + sortedDisplayNotifList.add(bd); + }else { + bd.deleteGLTexture(); + } + } + } + } + + public void runTick() { + long millis = EagRuntime.steadyTimeMillis(); + if(millis - lastCleanup > 2500l) { + lastCleanup = millis; + int len = sortedNotifList.size(); + if(len > 128) { + removeAllNotifFromActiveList(new ArrayList(sortedNotifList.subList(128, len))); + } + Iterator itr = activeIcons.values().iterator(); + while(itr.hasNext()) { + NotificationIcon icn = itr.next(); + if(!icn.isValid()) { + itr.remove(); + textureMgr.deleteTexture(icn.resource); + } + } + if(!sortedDisplayNotifList.isEmpty()) { + Iterator itr2 = sortedDisplayNotifList.iterator(); + while(itr2.hasNext()) { + NotificationBadge bd = itr2.next(); + if(bd.hideAtMillis != -1l) { + if(millis - bd.hideAtMillis > 500l) { + bd.deleteGLTexture(); + itr2.remove(); + } + }else { + long age = millis - bd.clientTimestamp; + if(age > (long)(bd.hideAfterSec * 1000) || age > (long)(bd.expireAfterSec * 1000)) { + bd.deleteGLTexture(); + itr2.remove(); + } + } + } + } + if(!activeNotifications.isEmpty()) { + Iterator itr3 = activeNotifications.values().iterator(); + List toDelete = null; + while(itr3.hasNext()) { + NotificationBadge bd = itr3.next(); + long age = millis - bd.clientTimestamp; + if(age > (long)(bd.expireAfterSec * 1000)) { + if(toDelete == null) { + toDelete = new ArrayList<>(); + } + toDelete.add(bd); + } + } + if(toDelete != null) { + removeAllNotifFromActiveList(toDelete); + } + } + } + } + + public int getUnread() { + if(unreadCounter < 0) unreadCounter = 0; + return unreadCounter; + } + + public void commitUnreadFlag() { + for(NotificationBadge badge : activeNotifications.values()) { + badge.unreadFlagRender = badge.unreadFlag; + } + } + + public void markRead() { + for(NotificationBadge badge : activeNotifications.values()) { + badge.unreadFlag = false; + badge.unreadFlagRender = false; + } + unreadCounter = 0; + } + + public void destroy() { + for(NotificationIcon icn : activeIcons.values()) { + textureMgr.deleteTexture(icn.resource); + } + activeIcons.clear(); + activeNotifications.clear(); + sortedNotifList = null; + sortedDisplayNotifList = null; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ServerNotificationRenderer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ServerNotificationRenderer.java new file mode 100644 index 00000000..b61b5434 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/notifications/ServerNotificationRenderer.java @@ -0,0 +1,539 @@ +package net.lax1dude.eaglercraft.v1_8.notifications; + +import java.util.ArrayList; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.internal.IFramebufferGL; +import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; +import net.lax1dude.eaglercraft.v1_8.opengl.VertexFormat; +import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.gui.GuiChat; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiUtilRenderComponents; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.event.ClickEvent; +import net.minecraft.event.HoverEvent; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.IChatComponent; +import net.minecraft.util.MathHelper; +import net.minecraft.util.ResourceLocation; + +import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL.*; +import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; +import static net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.ExtGLEnums.*; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ServerNotificationRenderer { + + protected static final Logger logger = LogManager.getLogger("ServerNotificationRenderer"); + + protected Minecraft mc; + protected int width; + protected int height; + protected int scaleFactor; + + protected IFramebufferGL rendererFramebuffer; + + protected static final int BADGE_WIDTH = 160; + protected static final int BADGE_HEIGHT = 64; + + private static final ResourceLocation eaglerGui = new ResourceLocation("eagler:gui/eagler_gui.png"); + + public ServerNotificationRenderer() { + + } + + public void init() { + destroy(); + rendererFramebuffer = _wglCreateFramebuffer(); + } + + public void setResolution(Minecraft mc, int w, int h, int scaleFactor) { + this.mc = mc; + this.width = w; + this.height = h; + this.scaleFactor = scaleFactor; + } + + public boolean handleClicked(GuiScreen currentScreen, int posX, int posY) { + if(mc.thePlayer == null) return false; + ServerNotificationManager mgr = mc.thePlayer.sendQueue.getNotifManager(); + List lst = mgr.getNotifBadgesToDisplay(); + if(!lst.isEmpty()) { + int baseOffset = mc.guiAchievement.getHeight(); + boolean showX = (currentScreen instanceof GuiChat); + if(showX) { + baseOffset += 25; // exit button in chat screen; + } + long millis = EagRuntime.steadyTimeMillis(); + for(int i = 0, l = lst.size(); i < l; ++i) { + NotificationBadge badge = lst.get(i); + CachedNotifBadgeTexture tex = badge.currentCacheGLTexture; + if(tex != null) { + int baseX = width - tex.width; + float texHeight = tex.height; + float timeRemainingSec; + long age = millis - badge.clientTimestamp; + if(badge.hideAtMillis != -1l) { + timeRemainingSec = (float)((double)(500l - (millis - badge.hideAtMillis)) * 0.001); + }else { + timeRemainingSec = (float)((double)((long)badge.hideAfterSec * 1000l - age) * 0.001); + } + timeRemainingSec = Math.min((float)(age * 0.001) + 0.001f, timeRemainingSec); + float f = MathHelper.clamp_float(timeRemainingSec * 3.0F, 0.0F, 1.0F); + f *= f; + texHeight *= f; + if(badge.hideAtMillis == -1l) { + if(posX >= baseX && posX < width && posY >= baseOffset && posY < baseOffset + texHeight) { + if(showX) { + int xposX = baseX + tex.width - 21; + int xposY = baseOffset + 5; + if(posX >= xposX && posY >= xposY && posX < xposX + 16 && posY < xposY + 16) { + badge.hideNotif(); + mc.getSoundHandler().playSound(PositionedSoundRecord.create(new ResourceLocation("gui.button.press"), 1.0F)); + return true; + } + } + if(tex.rootClickEvent != null) { + if(currentScreen.handleComponentClick(tex.rootClickEvent)) { + mc.getSoundHandler().playSound(PositionedSoundRecord.create(new ResourceLocation("gui.button.press"), 1.0F)); + return true; + } + } + List cursorEvents = tex.cursorEvents; + if(tex.hasClickEvents && cursorEvents != null) { + for(int j = 0, m = cursorEvents.size(); j < m; ++j) { + ClickEventZone evt = cursorEvents.get(j); + if(evt.hasClickEvent) { + int offsetPosX = baseX + evt.posX; + int offsetPosY = baseOffset + evt.posY; + if(posX >= offsetPosX && posY >= offsetPosY && posX < offsetPosX + evt.width && posY < offsetPosY + evt.height) { + if(currentScreen.handleComponentClick(evt.chatComponent)) { + mc.getSoundHandler().playSound(PositionedSoundRecord.create(new ResourceLocation("gui.button.press"), 1.0F)); + return true; + } + } + } + } + } + } + } + baseOffset += texHeight; + } + } + } + return false; + } + + public void renderOverlay(int mouseX, int mouseY) { + if(mc.thePlayer == null) return; + ServerNotificationManager mgr = mc.thePlayer.sendQueue.getNotifManager(); + List lst = mgr.getNotifBadgesToDisplay(); + if(!lst.isEmpty()) { + GlStateManager.clear(GL_DEPTH_BUFFER_BIT); + boolean showXButtons = false; + int baseOffset = mc.guiAchievement.getHeight(); + if(mc.currentScreen != null) { + if(mc.currentScreen instanceof GuiChat) { + baseOffset += 25; // exit button in chat screen; + showXButtons = true; + }else if(mc.currentScreen instanceof GuiScreenNotifications) { + return; + } + } + long millis = EagRuntime.steadyTimeMillis(); + boolean isBlend = false; + for(int i = 0, l = lst.size(); i < l; ++i) { + NotificationBadge badge = lst.get(i); + boolean isHiding = false; + if(badge.hideAtMillis != -1l) { + isHiding = true; + if(millis - badge.hideAtMillis > 500l) { + continue; + } + } + CachedNotifBadgeTexture tex = badge.getGLTexture(this, scaleFactor, showXButtons); + if(tex != null) { + GlStateManager.bindTexture(tex.glTexture); + float alphaTop = 1.0f; + float alphaBottom = 1.0f; + float timeRemainingSec; + long age = millis - badge.clientTimestamp; + if(isHiding) { + timeRemainingSec = (float)((double)(500l - (millis - badge.hideAtMillis)) * 0.001); + }else { + timeRemainingSec = (float)((double)((long)badge.hideAfterSec * 1000l - age) * 0.001); + } + timeRemainingSec = Math.min((float)(age * 0.001) + 0.001f, timeRemainingSec); + alphaTop *= MathHelper.clamp_float(timeRemainingSec * 3.0F, 0.0F, 1.0F); + alphaTop *= alphaTop; + alphaBottom *= MathHelper.clamp_float(timeRemainingSec * 2.0F, 0.0F, 1.0F); + alphaBottom *= alphaBottom; + if(alphaTop == 0.0F && alphaBottom == 0.0F) { + continue; + } + boolean blend = alphaTop < 1.0f || alphaBottom < 1.0f; + if(blend != isBlend) { + if(blend) { + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, 1, 0); + }else { + GlStateManager.disableBlend(); + } + isBlend = blend; + } + int px = width - tex.width; + drawTexturedGradientFBRect(px, baseOffset, tex.width, tex.height, + ((int) (alphaTop * 255.0f) << 24) | 0xFFFFFF, + ((int) (alphaBottom * 255.0f) << 24) | 0xFFFFFF, 200.0f); + if(showXButtons && tex.hasHoverEvents) { + if(mouseX >= px && mouseY >= baseOffset && mouseX < px + tex.width && mouseY < baseOffset + tex.height) { + List cursorEvents = tex.cursorEvents; + if(cursorEvents != null) { + for(int j = 0, m = cursorEvents.size(); j < m; ++j) { + ClickEventZone evt = cursorEvents.get(j); + if(evt.hasHoverEvent) { + int offsetPosX = px + evt.posX; + int offsetPosY = baseOffset + evt.posY; + if(mouseX >= offsetPosX && mouseY >= offsetPosY && mouseX < offsetPosX + evt.width + && mouseY < offsetPosY + evt.height) { + if(isBlend) { + GlStateManager.disableBlend(); + isBlend = false; + } + mc.currentScreen.handleComponentHover(evt.chatComponent, mouseX, mouseY); + } + } + } + } + } + } + baseOffset += tex.height * alphaTop; + } + } + if(isBlend) { + GlStateManager.disableBlend(); + } + } + } + + protected CachedNotifBadgeTexture renderBadge(NotificationBadge badge, int scaleFactor, boolean showXButton) { + int badgeWidth = BADGE_WIDTH; + int badgeHeight = 10; + + int leftPadding = 6; + int rightPadding = 26; + + int mainIconSW = 32; + if(badge.mainIcon != null) { + int iw = badge.mainIcon.texture.getWidth(); + int ih = badge.mainIcon.texture.getHeight(); + float iaspect = (float)iw / (float)ih; + mainIconSW = (int)(32 * iaspect); + leftPadding += Math.min(mainIconSW, 64) + 3; + } + + int textZoneWidth = badgeWidth - leftPadding - rightPadding; + int bodyYOffset = 5; + + String titleText = null; + IChatComponent titleComponent = badge.getTitleProfanityFilter(); + if(titleComponent != null) { + titleText = titleComponent.getFormattedText(); + if(titleText.length() > 0) { + badgeHeight += 12; + bodyYOffset += 12; + }else { + titleText = null; + } + } + + if(badge.titleIcon != null && titleText == null) { + badgeHeight += 12; + bodyYOffset += 12; + } + + float bodyFontSize = 0.75f; + List bodyLines = null; + List clickEvents = null; + IChatComponent rootClickEvt = null; + boolean hasClickEvents = false; + boolean hasHoverEvents = false; + + int bodyHeight = 0; + + IChatComponent bodyComponent = badge.getBodyProfanityFilter(); + if(bodyComponent != null) { + if (bodyComponent.getChatStyle().getChatClickEvent() != null + && bodyComponent.getChatStyle().getChatClickEvent().getAction().shouldAllowInChat()) { + rootClickEvt = bodyComponent; + } + bodyLines = GuiUtilRenderComponents.func_178908_a(bodyComponent, (int) (textZoneWidth / bodyFontSize), + mc.fontRendererObj, true, true); + + int maxHeight = BADGE_HEIGHT - 32; + int maxLines = MathHelper.floor_float(maxHeight / (9 * bodyFontSize)); + if(bodyLines.size() > maxLines) { + bodyLines = bodyLines.subList(0, maxLines); + bodyComponent = bodyLines.get(maxLines - 1); + List siblings = bodyComponent.getSiblings(); + IChatComponent dots = new ChatComponentText("..."); + if(siblings != null && siblings.size() > 0) { + dots.setChatStyle(siblings.get(siblings.size() - 1).getChatStyle()); + } + bodyComponent.appendSibling(dots); + } + bodyHeight = MathHelper.floor_float(bodyLines.size() * (9 * bodyFontSize)); + } + + String sourceText = null; + IChatComponent sourceComponent = badge.getSourceProfanityFilter(); + if(sourceComponent != null) { + sourceText = sourceComponent.getFormattedText(); + if(sourceText.length() == 0) { + sourceText = null; + } + } + + if(badge.mainIcon != null) { + bodyHeight = Math.max(sourceText != null ? 30 : 32, bodyHeight); + } + + if(sourceText != null) { + badgeHeight += 6; + } + + badgeHeight += bodyHeight; + + badgeHeight = Math.max(badgeHeight, showXButton ? 42 : 26); + + if(badgeHeight > BADGE_HEIGHT) { + logger.info("Warning: Badge {} was {} pixels too high!", badge.badgeUUID, BADGE_HEIGHT - badgeHeight); + badgeHeight = BADGE_HEIGHT; + } + + int glTex = GlStateManager.generateTexture(); + GlStateManager.bindTexture(glTex); + EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + EaglercraftGPU.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + EaglercraftGPU.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, badgeWidth * scaleFactor, badgeHeight * scaleFactor, 0, GL_RGBA, + GL_UNSIGNED_BYTE, (ByteBuffer) null); + _wglBindFramebuffer(_GL_FRAMEBUFFER, rendererFramebuffer); + _wglFramebufferTexture2D(_GL_FRAMEBUFFER, _GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, EaglercraftGPU.getNativeTexture(glTex), 0); + _wglDrawBuffers(_GL_COLOR_ATTACHMENT0); + + int[] oldViewport = new int[4]; + EaglercraftGPU.glGetInteger(GL_VIEWPORT, oldViewport); + + GlStateManager.viewport(0, 0, badgeWidth * scaleFactor, badgeHeight * scaleFactor); + + GlStateManager.disableDepth(); + GlStateManager.depthMask(false); + GlStateManager.enableTexture2D(); + GlStateManager.disableLighting(); + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); + + GlStateManager.matrixMode(GL_PROJECTION); + GlStateManager.pushMatrix(); + GlStateManager.loadIdentity(); + GlStateManager.ortho(0.0D, badgeWidth, badgeHeight, 0.0D, 1000.0D, 3000.0D); + GlStateManager.matrixMode(GL_MODELVIEW); + GlStateManager.pushMatrix(); + GlStateManager.loadIdentity(); + GlStateManager.translate(0.0F, 0.0F, -2000.0F); + + Tessellator tess = Tessellator.getInstance(); + WorldRenderer worldRenderer = tess.getWorldRenderer(); + + worldRenderer.begin(GL_QUADS, VertexFormat.POSITION_TEX_COLOR); + + mc.getTextureManager().bindTexture(eaglerGui); + + drawTexturedColoredRect(worldRenderer, 0, 0, 96, 192, 160, 8, (badge.backgroundColor >>> 16) & 0xFF, + (badge.backgroundColor >>> 8) & 0xFF, badge.backgroundColor & 0xFF, 0xFF); + + drawTexturedColoredRect(worldRenderer, 0, 8, 96, 192 + (BADGE_HEIGHT - badgeHeight + 8), 160, (badgeHeight - 8), + (badge.backgroundColor >>> 16) & 0xFF, (badge.backgroundColor >>> 8) & 0xFF, + badge.backgroundColor & 0xFF, 0xFF); + + switch(badge.priority) { + case LOW: + default: + drawTexturedColoredRect(worldRenderer, badgeWidth - 21, badgeHeight - 21, 192, 176, 16, 16, (badge.backgroundColor >>> 16) & 0xFF, + (badge.backgroundColor >>> 8) & 0xFF, badge.backgroundColor & 0xFF, 0xFF); + break; + case NORMAL: + drawTexturedColoredRect(worldRenderer, badgeWidth - 21, badgeHeight - 21, 208, 176, 16, 16, 0xFF, 0xFF, 0xFF, 0xFF); + break; + case HIGHER: + drawTexturedColoredRect(worldRenderer, badgeWidth - 21, badgeHeight - 21, 224, 176, 16, 16, 0xFF, 0xFF, 0xFF, 0xFF); + break; + case HIGHEST: + drawTexturedColoredRect(worldRenderer, badgeWidth - 21, badgeHeight - 21, 240, 176, 16, 16, 0xFF, 0xFF, 0xFF, 0xFF); + break; + } + + if(showXButton) { + drawTexturedColoredRect(worldRenderer, badgeWidth - 21, 5, 80, 208, 16, 16, 0xFF, 0xFF, 0xFF, 0xFF); + } + + tess.draw(); + + if(badge.mainIcon != null) { + mc.getTextureManager().bindTexture(badge.mainIcon.resource); + drawTexturedRect(6, bodyYOffset, mainIconSW, 32); + } + + if(badge.titleIcon != null) { + mc.getTextureManager().bindTexture(badge.titleIcon.resource); + drawTexturedRect(6, 5, 8, 8); + } + + if(titleText != null) { + GlStateManager.pushMatrix(); + GlStateManager.translate(6 + (badge.titleIcon != null ? 10 : 0), 6, 0.0f); + GlStateManager.scale(0.75f, 0.75f, 0.75f); + mc.fontRendererObj.drawStringWithShadow(titleText, 0, 0, badge.titleTxtColor); + GlStateManager.popMatrix(); + } + + if(bodyLines != null && !bodyLines.isEmpty()) { + GlStateManager.pushMatrix(); + if(!showXButton && badge.mainIcon == null && titleText != null) { + bodyYOffset -= 2; + } + GlStateManager.translate(leftPadding, bodyYOffset, 0.0f); + int l = bodyLines.size(); + GlStateManager.scale(bodyFontSize, bodyFontSize, bodyFontSize); + for(int i = 0; i < l; ++i) { + int startXLocal = 0; + int startXReal = leftPadding; + for(IChatComponent comp : bodyLines.get(i)) { + int w = mc.fontRendererObj.drawStringWithShadow( + comp.getChatStyle().getFormattingCode() + comp.getUnformattedTextForChat(), startXLocal, + i * 9, badge.bodyTxtColor) - startXLocal; + ClickEvent clickEvent = comp.getChatStyle().getChatClickEvent(); + HoverEvent hoverEvent = comp.getChatStyle().getChatHoverEvent(); + if(clickEvent != null && !clickEvent.getAction().shouldAllowInChat()) { + clickEvent = null; + } + if(hoverEvent != null && !hoverEvent.getAction().shouldAllowInChat()) { + hoverEvent = null; + } + if(clickEvent != null || hoverEvent != null) { + hasClickEvents |= clickEvent != null; + hasHoverEvents |= hoverEvent != null; + if(clickEvents == null) { + clickEvents = new ArrayList<>(); + } + clickEvents.add(new ClickEventZone(startXReal + (int) (startXLocal * bodyFontSize), + bodyYOffset + (int) (i * 9 * bodyFontSize), (int) (w * bodyFontSize), + (int) (9 * bodyFontSize), comp, clickEvent != null, hoverEvent != null)); + } + startXLocal += w; + } + } + GlStateManager.popMatrix(); + } + + if(sourceText != null) { + GlStateManager.pushMatrix(); + GlStateManager.translate(badgeWidth - 21, badgeHeight - 5, 0.0f); + GlStateManager.scale(0.5f, 0.5f, 0.5f); + mc.fontRendererObj.drawStringWithShadow(sourceText, -mc.fontRendererObj.getStringWidth(sourceText) - 4, -10, badge.sourceTxtColor); + GlStateManager.popMatrix(); + } + + GlStateManager.matrixMode(GL_PROJECTION); + GlStateManager.popMatrix(); + GlStateManager.matrixMode(GL_MODELVIEW); + GlStateManager.popMatrix(); + + GlStateManager.depthMask(true); + GlStateManager.enableDepth(); + + _wglBindFramebuffer(_GL_FRAMEBUFFER, null); + GlStateManager.viewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]); + + return new CachedNotifBadgeTexture(glTex, scaleFactor, badgeWidth, badgeHeight, clickEvents, rootClickEvt, hasClickEvents, hasHoverEvents); + } + + static void drawTexturedColoredRect(WorldRenderer worldRenderer, float xCoord, float yCoord, int minU, + int minV, int width, int height, int r, int g, int b, int a) { + float f = 0.00390625F; + float f1 = 0.00390625F; + worldRenderer.pos((double) (xCoord + 0.0F), (double) (yCoord + (float) height), 0.0).color(r, g, b, a) + .tex((double) ((float) (minU + 0) * f), (double) ((float) (minV + height) * f1)).endVertex(); + worldRenderer.pos((double) (xCoord + (float) width), (double) (yCoord + (float) height), 0.0).color(r, g, b, a) + .tex((double) ((float) (minU + width) * f), (double) ((float) (minV + height) * f1)).endVertex(); + worldRenderer.pos((double) (xCoord + (float) width), (double) (yCoord + 0.0F), 0.0).color(r, g, b, a) + .tex((double) ((float) (minU + width) * f), (double) ((float) (minV + 0) * f1)).endVertex(); + worldRenderer.pos((double) (xCoord + 0.0F), (double) (yCoord + 0.0F), 0.0).color(r, g, b, a) + .tex((double) ((float) (minU + 0) * f), (double) ((float) (minV + 0) * f1)).endVertex(); + } + + static void drawTexturedGradientFBRect(float xCoord, float yCoord, int width, int height, int rgbaTop, int rgbaBottom, float zIndex) { + int topR = (rgbaTop >>> 16) & 0xFF; + int topG = (rgbaTop >>> 8) & 0xFF; + int topB = rgbaTop & 0xFF; + int topA = (rgbaTop >>> 24) & 0xFF; + int bottomR = (rgbaBottom >>> 16) & 0xFF; + int bottomG = (rgbaBottom >>> 8) & 0xFF; + int bottomB = rgbaBottom & 0xFF; + int bottomA = (rgbaBottom >>> 24) & 0xFF; + Tessellator tess = Tessellator.getInstance(); + WorldRenderer worldRenderer = tess.getWorldRenderer(); + worldRenderer.begin(GL_QUADS, VertexFormat.POSITION_TEX_COLOR); + worldRenderer.pos((double) (xCoord + 0.0F), (double) (yCoord + (float) height), zIndex) + .color(bottomR, bottomG, bottomB, bottomA).tex(0.0, 0.0).endVertex(); + worldRenderer.pos((double) (xCoord + (float) width), (double) (yCoord + (float) height), zIndex) + .color(bottomR, bottomG, bottomB, bottomA).tex(1.0, 0.0).endVertex(); + worldRenderer.pos((double) (xCoord + (float) width), (double) (yCoord + 0.0F), zIndex) + .color(topR, topG, topB, topA).tex(1.0, 1.0).endVertex(); + worldRenderer.pos((double) (xCoord + 0.0F), (double) (yCoord + 0.0F), zIndex).color(topR, topG, topB, topA) + .tex(0.0, 1.0).endVertex(); + tess.draw(); + } + + static void drawTexturedRect(float xCoord, float yCoord, int width, int height) { + Tessellator tess = Tessellator.getInstance(); + WorldRenderer worldRenderer = tess.getWorldRenderer(); + worldRenderer.begin(GL_QUADS, VertexFormat.POSITION_TEX); + worldRenderer.pos((double) (xCoord + 0.0F), (double) (yCoord + (float) height), 0.0).tex(0.0, 1.0).endVertex(); + worldRenderer.pos((double) (xCoord + (float) width), (double) (yCoord + (float) height), 0.0).tex(1.0, 1.0).endVertex(); + worldRenderer.pos((double) (xCoord + (float) width), (double) (yCoord + 0.0F), 0.0).tex(1.0, 0.0).endVertex(); + worldRenderer.pos((double) (xCoord + 0.0F), (double) (yCoord + 0.0F), 0.0).tex(0.0, 0.0).endVertex(); + tess.draw(); + } + + public void destroy() { + if(rendererFramebuffer != null) { + _wglDeleteFramebuffer(rendererFramebuffer); + rendererFramebuffer = null; + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/DrawUtils.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/DrawUtils.java index a3adccae..58872a1e 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/DrawUtils.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/DrawUtils.java @@ -3,15 +3,16 @@ package net.lax1dude.eaglercraft.v1_8.opengl; import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL.*; import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; +import java.util.List; + import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.internal.IBufferArrayGL; import net.lax1dude.eaglercraft.v1_8.internal.IBufferGL; import net.lax1dude.eaglercraft.v1_8.internal.IShaderGL; import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer; -import net.lax1dude.eaglercraft.v1_8.opengl.FixedFunctionShader.FixedFunctionConstants; /** - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -28,17 +29,19 @@ import net.lax1dude.eaglercraft.v1_8.opengl.FixedFunctionShader.FixedFunctionCon public class DrawUtils { public static final String vertexShaderPath = "/assets/eagler/glsl/local.vsh"; + public static final String vertexShaderPrecision = "precision highp float;\n"; public static IBufferArrayGL standardQuad2DVAO = null; public static IBufferArrayGL standardQuad3DVAO = null; public static IBufferGL standardQuadVBO = null; public static IShaderGL vshLocal = null; + public static List vshLocalLayout = null; static void init() { if(standardQuad2DVAO == null) { - standardQuad2DVAO = _wglGenVertexArrays(); - standardQuad3DVAO = _wglGenVertexArrays(); + standardQuad2DVAO = EaglercraftGPU.createGLBufferArray(); + standardQuad3DVAO = EaglercraftGPU.createGLBufferArray(); standardQuadVBO = _wglGenBuffers(); FloatBuffer verts = EagRuntime.allocateFloatBuffer(18); @@ -48,30 +51,29 @@ public class DrawUtils { }); verts.flip(); - EaglercraftGPU.bindGLArrayBuffer(standardQuadVBO); + EaglercraftGPU.bindVAOGLArrayBufferNow(standardQuadVBO); _wglBufferData(GL_ARRAY_BUFFER, verts, GL_STATIC_DRAW); EagRuntime.freeFloatBuffer(verts); EaglercraftGPU.bindGLBufferArray(standardQuad2DVAO); - _wglEnableVertexAttribArray(0); - _wglVertexAttribPointer(0, 2, GL_FLOAT, false, 12, 0); + EaglercraftGPU.enableVertexAttribArray(0); + EaglercraftGPU.vertexAttribPointer(0, 2, GL_FLOAT, false, 12, 0); EaglercraftGPU.bindGLBufferArray(standardQuad3DVAO); - _wglEnableVertexAttribArray(0); - _wglVertexAttribPointer(0, 3, GL_FLOAT, false, 12, 0); + EaglercraftGPU.enableVertexAttribArray(0); + EaglercraftGPU.vertexAttribPointer(0, 3, GL_FLOAT, false, 12, 0); } if(vshLocal == null) { - String vertexSource = EagRuntime.getResourceString(vertexShaderPath); - if(vertexSource == null) { - throw new RuntimeException("vertex shader \"" + vertexShaderPath + "\" is missing!"); - } - + String vertexSource = EagRuntime.getRequiredResourceString(vertexShaderPath); + + vshLocalLayout = VSHInputLayoutParser.getShaderInputs(vertexSource); + vshLocal = _wglCreateShader(GL_VERTEX_SHADER); - - _wglShaderSource(vshLocal, FixedFunctionConstants.VERSION + "\n" + vertexSource); + + _wglShaderSource(vshLocal, GLSLHeader.getVertexHeaderCompat(vertexSource, vertexShaderPrecision)); _wglCompileShader(vshLocal); if(_wglGetShaderi(vshLocal, GL_COMPILE_STATUS) != GL_TRUE) { @@ -90,12 +92,32 @@ public class DrawUtils { public static void drawStandardQuad2D() { EaglercraftGPU.bindGLBufferArray(standardQuad2DVAO); - _wglDrawArrays(GL_TRIANGLES, 0, 6); + EaglercraftGPU.doDrawArrays(GL_TRIANGLES, 0, 6); } public static void drawStandardQuad3D() { EaglercraftGPU.bindGLBufferArray(standardQuad3DVAO); - _wglDrawArrays(GL_TRIANGLES, 0, 6); + EaglercraftGPU.doDrawArrays(GL_TRIANGLES, 0, 6); + } + + public static void destroy() { + if(standardQuad2DVAO != null) { + EaglercraftGPU.destroyGLBufferArray(standardQuad2DVAO); + standardQuad2DVAO = null; + } + if(standardQuad3DVAO != null) { + EaglercraftGPU.destroyGLBufferArray(standardQuad3DVAO); + standardQuad3DVAO = null; + } + if(standardQuadVBO != null) { + _wglDeleteBuffers(standardQuadVBO); + standardQuadVBO = null; + } + if(vshLocal != null) { + vshLocal.free(); + vshLocal = null; + vshLocalLayout = null; + } } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EaglerMeshLoader.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EaglerMeshLoader.java index e4c6f11b..cb1c7b60 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EaglerMeshLoader.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EaglerMeshLoader.java @@ -39,7 +39,7 @@ public class EaglerMeshLoader implements IResourceManagerReloadListener { private static final Logger logger = LogManager.getLogger("EaglerMeshLoader"); - private static final Map meshCache = new HashMap(); + private static final Map meshCache = new HashMap<>(); public static HighPolyMesh getEaglerMesh(ResourceLocation meshLoc) { if(meshLoc.cachedPointerType == ResourceLocation.CACHED_POINTER_EAGLER_MESH) { @@ -104,7 +104,7 @@ public class EaglerMeshLoader implements IResourceManagerReloadListener { } if(meshStruct.vertexArray == null) { - meshStruct.vertexArray = _wglGenVertexArrays(); + meshStruct.vertexArray = EaglercraftGPU.createGLBufferArray(); } if(meshStruct.vertexBuffer == null) { meshStruct.vertexBuffer = _wglGenBuffers(); @@ -115,29 +115,29 @@ public class EaglerMeshLoader implements IResourceManagerReloadListener { up1.position(0).limit(intsOfVertex); - EaglercraftGPU.bindGLArrayBuffer(meshStruct.vertexBuffer); + EaglercraftGPU.bindVAOGLArrayBufferNow(meshStruct.vertexBuffer); _wglBufferData(GL_ARRAY_BUFFER, up1, GL_STATIC_DRAW); EaglercraftGPU.bindGLBufferArray(meshStruct.vertexArray); up1.position(intsOfVertex).limit(intsTotal); - _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, meshStruct.indexBuffer); + EaglercraftGPU.bindVAOGLElementArrayBufferNow(meshStruct.indexBuffer); _wglBufferData(GL_ELEMENT_ARRAY_BUFFER, up1, GL_STATIC_DRAW); - _wglEnableVertexAttribArray(0); - _wglVertexAttribPointer(0, 3, GL_FLOAT, false, stride, 0); + EaglercraftGPU.enableVertexAttribArray(0); + EaglercraftGPU.vertexAttribPointer(0, 3, GL_FLOAT, false, stride, 0); if(meshStruct.hasTexture) { - _wglEnableVertexAttribArray(1); - _wglVertexAttribPointer(1, 2, GL_FLOAT, false, stride, 16); + EaglercraftGPU.enableVertexAttribArray(1); + EaglercraftGPU.vertexAttribPointer(1, 2, GL_FLOAT, false, stride, 16); } - _wglEnableVertexAttribArray(meshStruct.hasTexture ? 2 : 1); - _wglVertexAttribPointer(meshStruct.hasTexture ? 2 : 1, 4, GL_BYTE, true, stride, 12); + EaglercraftGPU.enableVertexAttribArray(meshStruct.hasTexture ? 2 : 1); + EaglercraftGPU.vertexAttribPointer(meshStruct.hasTexture ? 2 : 1, 4, GL_BYTE, true, stride, 12); }catch(Throwable ex) { if(meshStruct.vertexArray != null) { - _wglDeleteVertexArrays(meshStruct.vertexArray); + EaglercraftGPU.destroyGLBufferArray(meshStruct.vertexArray); meshStruct.vertexArray = null; } if(meshStruct.vertexBuffer != null) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EaglercraftGPU.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EaglercraftGPU.java index a5b60b2f..87ac654d 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EaglercraftGPU.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EaglercraftGPU.java @@ -5,6 +5,7 @@ import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer; import net.lax1dude.eaglercraft.v1_8.internal.buffer.IntBuffer; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.minecraft.util.MathHelper; import java.util.HashMap; import java.util.Map; @@ -16,7 +17,6 @@ import net.lax1dude.eaglercraft.v1_8.internal.IBufferGL; import net.lax1dude.eaglercraft.v1_8.internal.IProgramGL; import net.lax1dude.eaglercraft.v1_8.internal.IQueryGL; import net.lax1dude.eaglercraft.v1_8.internal.ITextureGL; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformBufferFunctions; import net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL; import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; @@ -39,12 +39,15 @@ import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL.*; */ public class EaglercraftGPU { - static final GLObjectMap mapTexturesGL = new GLObjectMap(32767); - static final GLObjectMap mapQueriesGL = new GLObjectMap(32767); - static final GLObjectMap mapDisplayListsGL = new GLObjectMap(32767); + static final GLObjectMap mapTexturesGL = new GLObjectMap<>(8192); + static final GLObjectMap mapQueriesGL = new GLObjectMap<>(8192); + static final GLObjectMap mapDisplayListsGL = new GLObjectMap<>(8192); static final Logger logger = LogManager.getLogger("EaglercraftGPU"); + static boolean emulatedVAOs = false; + static SoftGLBufferState emulatedVAOState = new SoftGLBufferState(); + public static final String gluErrorString(int i) { switch(i) { case GL_INVALID_ENUM: return "GL_INVALID_ENUM"; @@ -87,16 +90,16 @@ public class EaglercraftGPU { EaglercraftGPU.bindGLBufferArray(dp.vertexArray); int c = 0; if((dp.attribs & ATTRIB_TEXTURE) == ATTRIB_TEXTURE) { - _wglDisableVertexAttribArray(++c); + EaglercraftGPU.disableVertexAttribArray(++c); } if((dp.attribs & ATTRIB_COLOR) == ATTRIB_COLOR) { - _wglDisableVertexAttribArray(++c); + EaglercraftGPU.disableVertexAttribArray(++c); } if((dp.attribs & ATTRIB_NORMAL) == ATTRIB_NORMAL) { - _wglDisableVertexAttribArray(++c); + EaglercraftGPU.disableVertexAttribArray(++c); } if((dp.attribs & ATTRIB_LIGHTMAP) == ATTRIB_LIGHTMAP) { - _wglDisableVertexAttribArray(++c); + EaglercraftGPU.disableVertexAttribArray(++c); } } dp.attribs = -1; @@ -109,7 +112,7 @@ public class EaglercraftGPU { if(displayListBuffer.capacity() < wantSize) { int newSize = (wantSize & 0xFFFE0000) + 0x40000; ByteBuffer newBuffer = EagRuntime.allocateByteBuffer(newSize); - PlatformBufferFunctions.put(newBuffer, (ByteBuffer)displayListBuffer.flip()); + newBuffer.put((ByteBuffer)displayListBuffer.flip()); EagRuntime.freeByteBuffer(displayListBuffer); displayListBuffer = newBuffer; } @@ -123,7 +126,7 @@ public class EaglercraftGPU { if(dp.attribs == -1) { if(dp.vertexArray != null) { - _wglDeleteVertexArrays(dp.vertexArray); + EaglercraftGPU.destroyGLBufferArray(dp.vertexArray); dp.vertexArray = null; } if(dp.vertexBuffer != null) { @@ -135,7 +138,7 @@ public class EaglercraftGPU { } if(dp.vertexArray == null) { - dp.vertexArray = _wglGenVertexArrays(); + dp.vertexArray = createGLBufferArray(); dp.bindQuad16 = false; dp.bindQuad32 = false; } @@ -143,7 +146,7 @@ public class EaglercraftGPU { dp.vertexBuffer = _wglGenBuffers(); } - bindGLArrayBuffer(dp.vertexBuffer); + bindVAOGLArrayBufferNow(dp.vertexBuffer); displayListBuffer.flip(); _wglBufferData(GL_ARRAY_BUFFER, displayListBuffer, GL_STATIC_DRAW); displayListBuffer.clear(); @@ -194,7 +197,7 @@ public class EaglercraftGPU { } dp.attribs = -1; if(dp.vertexArray != null) { - _wglDeleteVertexArrays(dp.vertexArray); + EaglercraftGPU.destroyGLBufferArray(dp.vertexArray); dp.vertexArray = null; } if(dp.vertexBuffer != null) { @@ -210,7 +213,7 @@ public class EaglercraftGPU { ++GlStateManager.stateNormalSerial; } - private static final Map stringCache = new HashMap(); + private static final Map stringCache = new HashMap<>(); public static final String glGetString(int param) { String str = stringCache.get(param); @@ -238,9 +241,24 @@ public class EaglercraftGPU { return _wglGetInteger(param); } + public static final void glTexImage2D(int target, int level, int internalFormat, int w, int h, int unused, + int format, int type, ByteBuffer pixels) { + if(glesVers >= 300) { + _wglTexImage2D(target, level, internalFormat, w, h, unused, format, type, pixels); + }else { + int tv = TextureFormatHelper.trivializeInternalFormatToGLES20(internalFormat); + _wglTexImage2D(target, level, tv, w, h, unused, tv, type, pixels); + } + } + public static final void glTexImage2D(int target, int level, int internalFormat, int w, int h, int unused, int format, int type, IntBuffer pixels) { - _wglTexImage2D(target, level, internalFormat, w, h, unused, format, type, pixels); + if(glesVers >= 300) { + _wglTexImage2D(target, level, internalFormat, w, h, unused, format, type, pixels); + }else { + int tv = TextureFormatHelper.trivializeInternalFormatToGLES20(internalFormat); + _wglTexImage2D(target, level, tv, w, h, unused, tv, type, pixels); + } } public static final void glTexSubImage2D(int target, int level, int x, int y, int w, int h, int format, @@ -249,9 +267,32 @@ public class EaglercraftGPU { } public static final void glTexStorage2D(int target, int levels, int internalFormat, int w, int h) { - _wglTexStorage2D(target, levels, internalFormat, w, h); + if(texStorageCapable && (glesVers >= 300 || levels == 1 || (MathHelper.calculateLogBaseTwo(Math.max(w, h)) + 1) == levels)) { + _wglTexStorage2D(target, levels, internalFormat, w, h); + }else { + int tv = TextureFormatHelper.trivializeInternalFormatToGLES20(internalFormat); + int type = TextureFormatHelper.getTypeFromInternal(internalFormat); + for(int i = 0; i < levels; ++i) { + _wglTexImage2D(target, i, tv, Math.max(w >> i, 1), Math.max(h >> i, 1), 0, tv, type, (ByteBuffer)null); + } + } } - + + public static final void glReadPixels(int x, int y, int width, int height, int format, int type, ByteBuffer buffer) { + switch(type) { + case GL_FLOAT: + _wglReadPixels(x, y, width, height, format, GL_FLOAT, buffer.asFloatBuffer()); + break; + case 0x140B: // GL_HALF_FLOAT + _wglReadPixels_u16(x, y, width, height, format, glesVers == 200 ? 0x8D61 : 0x140B, buffer); + break; + case GL_UNSIGNED_BYTE: + default: + _wglReadPixels(x, y, width, height, format, type, buffer); + break; + } + } + public static final void glLineWidth(float f) { _wglLineWidth(f); } @@ -284,7 +325,7 @@ public class EaglercraftGPU { DisplayList d = mapDisplayListsGL.free(id); if(d != null) { if(d.vertexArray != null) { - _wglDeleteVertexArrays(d.vertexArray); + EaglercraftGPU.destroyGLBufferArray(d.vertexArray); } if(d.vertexBuffer != null) { _wglDeleteBuffers(d.vertexBuffer); @@ -302,18 +343,197 @@ public class EaglercraftGPU { GlStateManager.stateBlendEquation = equation; } } - - private static IBufferArrayGL currentBufferArray = null; - - public static final void bindGLBufferArray(IBufferArrayGL buffer) { - if(currentBufferArray != buffer) { - _wglBindVertexArray(buffer); - currentBufferArray = buffer; + + public static final boolean areVAOsEmulated() { + return emulatedVAOs; + } + + public static final IBufferArrayGL createGLBufferArray() { + if(emulatedVAOs) { + return new SoftGLBufferArray(); + }else { + return _wglGenVertexArrays(); } } + + public static final void destroyGLBufferArray(IBufferArrayGL buffer) { + if(!emulatedVAOs) { + _wglDeleteVertexArrays(buffer); + } + } + + public static final void enableVertexAttribArray(int index) { + if(emulatedVAOs) { + if(currentBufferArray == null) { + logger.warn("Skipping enable attrib with emulated VAO because no known VAO is bound!"); + return; + } + ((SoftGLBufferArray)currentBufferArray).enableAttrib(index, true); + }else { + _wglEnableVertexAttribArray(index); + } + } + + public static final void disableVertexAttribArray(int index) { + if(emulatedVAOs) { + if(currentBufferArray == null) { + logger.warn("Skipping disable attrib with emulated VAO because no known VAO is bound!"); + return; + } + ((SoftGLBufferArray)currentBufferArray).enableAttrib(index, false); + }else { + _wglDisableVertexAttribArray(index); + } + } + + public static final void vertexAttribPointer(int index, int size, int format, boolean normalized, int stride, int offset) { + if(emulatedVAOs) { + if(currentBufferArray == null) { + logger.warn("Skipping vertexAttribPointer with emulated VAO because no known VAO is bound!"); + return; + } + if(currentVAOArrayBuffer == null) { + logger.warn("Skipping vertexAttribPointer with emulated VAO because no VAO array buffer is bound!"); + return; + } + ((SoftGLBufferArray)currentBufferArray).setAttrib(currentVAOArrayBuffer, index, size, format, normalized, stride, offset); + }else { + _wglVertexAttribPointer(index, size, format, normalized, stride, offset); + } + } + + public static final void vertexAttribDivisor(int index, int divisor) { + if(emulatedVAOs) { + if(currentBufferArray == null) { + logger.warn("Skipping vertexAttribPointer with emulated VAO because no known VAO is bound!"); + return; + } + ((SoftGLBufferArray)currentBufferArray).setAttribDivisor(index, divisor); + }else { + _wglVertexAttribDivisor(index, divisor); + } + } + + public static final void doDrawArrays(int mode, int first, int count) { + if(emulatedVAOs) { + if(currentBufferArray == null) { + logger.warn("Skipping draw call with emulated VAO because no known VAO is bound!"); + return; + } + ((SoftGLBufferArray)currentBufferArray).transitionToState(emulatedVAOState, false); + } + _wglDrawArrays(mode, first, count); + } + + public static final void doDrawElements(int mode, int count, int type, int offset) { + if(emulatedVAOs) { + if(currentBufferArray == null) { + logger.warn("Skipping draw call with emulated VAO because no known VAO is bound!"); + return; + } + ((SoftGLBufferArray)currentBufferArray).transitionToState(emulatedVAOState, true); + } + _wglDrawElements(mode, count, type, offset); + } + + public static final void doDrawArraysInstanced(int mode, int first, int count, int instances) { + if(emulatedVAOs) { + if(currentBufferArray == null) { + logger.warn("Skipping instanced draw call with emulated VAO because no known VAO is bound!"); + return; + } + ((SoftGLBufferArray)currentBufferArray).transitionToState(emulatedVAOState, false); + } + _wglDrawArraysInstanced(mode, first, count, instances); + } + + public static final void doDrawElementsInstanced(int mode, int count, int type, int offset, int instances) { + if(emulatedVAOs) { + if(currentBufferArray == null) { + logger.warn("Skipping instanced draw call with emulated VAO because no known VAO is bound!"); + return; + } + ((SoftGLBufferArray)currentBufferArray).transitionToState(emulatedVAOState, true); + } + _wglDrawElementsInstanced(mode, count, type, offset, instances); + } + + static IBufferArrayGL currentBufferArray = null; - private static IBufferGL currentArrayBuffer = null; - + public static final void bindGLBufferArray(IBufferArrayGL buffer) { + if(emulatedVAOs) { + currentBufferArray = buffer; + }else { + if(currentBufferArray != buffer) { + _wglBindVertexArray(buffer); + currentBufferArray = buffer; + } + } + } + + static IBufferGL currentArrayBuffer = null; + + // only used when VAOs are emulated + static IBufferGL currentVAOArrayBuffer = null; + + public static final void bindVAOGLArrayBuffer(IBufferGL buffer) { + if(emulatedVAOs) { + currentVAOArrayBuffer = buffer; + }else { + if(currentArrayBuffer != buffer) { + _wglBindBuffer(GL_ARRAY_BUFFER, buffer); + currentArrayBuffer = buffer; + } + } + } + + public static final void bindVAOGLArrayBufferNow(IBufferGL buffer) { + if(emulatedVAOs) { + currentVAOArrayBuffer = buffer; + } + if(currentArrayBuffer != buffer) { + _wglBindBuffer(GL_ARRAY_BUFFER, buffer); + currentArrayBuffer = buffer; + } + } + + public static final void bindVAOGLElementArrayBuffer(IBufferGL buffer) { + if(emulatedVAOs) { + if(currentBufferArray == null) { + logger.warn("Skipping set element array buffer with emulated VAO because no known VAO is bound!"); + return; + } + ((SoftGLBufferArray)currentBufferArray).setIndexBuffer(buffer); + }else { + _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer); + } + } + + static final void bindVAOGLElementArrayBufferNow(IBufferGL buffer) { + if(emulatedVAOs) { + if(currentBufferArray == null) { + logger.warn("Skipping set element array buffer with emulated VAO because no known VAO is bound!"); + return; + } + ((SoftGLBufferArray)currentBufferArray).setIndexBuffer(buffer); + if(currentEmulatedVAOIndexBuffer != buffer) { + _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer); + currentEmulatedVAOIndexBuffer = buffer; + } + }else { + _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer); + } + } + + static IBufferGL currentEmulatedVAOIndexBuffer = null; + + static final void bindEmulatedVAOIndexBuffer(IBufferGL buffer) { + if(currentEmulatedVAOIndexBuffer != buffer) { + _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer); + currentEmulatedVAOIndexBuffer = buffer; + } + } + public static final void bindGLArrayBuffer(IBufferGL buffer) { if(currentArrayBuffer != buffer) { _wglBindBuffer(GL_ARRAY_BUFFER, buffer); @@ -321,7 +541,7 @@ public class EaglercraftGPU { } } - private static IBufferGL currentUniformBuffer = null; + static IBufferGL currentUniformBuffer = null; public static final void bindGLUniformBuffer(IBufferGL buffer) { if(currentUniformBuffer != buffer) { @@ -330,7 +550,7 @@ public class EaglercraftGPU { } } - private static IProgramGL currentShaderProgram = null; + static IProgramGL currentShaderProgram = null; public static final void bindGLShaderProgram(IProgramGL prog) { if(currentShaderProgram != prog) { @@ -352,6 +572,38 @@ public class EaglercraftGPU { currentUniformBlockBindingSize[index] = size; } } + + public static final int CLEAR_BINDING_TEXTURE = 1; + public static final int CLEAR_BINDING_TEXTURE0 = 2; + public static final int CLEAR_BINDING_ACTIVE_TEXTURE = 4; + public static final int CLEAR_BINDING_BUFFER_ARRAY = 8; + public static final int CLEAR_BINDING_ARRAY_BUFFER = 16; + public static final int CLEAR_BINDING_SHADER_PROGRAM = 32; + + public static final void clearCurrentBinding(int mask) { + if((mask & CLEAR_BINDING_TEXTURE) != 0) { + int[] i = GlStateManager.boundTexture; + for(int j = 0; j < i.length; ++j) { + i[j] = -1; + } + } + if((mask & CLEAR_BINDING_TEXTURE0) != 0) { + GlStateManager.boundTexture[0] = -1; + } + if((mask & CLEAR_BINDING_ACTIVE_TEXTURE) != 0) { + GlStateManager.activeTexture = 0; + _wglActiveTexture(GL_TEXTURE0); + } + if((mask & CLEAR_BINDING_BUFFER_ARRAY) != 0) { + currentBufferArray = null; + } + if((mask & CLEAR_BINDING_ARRAY_BUFFER) != 0) { + currentArrayBuffer = currentVAOArrayBuffer = null; + } + if((mask & CLEAR_BINDING_SHADER_PROGRAM) != 0) { + currentShaderProgram = null; + } + } public static final int ATTRIB_TEXTURE = 1; public static final int ATTRIB_COLOR = 2; @@ -414,7 +666,7 @@ public class EaglercraftGPU { if(newSize > 0xFFFF) { newSize = 0xFFFF; } - _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buf); + EaglercraftGPU.bindVAOGLElementArrayBufferNow(buf); resizeQuad16EmulationBuffer(newSize >> 2); }else { int cnt = quad16EmulationBufferSize; @@ -423,10 +675,10 @@ public class EaglercraftGPU { if(newSize > 0xFFFF) { newSize = 0xFFFF; } - _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buf); + EaglercraftGPU.bindVAOGLElementArrayBufferNow(buf); resizeQuad16EmulationBuffer(newSize >> 2); }else if(bind) { - _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buf); + EaglercraftGPU.bindVAOGLElementArrayBuffer(buf); } } } @@ -436,16 +688,16 @@ public class EaglercraftGPU { if(buf == null) { quad32EmulationBuffer = buf = _wglGenBuffers(); int newSize = quad32EmulationBufferSize = (vertexCount & 0xFFFFC000) + 0x8000; - _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buf); + EaglercraftGPU.bindVAOGLElementArrayBufferNow(buf); resizeQuad32EmulationBuffer(newSize >> 2); }else { int cnt = quad32EmulationBufferSize; if(cnt < vertexCount) { int newSize = quad32EmulationBufferSize = (vertexCount & 0xFFFFC000) + 0x8000; - _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buf); + EaglercraftGPU.bindVAOGLElementArrayBufferNow(buf); resizeQuad32EmulationBuffer(newSize >> 2); }else if(bind) { - _wglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buf); + EaglercraftGPU.bindVAOGLElementArrayBuffer(buf); } } } @@ -508,9 +760,19 @@ public class EaglercraftGPU { p.drawElements(GL_TRIANGLES, mesh.indexCount, GL_UNSIGNED_SHORT, 0); } + static int glesVers = -1; static boolean hasFramebufferHDR16FSupport = false; static boolean hasFramebufferHDR32FSupport = false; + static boolean hasLinearHDR16FSupport = false; static boolean hasLinearHDR32FSupport = false; + static boolean fboRenderMipmapCapable = false; + static boolean vertexArrayCapable = false; + static boolean instancingCapable = false; + static boolean texStorageCapable = false; + static boolean textureLODCapable = false; + static boolean shader5Capable = false; + static boolean npotCapable = false; + static int uniformBufferOffsetAlignment = -1; public static final void createFramebufferHDR16FTexture(int target, int level, int w, int h, int format, boolean allow32bitFallback) { createFramebufferHDR16FTexture(target, level, w, h, format, allow32bitFallback, null); @@ -525,19 +787,24 @@ public class EaglercraftGPU { int internalFormat; switch(format) { case GL_RED: - internalFormat = 0x822D; // GL_R16F + if(glesVers == 200) { + format = GL_LUMINANCE; + internalFormat = GL_LUMINANCE; + }else { + internalFormat = glesVers == 200 ? GL_LUMINANCE : 0x822D; // GL_R16F + } break; case 0x8227: // GL_RG - internalFormat = 0x822F; // GL_RG16F + internalFormat = glesVers == 200 ? 0x8227 : 0x822F; // GL_RG16F case GL_RGB: throw new UnsupportedOperationException("GL_RGB16F isn't supported specifically in WebGL 2.0 for some goddamn reason"); case GL_RGBA: - internalFormat = 0x881A; // GL_RGBA16F + internalFormat = glesVers == 200 ? GL_RGBA : 0x881A; // GL_RGBA16F break; default: throw new UnsupportedOperationException("Unknown format: " + format); } - _wglTexImage2Du16(target, level, internalFormat, w, h, 0, format, 0x140B, pixelData); + _wglTexImage2Du16(target, level, internalFormat, w, h, 0, format, glesVers == 200 ? 0x8D61 : 0x140B, pixelData); }else { if(allow32bitFallback) { if(hasFramebufferHDR32FSupport) { @@ -567,7 +834,7 @@ public class EaglercraftGPU { internalFormat = 0x822E; // GL_R32F break; case 0x8227: // GL_RG - internalFormat = 0x822F; // GL_RG32F + internalFormat = 0x8230; // GL_RG32F case GL_RGB: throw new UnsupportedOperationException("GL_RGB32F isn't supported specifically in WebGL 2.0 for some goddamn reason"); case GL_RGBA: @@ -594,19 +861,38 @@ public class EaglercraftGPU { EaglercraftGPU.glGetString(7936); EaglercraftGPU.glGetString(7937); EaglercraftGPU.glGetString(7938); + glesVers = PlatformOpenGL.checkOpenGLESVersion(); + vertexArrayCapable = PlatformOpenGL.checkVAOCapable(); + emulatedVAOs = !vertexArrayCapable; + fboRenderMipmapCapable = PlatformOpenGL.checkFBORenderMipmapCapable(); + instancingCapable = PlatformOpenGL.checkInstancingCapable(); + texStorageCapable = PlatformOpenGL.checkTexStorageCapable(); + textureLODCapable = PlatformOpenGL.checkTextureLODCapable(); + shader5Capable = PlatformOpenGL.checkOESGPUShader5Capable() || PlatformOpenGL.checkEXTGPUShader5Capable(); + npotCapable = PlatformOpenGL.checkNPOTCapable(); + uniformBufferOffsetAlignment = glesVers >= 300 ? _wglGetInteger(0x8A34) : -1; + if(!npotCapable) { + logger.warn("NPOT texture support detected as false, texture wrapping must be set to GL_CLAMP_TO_EDGE if the texture's width or height is not a power of 2"); + } hasFramebufferHDR16FSupport = PlatformOpenGL.checkHDRFramebufferSupport(16); if(hasFramebufferHDR16FSupport) { logger.info("16-bit HDR render target support: true"); }else { logger.error("16-bit HDR render target support: false"); } + hasLinearHDR16FSupport = PlatformOpenGL.checkLinearHDRFilteringSupport(16); + if(hasLinearHDR16FSupport) { + logger.info("16-bit HDR linear filter support: true"); + }else { + logger.error("16-bit HDR linear filter support: false"); + } hasFramebufferHDR32FSupport = PlatformOpenGL.checkHDRFramebufferSupport(32); if(hasFramebufferHDR32FSupport) { logger.info("32-bit HDR render target support: true"); }else { logger.error("32-bit HDR render target support: false"); } - hasLinearHDR32FSupport = PlatformOpenGL.checkLinearHDR32FSupport(); + hasLinearHDR32FSupport = PlatformOpenGL.checkLinearHDRFilteringSupport(32); if(hasLinearHDR32FSupport) { logger.info("32-bit HDR linear filter support: true"); }else { @@ -615,16 +901,86 @@ public class EaglercraftGPU { if(!checkHasHDRFramebufferSupportWithFilter()) { logger.error("No HDR render target support was detected! Shaders will be disabled."); } + if(emulatedVAOs) { + logger.info("Note: Could not unlock VAOs via OpenGL extensions, emulating them instead"); + } + if(!instancingCapable) { + logger.info("Note: Could not unlock instancing via OpenGL extensions, using slow vanilla font and particle rendering"); + } + emulatedVAOState = emulatedVAOs ? new SoftGLBufferState() : null; + PlatformOpenGL.enterVAOEmulationHook(); + GLSLHeader.init(); DrawUtils.init(); SpriteLevelMixer.initialize(); - InstancedFontRenderer.initialize(); - InstancedParticleRenderer.initialize(); + if(instancingCapable) { + InstancedFontRenderer.initialize(); + InstancedParticleRenderer.initialize(); + } EffectPipelineFXAA.initialize(); TextureCopyUtil.initialize(); DrawUtils.vshLocal.free(); DrawUtils.vshLocal = null; } + public static final void destroyCache() { + stringCache.clear(); + mapTexturesGL.clear(); + mapQueriesGL.clear(); + mapDisplayListsGL.clear(); + emulatedVAOs = false; + emulatedVAOState = null; + glesVers = -1; + fboRenderMipmapCapable = false; + vertexArrayCapable = false; + instancingCapable = false; + hasFramebufferHDR16FSupport = false; + hasFramebufferHDR32FSupport = false; + hasLinearHDR32FSupport = false; + GLSLHeader.destroy(); + DrawUtils.destroy(); + SpriteLevelMixer.destroy(); + InstancedFontRenderer.destroy(); + InstancedParticleRenderer.destroy(); + EffectPipelineFXAA.destroy(); + TextureCopyUtil.destroy(); + } + + public static final int checkOpenGLESVersion() { + return glesVers; + } + + public static final boolean checkFBORenderMipmapCapable() { + return fboRenderMipmapCapable; + } + + public static final boolean checkVAOCapable() { + return vertexArrayCapable; + } + + public static final boolean checkInstancingCapable() { + return instancingCapable; + } + + public static final boolean checkTexStorageCapable() { + return texStorageCapable; + } + + public static final boolean checkTextureLODCapable() { + return textureLODCapable; + } + + public static final boolean checkShader5Capable() { + return shader5Capable; + } + + public static final boolean checkNPOTCapable() { + return npotCapable; + } + + public static final int getUniformBufferOffsetAlignment() { + return uniformBufferOffsetAlignment; + } + public static final boolean checkHDRFramebufferSupport(int bits) { switch(bits) { case 16: @@ -636,15 +992,28 @@ public class EaglercraftGPU { } } + public static final boolean checkLinearHDRFilteringSupport(int bits) { + switch(bits) { + case 16: + return hasLinearHDR16FSupport; + case 32: + return hasLinearHDR32FSupport; + default: + return false; + } + } + public static final boolean checkHasHDRFramebufferSupport() { return hasFramebufferHDR16FSupport || hasFramebufferHDR32FSupport; } public static final boolean checkHasHDRFramebufferSupportWithFilter() { - return hasFramebufferHDR16FSupport || (hasFramebufferHDR32FSupport && hasLinearHDR32FSupport); + return (hasFramebufferHDR16FSupport && hasLinearHDR16FSupport) || (hasFramebufferHDR32FSupport && hasLinearHDR32FSupport); } + //legacy public static final boolean checkLinearHDR32FSupport() { return hasLinearHDR32FSupport; } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EffectPipelineFXAA.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EffectPipelineFXAA.java index f61fe144..9166ec7f 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EffectPipelineFXAA.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/EffectPipelineFXAA.java @@ -8,7 +8,6 @@ import net.lax1dude.eaglercraft.v1_8.internal.IUniformGL; import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.opengl.FixedFunctionShader.FixedFunctionConstants; import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL.*; @@ -35,11 +34,13 @@ public class EffectPipelineFXAA { private static final Logger logger = LogManager.getLogger("EffectPipelineFXAA"); public static final String fragmentShaderPath = "/assets/eagler/glsl/post_fxaa.fsh"; + public static final String fragmentShaderPrecision = "precision lowp int;\nprecision mediump float;\nprecision mediump sampler2D;\n"; private static final int _GL_FRAMEBUFFER = 0x8D40; private static final int _GL_RENDERBUFFER = 0x8D41; private static final int _GL_COLOR_ATTACHMENT0 = 0x8CE0; private static final int _GL_DEPTH_ATTACHMENT = 0x8D00; + private static final int _GL_DEPTH_COMPONENT16 = 0x81A5; private static final int _GL_DEPTH_COMPONENT32F = 0x8CAC; private static IProgramGL shaderProgram = null; @@ -53,14 +54,11 @@ public class EffectPipelineFXAA { private static int currentHeight = -1; static void initialize() { - String fragmentSource = EagRuntime.getResourceString(fragmentShaderPath); - if(fragmentSource == null) { - throw new RuntimeException("EffectPipelineFXAA shader \"" + fragmentShaderPath + "\" is missing!"); - } + String fragmentSource = EagRuntime.getRequiredResourceString(fragmentShaderPath); IShaderGL frag = _wglCreateShader(GL_FRAGMENT_SHADER); - _wglShaderSource(frag, FixedFunctionConstants.VERSION + "\n" + fragmentSource); + _wglShaderSource(frag, GLSLHeader.getFragmentHeaderCompat(fragmentSource, fragmentShaderPrecision)); _wglCompileShader(frag); if(_wglGetShaderi(frag, GL_COMPILE_STATUS) != GL_TRUE) { @@ -80,6 +78,10 @@ public class EffectPipelineFXAA { _wglAttachShader(shaderProgram, DrawUtils.vshLocal); _wglAttachShader(shaderProgram, frag); + if(EaglercraftGPU.checkOpenGLESVersion() == 200) { + VSHInputLayoutParser.applyLayout(shaderProgram, DrawUtils.vshLocalLayout); + } + _wglLinkProgram(shaderProgram); _wglDetachShader(shaderProgram, DrawUtils.vshLocal); @@ -130,10 +132,10 @@ public class EffectPipelineFXAA { currentHeight = height; GlStateManager.bindTexture(framebufferColor); - _wglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (ByteBuffer)null); + EaglercraftGPU.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (ByteBuffer)null); _wglBindRenderbuffer(_GL_RENDERBUFFER, framebufferDepth); - _wglRenderbufferStorage(_GL_RENDERBUFFER, _GL_DEPTH_COMPONENT32F, width, height); + _wglRenderbufferStorage(_GL_RENDERBUFFER, EaglercraftGPU.checkOpenGLESVersion() == 200 ? _GL_DEPTH_COMPONENT16 : _GL_DEPTH_COMPONENT32F, width, height); } _wglBindFramebuffer(_GL_FRAMEBUFFER, framebuffer); @@ -155,4 +157,24 @@ public class EffectPipelineFXAA { DrawUtils.drawStandardQuad2D(); } + public static void destroy() { + if(shaderProgram != null) { + _wglDeleteProgram(shaderProgram); + shaderProgram = null; + } + u_screenSize2f = null; + if(framebuffer != null) { + _wglDeleteFramebuffer(framebuffer); + framebuffer = null; + } + if(framebufferColor != -1) { + GlStateManager.deleteTexture(framebufferColor); + framebufferColor = -2; + } + if(framebufferDepth != null) { + _wglDeleteRenderbuffer(framebufferDepth); + framebufferDepth = null; + } + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/FixedFunctionPipeline.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/FixedFunctionPipeline.java index 1b714c0f..8b65c2e5 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/FixedFunctionPipeline.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/FixedFunctionPipeline.java @@ -98,33 +98,33 @@ public class FixedFunctionPipeline { } EaglercraftGPU.bindGLBufferArray(list.vertexArray); - EaglercraftGPU.bindGLArrayBuffer(list.vertexBuffer); + EaglercraftGPU.bindVAOGLArrayBuffer(list.vertexBuffer); - _wglEnableVertexAttribArray(0); - _wglVertexAttribPointer(0, VertexFormat.COMPONENT_POSITION_SIZE, + EaglercraftGPU.enableVertexAttribArray(0); + EaglercraftGPU.vertexAttribPointer(0, VertexFormat.COMPONENT_POSITION_SIZE, VertexFormat.COMPONENT_POSITION_FORMAT, false, self.attribStride, 0); if(self.attribTextureIndex != -1) { - _wglEnableVertexAttribArray(self.attribTextureIndex); - _wglVertexAttribPointer(self.attribTextureIndex, VertexFormat.COMPONENT_TEX_SIZE, + EaglercraftGPU.enableVertexAttribArray(self.attribTextureIndex); + EaglercraftGPU.vertexAttribPointer(self.attribTextureIndex, VertexFormat.COMPONENT_TEX_SIZE, VertexFormat.COMPONENT_TEX_FORMAT, false, self.attribStride, self.attribTextureOffset); } if(self.attribColorIndex != -1) { - _wglEnableVertexAttribArray(self.attribColorIndex); - _wglVertexAttribPointer(self.attribColorIndex, VertexFormat.COMPONENT_COLOR_SIZE, + EaglercraftGPU.enableVertexAttribArray(self.attribColorIndex); + EaglercraftGPU.vertexAttribPointer(self.attribColorIndex, VertexFormat.COMPONENT_COLOR_SIZE, VertexFormat.COMPONENT_COLOR_FORMAT, true, self.attribStride, self.attribColorOffset); } if(self.attribNormalIndex != -1) { - _wglEnableVertexAttribArray(self.attribNormalIndex); - _wglVertexAttribPointer(self.attribNormalIndex, VertexFormat.COMPONENT_NORMAL_SIZE, + EaglercraftGPU.enableVertexAttribArray(self.attribNormalIndex); + EaglercraftGPU.vertexAttribPointer(self.attribNormalIndex, VertexFormat.COMPONENT_NORMAL_SIZE, VertexFormat.COMPONENT_NORMAL_FORMAT, true, self.attribStride, self.attribNormalOffset); } if(self.attribLightmapIndex != -1) { - _wglEnableVertexAttribArray(self.attribLightmapIndex); - _wglVertexAttribPointer(self.attribLightmapIndex, VertexFormat.COMPONENT_LIGHTMAP_SIZE, + EaglercraftGPU.enableVertexAttribArray(self.attribLightmapIndex); + EaglercraftGPU.vertexAttribPointer(self.attribLightmapIndex, VertexFormat.COMPONENT_LIGHTMAP_SIZE, VertexFormat.COMPONENT_LIGHTMAP_FORMAT, false, self.attribStride, self.attribLightmapOffset); } @@ -145,7 +145,7 @@ public class FixedFunctionPipeline { void drawArrays(int mode, int offset, int count) { EaglercraftGPU.bindGLShaderProgram(shaderProgram); - PlatformOpenGL._wglDrawArrays(mode, offset, count); + EaglercraftGPU.doDrawArrays(mode, offset, count); } void drawDirectArrays(int mode, int offset, int count) { @@ -160,7 +160,7 @@ public class FixedFunctionPipeline { }else { EaglercraftGPU.attachQuad32EmulationBuffer(count, false); } - PlatformOpenGL._wglDrawElements(GL_TRIANGLES, count + (count >> 1), + EaglercraftGPU.doDrawElements(GL_TRIANGLES, count + (count >> 1), GL_UNSIGNED_INT, 0); }else { if(!sb.bindQuad16) { @@ -170,17 +170,17 @@ public class FixedFunctionPipeline { }else { EaglercraftGPU.attachQuad16EmulationBuffer(count, false); } - PlatformOpenGL._wglDrawElements(GL_TRIANGLES, count + (count >> 1), + EaglercraftGPU.doDrawElements(GL_TRIANGLES, count + (count >> 1), GL_UNSIGNED_SHORT, 0); } }else { - PlatformOpenGL._wglDrawArrays(mode, offset, count); + EaglercraftGPU.doDrawArrays(mode, offset, count); } } void drawElements(int mode, int count, int type, int offset) { EaglercraftGPU.bindGLShaderProgram(shaderProgram); - PlatformOpenGL._wglDrawElements(mode, count, type, offset); + EaglercraftGPU.doDrawElements(mode, count, type, offset); } private static IExtPipelineCompiler extensionProvider; @@ -192,7 +192,7 @@ public class FixedFunctionPipeline { private static final FixedFunctionPipeline[] pipelineStateCache = new FixedFunctionPipeline[fixedFunctionStatesBits + 1]; private static final FixedFunctionPipeline[][] pipelineExtStateCache = new FixedFunctionPipeline[fixedFunctionStatesBits + 1][]; - private static final List pipelineListTracker = new ArrayList(1024); + private static final List pipelineListTracker = new ArrayList<>(1024); private static String shaderSourceCacheVSH = null; private static String shaderSourceCacheFSH = null; @@ -232,22 +232,16 @@ public class FixedFunctionPipeline { fshSource = extSource[1]; }else { if(shaderSourceCacheVSH == null) { - shaderSourceCacheVSH = EagRuntime.getResourceString(FILENAME_VSH); - if(shaderSourceCacheVSH == null) { - throw new RuntimeException("Could not load: " + FILENAME_VSH); - } + shaderSourceCacheVSH = EagRuntime.getRequiredResourceString(FILENAME_VSH); } vshSource = shaderSourceCacheVSH; if(shaderSourceCacheFSH == null) { - shaderSourceCacheFSH = EagRuntime.getResourceString(FILENAME_FSH); - if(shaderSourceCacheFSH == null) { - throw new RuntimeException("Could not load: " + FILENAME_FSH); - } + shaderSourceCacheFSH = EagRuntime.getRequiredResourceString(FILENAME_FSH); } fshSource = shaderSourceCacheFSH; } - StringBuilder macros = new StringBuilder(VERSION + "\n"); + StringBuilder macros = new StringBuilder(); if((coreBits & STATE_HAS_ATTRIB_TEXTURE) != 0) { macros.append("#define " + MACRO_ATTRIB_TEXTURE + "\n"); } @@ -291,7 +285,8 @@ public class FixedFunctionPipeline { IShaderGL vsh = _wglCreateShader(GL_VERTEX_SHADER); - _wglShaderSource(vsh, macros.toString() + vshSource); + String macrosStr = macros.toString(); + _wglShaderSource(vsh, GLSLHeader.getVertexHeaderCompat(vshSource, macrosStr)); _wglCompileShader(vsh); if(_wglGetShaderi(vsh, GL_COMPILE_STATUS) != GL_TRUE) { @@ -309,7 +304,7 @@ public class FixedFunctionPipeline { IShaderGL fsh = _wglCreateShader(GL_FRAGMENT_SHADER); - _wglShaderSource(fsh, macros.toString() + fshSource); + _wglShaderSource(fsh, GLSLHeader.getFragmentHeaderCompat(fshSource, macrosStr)); _wglCompileShader(fsh); if(_wglGetShaderi(fsh, GL_COMPILE_STATUS) != GL_TRUE) { @@ -581,45 +576,45 @@ public class FixedFunctionPipeline { streamBuffer = new StreamBuffer(FixedFunctionShader.initialSize, FixedFunctionShader.initialCount, FixedFunctionShader.maxCount, (vertexArray, vertexBuffer) -> { EaglercraftGPU.bindGLBufferArray(vertexArray); - EaglercraftGPU.bindGLArrayBuffer(vertexBuffer); + EaglercraftGPU.bindVAOGLArrayBuffer(vertexBuffer); - _wglEnableVertexAttribArray(0); - _wglVertexAttribPointer(0, VertexFormat.COMPONENT_POSITION_SIZE, + EaglercraftGPU.enableVertexAttribArray(0); + EaglercraftGPU.vertexAttribPointer(0, VertexFormat.COMPONENT_POSITION_SIZE, VertexFormat.COMPONENT_POSITION_FORMAT, false, attribStride, 0); if(attribTextureIndex != -1) { - _wglEnableVertexAttribArray(attribTextureIndex); - _wglVertexAttribPointer(attribTextureIndex, VertexFormat.COMPONENT_TEX_SIZE, + EaglercraftGPU.enableVertexAttribArray(attribTextureIndex); + EaglercraftGPU.vertexAttribPointer(attribTextureIndex, VertexFormat.COMPONENT_TEX_SIZE, VertexFormat.COMPONENT_TEX_FORMAT, false, attribStride, attribTextureOffset); } if(attribColorIndex != -1) { - _wglEnableVertexAttribArray(attribColorIndex); - _wglVertexAttribPointer(attribColorIndex, VertexFormat.COMPONENT_COLOR_SIZE, + EaglercraftGPU.enableVertexAttribArray(attribColorIndex); + EaglercraftGPU.vertexAttribPointer(attribColorIndex, VertexFormat.COMPONENT_COLOR_SIZE, VertexFormat.COMPONENT_COLOR_FORMAT, true, attribStride, attribColorOffset); } if(attribNormalIndex != -1) { - _wglEnableVertexAttribArray(attribNormalIndex); - _wglVertexAttribPointer(attribNormalIndex, VertexFormat.COMPONENT_NORMAL_SIZE, + EaglercraftGPU.enableVertexAttribArray(attribNormalIndex); + EaglercraftGPU.vertexAttribPointer(attribNormalIndex, VertexFormat.COMPONENT_NORMAL_SIZE, VertexFormat.COMPONENT_NORMAL_FORMAT, true, attribStride, attribNormalOffset); } if(attribLightmapIndex != -1) { - _wglEnableVertexAttribArray(attribLightmapIndex); - _wglVertexAttribPointer(attribLightmapIndex, VertexFormat.COMPONENT_LIGHTMAP_SIZE, + EaglercraftGPU.enableVertexAttribArray(attribLightmapIndex); + EaglercraftGPU.vertexAttribPointer(attribLightmapIndex, VertexFormat.COMPONENT_LIGHTMAP_SIZE, VertexFormat.COMPONENT_LIGHTMAP_FORMAT, false, attribStride, attribLightmapOffset); } }); - stateEnableTexture2D = (bits & STATE_ENABLE_TEXTURE2D) == STATE_ENABLE_TEXTURE2D; - stateEnableLightmap = (bits & STATE_ENABLE_LIGHTMAP) == STATE_ENABLE_LIGHTMAP; - stateEnableAlphaTest = (bits & STATE_ENABLE_ALPHA_TEST) == STATE_ENABLE_ALPHA_TEST; - stateEnableMCLighting = (bits & STATE_ENABLE_MC_LIGHTING) == STATE_ENABLE_MC_LIGHTING; - stateEnableEndPortal = (bits & STATE_ENABLE_END_PORTAL) == STATE_ENABLE_END_PORTAL; - stateEnableAnisotropicFix = (bits & STATE_ENABLE_ANISOTROPIC_FIX) == STATE_ENABLE_ANISOTROPIC_FIX; - stateEnableFog = (bits & STATE_ENABLE_FOG) == STATE_ENABLE_FOG; - stateEnableBlendAdd = (bits & STATE_ENABLE_BLEND_ADD) == STATE_ENABLE_BLEND_ADD; + stateEnableTexture2D = (bits & STATE_ENABLE_TEXTURE2D) != 0; + stateEnableLightmap = (bits & STATE_ENABLE_LIGHTMAP) != 0; + stateEnableAlphaTest = (bits & STATE_ENABLE_ALPHA_TEST) != 0; + stateEnableMCLighting = (bits & STATE_ENABLE_MC_LIGHTING) != 0; + stateEnableEndPortal = (bits & STATE_ENABLE_END_PORTAL) != 0; + stateEnableAnisotropicFix = (bits & STATE_ENABLE_ANISOTROPIC_FIX) != 0; + stateEnableFog = (bits & STATE_ENABLE_FOG) != 0; + stateEnableBlendAdd = (bits & STATE_ENABLE_BLEND_ADD) != 0; for(int i = 0; i < stateLightsVectors.length; ++i) { stateLightsVectors[i] = new Vector4f(-999.0f, -999.0f, -999.0f, 0.0f); @@ -1068,7 +1063,6 @@ public class FixedFunctionPipeline { } static void optimize() { - FixedFunctionPipeline pp; for(int i = 0, l = pipelineListTracker.size(); i < l; ++i) { pipelineListTracker.get(i).streamBuffer.optimize(); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/FixedFunctionShader.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/FixedFunctionShader.java index 3ca87543..392ad203 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/FixedFunctionShader.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/FixedFunctionShader.java @@ -44,7 +44,6 @@ public class FixedFunctionShader { public class FixedFunctionConstants { - public static final String VERSION = "#version 300 es"; public static final String FILENAME_VSH = "/assets/eagler/glsl/core.vsh"; public static final String FILENAME_FSH = "/assets/eagler/glsl/core.fsh"; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GLSLHeader.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GLSLHeader.java new file mode 100644 index 00000000..5e93f199 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GLSLHeader.java @@ -0,0 +1,99 @@ +package net.lax1dude.eaglercraft.v1_8.opengl; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GLSLHeader { + + public static final String GLES2_COMPAT_FILE_NAME = "/assets/eagler/glsl/gles2_compat.glsl"; + + private static String header = null; + private static String gles2CompatFile = null; + + static void init() { + gles2CompatFile = EagRuntime.getRequiredResourceString(GLES2_COMPAT_FILE_NAME); + int glesVersion = EaglercraftGPU.checkOpenGLESVersion(); + StringBuilder headerBuilder; + if(glesVersion >= 310) { + headerBuilder = new StringBuilder("#version 310 es"); + boolean oes5 = PlatformOpenGL.checkOESGPUShader5Capable(); + boolean ext5 = !oes5 && PlatformOpenGL.checkEXTGPUShader5Capable(); + if(oes5) { + headerBuilder.append("\n#extension GL_OES_gpu_shader5 : enable"); + }else if(ext5) { + headerBuilder.append("\n#extension GL_EXT_gpu_shader5 : enable"); + } + headerBuilder.append("\n#define EAGLER_IS_GLES_310"); + headerBuilder.append("\n#define EAGLER_HAS_GLES_310"); + headerBuilder.append("\n#define EAGLER_HAS_GLES_300"); + headerBuilder.append("\n#define EAGLER_HAS_GLES_200"); + if(oes5 || ext5) { + headerBuilder.append("\n#define EAGLER_HAS_GLES_310_SHADER_5"); + } + }else if(glesVersion == 300) { + headerBuilder = new StringBuilder("#version 300 es"); + headerBuilder.append("\n#define EAGLER_IS_GLES_300"); + headerBuilder.append("\n#define EAGLER_HAS_GLES_300"); + headerBuilder.append("\n#define EAGLER_HAS_GLES_200"); + }else if(glesVersion == 200) { + boolean texLOD = PlatformOpenGL.checkTextureLODCapable(); + headerBuilder = new StringBuilder("#version 100"); + if(texLOD) { + headerBuilder.append("\n#extension GL_EXT_shader_texture_lod : enable"); + } + headerBuilder.append("\n#define EAGLER_HAS_GLES_200"); + headerBuilder.append("\n#define EAGLER_IS_GLES_200"); + if(texLOD) { + headerBuilder.append("\n#define EAGLER_HAS_GLES_200_SHADER_TEXTURE_LOD"); + } + }else { + throw new IllegalStateException("Unsupported OpenGL ES version: " + glesVersion); + } + header = headerBuilder.append('\n').toString(); + } + + static void destroy() { + header = null; + } + + public static String getHeader() { + if(header == null) throw new IllegalStateException(); + return header; + } + + public static String getVertexHeader(String shaderSrc) { + if(header == null) throw new IllegalStateException(); + return header + "#define EAGLER_IS_VERTEX_SHADER\n" + shaderSrc; + } + + public static String getFragmentHeader(String shaderSrc) { + if(header == null) throw new IllegalStateException(); + return header + "#define EAGLER_IS_FRAGMENT_SHADER\n" + shaderSrc; + } + + public static String getVertexHeaderCompat(String shaderSrc, String precisions) { + if(header == null || gles2CompatFile == null) throw new IllegalStateException(); + return header + "#define EAGLER_IS_VERTEX_SHADER\n" + (precisions == null ? "" : precisions + "\n") + gles2CompatFile + "\n" + shaderSrc; + } + + public static String getFragmentHeaderCompat(String shaderSrc, String precisions) { + if(header == null || gles2CompatFile == null) throw new IllegalStateException(); + return header + "#define EAGLER_IS_FRAGMENT_SHADER\n"+ (precisions == null ? "" : precisions + "\n") + gles2CompatFile + "\n" + shaderSrc; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GameOverlayFramebuffer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GameOverlayFramebuffer.java index 94a54027..9aec1e65 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GameOverlayFramebuffer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GameOverlayFramebuffer.java @@ -5,10 +5,13 @@ import net.lax1dude.eaglercraft.v1_8.internal.IRenderbufferGL; import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; + import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL.*; /** - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -40,10 +43,20 @@ public class GameOverlayFramebuffer { private int framebufferColor = -1; - public void beginRender(int width, int height) { + private final boolean enableDepth; + + public GameOverlayFramebuffer() { + this(true); + } + + public GameOverlayFramebuffer(boolean enableDepth) { + this.enableDepth = enableDepth; + } + + public boolean beginRender(int width, int height) { if(framebuffer == null) { framebuffer = _wglCreateFramebuffer(); - depthBuffer = _wglCreateRenderbuffer(); + depthBuffer = enableDepth ? _wglCreateRenderbuffer() : null; framebufferColor = GlStateManager.generateTexture(); _wglBindFramebuffer(_GL_FRAMEBUFFER, framebuffer); GlStateManager.bindTexture(framebufferColor); @@ -52,29 +65,35 @@ public class GameOverlayFramebuffer { _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); _wglFramebufferTexture2D(_GL_FRAMEBUFFER, _GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, EaglercraftGPU.getNativeTexture(framebufferColor), 0); - _wglBindRenderbuffer(_GL_RENDERBUFFER, depthBuffer); - _wglFramebufferRenderbuffer(_GL_FRAMEBUFFER, _GL_DEPTH_ATTACHMENT, _GL_RENDERBUFFER, depthBuffer); + if(enableDepth) { + _wglBindRenderbuffer(_GL_RENDERBUFFER, depthBuffer); + _wglFramebufferRenderbuffer(_GL_FRAMEBUFFER, _GL_DEPTH_ATTACHMENT, _GL_RENDERBUFFER, depthBuffer); + } } - if(currentWidth != width || currentHeight != height) { + boolean resized = currentWidth != width || currentHeight != height; + if(resized) { currentWidth = width; currentHeight = height; GlStateManager.bindTexture(framebufferColor); - _wglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (ByteBuffer)null); - _wglBindRenderbuffer(_GL_RENDERBUFFER, depthBuffer); - _wglRenderbufferStorage(_GL_RENDERBUFFER, _GL_DEPTH_COMPONENT16, width, height); + EaglercraftGPU.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (ByteBuffer)null); + if(enableDepth) { + _wglBindRenderbuffer(_GL_RENDERBUFFER, depthBuffer); + _wglRenderbufferStorage(_GL_RENDERBUFFER, _GL_DEPTH_COMPONENT16, width, height); + } } _wglBindFramebuffer(_GL_FRAMEBUFFER, framebuffer); + return resized; } public void endRender() { _wglBindFramebuffer(_GL_FRAMEBUFFER, null); - age = System.currentTimeMillis(); + age = EagRuntime.steadyTimeMillis(); } public long getAge() { - return age == -1l ? -1l : (System.currentTimeMillis() - age); + return age == -1l ? -1l : (EagRuntime.steadyTimeMillis() - age); } public int getTexture() { @@ -84,7 +103,9 @@ public class GameOverlayFramebuffer { public void destroy() { if(framebuffer != null) { _wglDeleteFramebuffer(framebuffer); - _wglDeleteRenderbuffer(depthBuffer); + if(enableDepth) { + _wglDeleteRenderbuffer(depthBuffer); + } GlStateManager.deleteTexture(framebufferColor); framebuffer = null; depthBuffer = null; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GlStateManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GlStateManager.java index 37819b91..537dfb50 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GlStateManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/GlStateManager.java @@ -30,10 +30,12 @@ public class GlStateManager { static final Logger logger = LogManager.getLogger("GlStateManager"); static boolean stateDepthTest = false; + static boolean stateDepthTestStash = false; static int stateDepthFunc = -1; static boolean stateDepthMask = true; static boolean stateCull = false; + static boolean stateCullStash = false; static int stateCullFace = GL_BACK; static boolean statePolygonOffset = false; @@ -58,6 +60,7 @@ public class GlStateManager { static boolean stateEnableShaderBlendColor = false; static boolean stateBlend = false; + static boolean stateBlendStash = false; static boolean stateGlobalBlend = true; static int stateBlendEquation = -1; static int stateBlendSRC = -1; @@ -312,6 +315,30 @@ public class GlStateManager { } } + public static final void eagPushStateForGLES2BlitHack() { + stateDepthTestStash = stateDepthTest; + stateCullStash = stateCull; + stateBlendStash = stateBlend; + } + + public static final void eagPopStateForGLES2BlitHack() { + if(stateDepthTestStash) { + enableDepth(); + }else { + disableDepth(); + } + if(stateCullStash) { + enableCull(); + }else { + disableCull(); + } + if(stateBlendStash) { + enableBlend(); + }else { + disableBlend(); + } + } + public static final void depthFunc(int depthFunc) { int rev = depthFunc; switch(depthFunc) { @@ -603,7 +630,9 @@ public class GlStateManager { f2 = f1; } _wglBindTexture(GL_TEXTURE_2D, null); - _wglBindTexture(GL_TEXTURE_3D, null); + if(EaglercraftGPU.checkOpenGLESVersion() >= 300) { + _wglBindTexture(GL_TEXTURE_3D, null); + } boundTexture[i] = -1; } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ImageData.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ImageData.java index ca5a5b19..ec8cbb21 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ImageData.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ImageData.java @@ -56,6 +56,22 @@ public class ImageData { return new ImageData(pw, ph, img, alpha); } + public static String getMimeFromType(String nameOrPath) { + if(nameOrPath == null) return "image/png"; + nameOrPath = nameOrPath.toLowerCase(); + if(nameOrPath.endsWith(".png")) { + return "image/png"; + }else if(nameOrPath.endsWith(".jpg") || nameOrPath.endsWith(".jpeg")) { + return "image/jpeg"; + }else if(nameOrPath.endsWith(".gif")) { + return "image/gif"; + }else if(nameOrPath.endsWith(".bmp")) { + return "image/bmp"; + }else { + return "image/png"; // rip + } + } + public static final ImageData loadImageFile(String path) { byte[] fileData = EagRuntime.getResourceBytes(path); if(fileData != null) { @@ -73,6 +89,23 @@ public class ImageData { return PlatformAssets.loadImageFile(data); } + public static final ImageData loadImageFile(String path, String mime) { + byte[] fileData = EagRuntime.getResourceBytes(path); + if(fileData != null) { + return loadImageFile(fileData, mime); + }else { + return null; + } + } + + public static final ImageData loadImageFile(InputStream data, String mime) { + return PlatformAssets.loadImageFile(data, mime); + } + + public static final ImageData loadImageFile(byte[] data, String mime) { + return PlatformAssets.loadImageFile(data, mime); + } + public void getRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) { for(int y = 0; y < h; ++y) { @@ -147,5 +180,22 @@ public class ImageData { public static int swapRB(int c) { return (c & 0xFF00FF00) | ((c & 0x00FF0000) >>> 16) | ((c & 0x000000FF) << 16); } + + public static int[] swapRB(int[] arr) { + for(int i = 0; i < arr.length; ++i) { + int j = arr[i]; + arr[i] = (j & 0xFF00FF00) | ((j & 0x00FF0000) >>> 16) | + ((j & 0x000000FF) << 16); + } + return arr; + } + + public boolean isNPOT() { + return (width & (width - 1)) != 0 || (height & (height - 1)) != 0; + } + + public static boolean isNPOTStatic(int w, int h) { + return (w & (w - 1)) != 0 || (h & (h - 1)) != 0; + } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/InstancedFontRenderer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/InstancedFontRenderer.java index 0a36fd0b..6c2839ab 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/InstancedFontRenderer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/InstancedFontRenderer.java @@ -13,13 +13,12 @@ import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.opengl.FixedFunctionShader.FixedFunctionConstants; import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.DeferredStateManager; import net.lax1dude.eaglercraft.v1_8.vector.Matrix4f; import net.lax1dude.eaglercraft.v1_8.vector.Vector4f; /** - * Copyright (c) 2022 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -38,7 +37,10 @@ public class InstancedFontRenderer { private static final Logger logger = LogManager.getLogger("InstancedFontRenderer"); public static final String vertexShaderPath = "/assets/eagler/glsl/accel_font.vsh"; + public static final String vertexShaderPrecision = "precision lowp int;\nprecision highp float;\nprecision mediump sampler2D;\n"; + public static final String fragmentShaderPath = "/assets/eagler/glsl/accel_font.fsh"; + public static final String fragmentShaderPrecision = "precision lowp int;\nprecision highp float;\nprecision mediump sampler2D;\n"; private static final int BYTES_PER_CHARACTER = 10; private static final int CHARACTER_LIMIT = 6553; @@ -78,21 +80,13 @@ public class InstancedFontRenderer { private static float charCoordHeightValue = -1.0f; static void initialize() { - - String vertexSource = EagRuntime.getResourceString(vertexShaderPath); - if(vertexSource == null) { - throw new RuntimeException("InstancedFontRenderer shader \"" + vertexShaderPath + "\" is missing!"); - } - - String fragmentSource = EagRuntime.getResourceString(fragmentShaderPath); - if(fragmentSource == null) { - throw new RuntimeException("InstancedFontRenderer shader \"" + fragmentShaderPath + "\" is missing!"); - } + String vertexSource = EagRuntime.getRequiredResourceString(vertexShaderPath); + String fragmentSource = EagRuntime.getRequiredResourceString(fragmentShaderPath); IShaderGL vert = _wglCreateShader(GL_VERTEX_SHADER); IShaderGL frag = _wglCreateShader(GL_FRAGMENT_SHADER); - _wglShaderSource(vert, FixedFunctionConstants.VERSION + "\n" + vertexSource); + _wglShaderSource(vert, GLSLHeader.getVertexHeaderCompat(vertexSource, vertexShaderPrecision)); _wglCompileShader(vert); if(_wglGetShaderi(vert, GL_COMPILE_STATUS) != GL_TRUE) { @@ -107,7 +101,7 @@ public class InstancedFontRenderer { throw new IllegalStateException("Vertex shader \"" + vertexShaderPath + "\" could not be compiled!"); } - _wglShaderSource(frag, FixedFunctionConstants.VERSION + "\n" + fragmentSource); + _wglShaderSource(frag, GLSLHeader.getFragmentHeaderCompat(fragmentSource, fragmentShaderPrecision)); _wglCompileShader(frag); if(_wglGetShaderi(frag, GL_COMPILE_STATUS) != GL_TRUE) { @@ -127,6 +121,10 @@ public class InstancedFontRenderer { _wglAttachShader(shaderProgram, vert); _wglAttachShader(shaderProgram, frag); + if(EaglercraftGPU.checkOpenGLESVersion() == 200) { + VSHInputLayoutParser.applyLayout(shaderProgram, VSHInputLayoutParser.getShaderInputs(vertexSource)); + } + _wglLinkProgram(shaderProgram); _wglDetachShader(shaderProgram, vert); @@ -161,60 +159,62 @@ public class InstancedFontRenderer { _wglUniform1i(_wglGetUniformLocation(shaderProgram, "u_inputTexture"), 0); - vertexArray = _wglGenVertexArrays(); + vertexArray = EaglercraftGPU.createGLBufferArray(); vertexBuffer = _wglGenBuffers(); instancesBuffer = _wglGenBuffers(); FloatBuffer verts = EagRuntime.allocateFloatBuffer(108); + float paddingA = 0.005f; + float paddingB = 1.0f - paddingA; verts.put(new float[] { // (0 - 6 - 12) regular: - 0.0f, 0.0f, 0.25f, 0.0f, 1.0f, 0.25f, 1.0f, 0.0f, 0.25f, - 1.0f, 0.0f, 0.25f, 0.0f, 1.0f, 0.25f, 1.0f, 1.0f, 0.25f, - 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, + paddingA, paddingA, 0.25f, paddingA, paddingB, 0.25f, paddingB, paddingA, 0.25f, + paddingB, paddingA, 0.25f, paddingA, paddingB, 0.25f, paddingB, paddingB, 0.25f, + paddingA, paddingA, 0.0f, paddingA, paddingB, 0.0f, paddingB, paddingA, 0.0f, + paddingB, paddingA, 0.0f, paddingA, paddingB, 0.0f, paddingB, paddingB, 0.0f, // (12 - 24 - 36) bold shadow: - 0.0f, 0.0f, 0.25f, 0.0f, 1.0f, 0.25f, 1.0f, 0.0f, 0.25f, - 1.0f, 0.0f, 0.25f, 0.0f, 1.0f, 0.25f, 1.0f, 1.0f, 0.25f, - 0.0f, 0.0f, 0.75f, 0.0f, 1.0f, 0.75f, 1.0f, 0.0f, 0.75f, - 1.0f, 0.0f, 0.75f, 0.0f, 1.0f, 0.75f, 1.0f, 1.0f, 0.75f, + paddingA, paddingA, 0.25f, paddingA, paddingB, 0.25f, paddingB, paddingA, 0.25f, + paddingB, paddingA, 0.25f, paddingA, paddingB, 0.25f, paddingB, paddingB, 0.25f, + paddingA, paddingA, 0.75f, paddingA, paddingB, 0.75f, paddingB, paddingA, 0.75f, + paddingB, paddingA, 0.75f, paddingA, paddingB, 0.75f, paddingB, paddingB, 0.75f, - 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.5f, 0.0f, 1.0f, 0.5f, 1.0f, 0.0f, 0.5f, - 1.0f, 0.0f, 0.5f, 0.0f, 1.0f, 0.5f, 1.0f, 1.0f, 0.5f + paddingA, paddingA, 0.0f, paddingA, paddingB, 0.0f, paddingB, paddingA, 0.0f, + paddingB, paddingA, 0.0f, paddingA, paddingB, 0.0f, paddingB, paddingB, 0.0f, + paddingA, paddingA, 0.5f, paddingA, paddingB, 0.5f, paddingB, paddingA, 0.5f, + paddingB, paddingA, 0.5f, paddingA, paddingB, 0.5f, paddingB, paddingB, 0.5f }); verts.flip(); EaglercraftGPU.bindGLBufferArray(vertexArray); - EaglercraftGPU.bindGLArrayBuffer(vertexBuffer); + EaglercraftGPU.bindVAOGLArrayBufferNow(vertexBuffer); _wglBufferData(GL_ARRAY_BUFFER, verts, GL_STATIC_DRAW); EagRuntime.freeFloatBuffer(verts); - _wglEnableVertexAttribArray(0); - _wglVertexAttribPointer(0, 3, GL_FLOAT, false, 12, 0); - _wglVertexAttribDivisor(0, 0); + EaglercraftGPU.enableVertexAttribArray(0); + EaglercraftGPU.vertexAttribPointer(0, 3, GL_FLOAT, false, 12, 0); + EaglercraftGPU.vertexAttribDivisor(0, 0); - EaglercraftGPU.bindGLArrayBuffer(instancesBuffer); + EaglercraftGPU.bindVAOGLArrayBufferNow(instancesBuffer); _wglBufferData(GL_ARRAY_BUFFER, fontDataBuffer.remaining(), GL_STREAM_DRAW); - _wglEnableVertexAttribArray(1); - _wglVertexAttribPointer(1, 2, GL_SHORT, false, 10, 0); - _wglVertexAttribDivisor(1, 1); + EaglercraftGPU.enableVertexAttribArray(1); + EaglercraftGPU.vertexAttribPointer(1, 2, GL_SHORT, false, 10, 0); + EaglercraftGPU.vertexAttribDivisor(1, 1); - _wglEnableVertexAttribArray(2); - _wglVertexAttribPointer(2, 2, GL_UNSIGNED_BYTE, false, 10, 4); - _wglVertexAttribDivisor(2, 1); + EaglercraftGPU.enableVertexAttribArray(2); + EaglercraftGPU.vertexAttribPointer(2, 2, GL_UNSIGNED_BYTE, false, 10, 4); + EaglercraftGPU.vertexAttribDivisor(2, 1); - _wglEnableVertexAttribArray(3); - _wglVertexAttribPointer(3, 4, GL_UNSIGNED_BYTE, true, 10, 6); - _wglVertexAttribDivisor(3, 1); + EaglercraftGPU.enableVertexAttribArray(3); + EaglercraftGPU.vertexAttribPointer(3, 4, GL_UNSIGNED_BYTE, true, 10, 6); + EaglercraftGPU.vertexAttribDivisor(3, 1); } @@ -381,7 +381,7 @@ public class InstancedFontRenderer { fontDataBuffer.position(p); fontDataBuffer.limit(l); - _wglDrawArraysInstanced(GL_TRIANGLES, shadow ? 0 : 6, shadow ? 12 : 6, charactersDrawn); + EaglercraftGPU.doDrawArraysInstanced(GL_TRIANGLES, shadow ? 0 : 6, shadow ? 12 : 6, charactersDrawn); } if(boldCharactersDrawn > 0) { @@ -394,7 +394,7 @@ public class InstancedFontRenderer { fontBoldDataBuffer.position(p); fontBoldDataBuffer.limit(l); - _wglDrawArraysInstanced(GL_TRIANGLES, shadow ? 12 : 24, shadow ? 24 : 12, boldCharactersDrawn); + EaglercraftGPU.doDrawArraysInstanced(GL_TRIANGLES, shadow ? 12 : 24, shadow ? 24 : 12, boldCharactersDrawn); } } @@ -455,4 +455,40 @@ public class InstancedFontRenderer { if(y > heightCalcMost || heightCalcMost == Integer.MAX_VALUE) heightCalcMost = y; } + public static void destroy() { + if(fontDataBuffer != null) { + EagRuntime.freeByteBuffer(fontDataBuffer); + fontDataBuffer = null; + } + if(fontBoldDataBuffer != null) { + EagRuntime.freeByteBuffer(fontBoldDataBuffer); + fontBoldDataBuffer = null; + } + if(shaderProgram != null) { + _wglDeleteProgram(shaderProgram); + shaderProgram = null; + } + if(matrixCopyBuffer != null) { + EagRuntime.freeFloatBuffer(matrixCopyBuffer); + matrixCopyBuffer = null; + } + u_matrixTransform = null; + u_charSize2f = null; + u_charCoordSize2f = null; + u_color4f = null; + u_colorBias4f = null; + if(vertexArray != null) { + EaglercraftGPU.destroyGLBufferArray(vertexArray); + vertexArray = null; + } + if(vertexBuffer != null) { + _wglDeleteBuffers(vertexBuffer); + vertexBuffer = null; + } + if(instancesBuffer != null) { + _wglDeleteBuffers(instancesBuffer); + instancesBuffer = null; + } + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/InstancedParticleRenderer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/InstancedParticleRenderer.java index 32b4b8cd..3ac10ab7 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/InstancedParticleRenderer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/InstancedParticleRenderer.java @@ -13,11 +13,10 @@ import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.opengl.FixedFunctionShader.FixedFunctionConstants; import net.lax1dude.eaglercraft.v1_8.vector.Matrix4f; /** - * Copyright (c) 2022 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -36,7 +35,10 @@ public class InstancedParticleRenderer { private static final Logger logger = LogManager.getLogger("InstancedParticleRenderer"); public static final String vertexShaderPath = "/assets/eagler/glsl/accel_particle.vsh"; + public static final String vertexShaderPrecision = "precision lowp int;\nprecision highp float;\nprecision mediump sampler2D;\n"; + public static final String fragmentShaderPath = "/assets/eagler/glsl/accel_particle.fsh"; + public static final String fragmentShaderPrecision = "precision lowp int;\nprecision mediump float;\nprecision mediump sampler2D;\n"; private static ByteBuffer particleBuffer = null; private static int particleCount = 0; @@ -79,20 +81,13 @@ public class InstancedParticleRenderer { private static float stateTransformParam5 = -999.0f; static void initialize() { - String vertexSource = EagRuntime.getResourceString(vertexShaderPath); - if(vertexSource == null) { - throw new RuntimeException("InstancedParticleRenderer shader \"" + vertexShaderPath + "\" is missing!"); - } - - String fragmentSource = EagRuntime.getResourceString(fragmentShaderPath); - if(fragmentSource == null) { - throw new RuntimeException("InstancedParticleRenderer shader \"" + fragmentShaderPath + "\" is missing!"); - } + String vertexSource = EagRuntime.getRequiredResourceString(vertexShaderPath); + String fragmentSource = EagRuntime.getRequiredResourceString(fragmentShaderPath); IShaderGL vert = _wglCreateShader(GL_VERTEX_SHADER); IShaderGL frag = _wglCreateShader(GL_FRAGMENT_SHADER); - _wglShaderSource(vert, FixedFunctionConstants.VERSION + "\n" + vertexSource); + _wglShaderSource(vert, GLSLHeader.getVertexHeaderCompat(vertexSource, vertexShaderPrecision)); _wglCompileShader(vert); if(_wglGetShaderi(vert, GL_COMPILE_STATUS) != GL_TRUE) { @@ -107,7 +102,7 @@ public class InstancedParticleRenderer { throw new IllegalStateException("Vertex shader \"" + vertexShaderPath + "\" could not be compiled!"); } - _wglShaderSource(frag, FixedFunctionConstants.VERSION + "\n" + fragmentSource); + _wglShaderSource(frag, GLSLHeader.getFragmentHeaderCompat(fragmentSource, fragmentShaderPrecision)); _wglCompileShader(frag); if(_wglGetShaderi(frag, GL_COMPILE_STATUS) != GL_TRUE) { @@ -127,6 +122,10 @@ public class InstancedParticleRenderer { _wglAttachShader(shaderProgram, vert); _wglAttachShader(shaderProgram, frag); + if(EaglercraftGPU.checkOpenGLESVersion() == 200) { + VSHInputLayoutParser.applyLayout(shaderProgram, VSHInputLayoutParser.getShaderInputs(vertexSource)); + } + _wglLinkProgram(shaderProgram); _wglDetachShader(shaderProgram, vert); @@ -161,7 +160,7 @@ public class InstancedParticleRenderer { _wglUniform1i(_wglGetUniformLocation(shaderProgram, "u_inputTexture"), 0); _wglUniform1i(_wglGetUniformLocation(shaderProgram, "u_lightmapTexture"), 1); - vertexArray = _wglGenVertexArrays(); + vertexArray = EaglercraftGPU.createGLBufferArray(); vertexBuffer = _wglGenBuffers(); instancesBuffer = _wglGenBuffers(); @@ -174,37 +173,37 @@ public class InstancedParticleRenderer { EaglercraftGPU.bindGLBufferArray(vertexArray); - EaglercraftGPU.bindGLArrayBuffer(vertexBuffer); + EaglercraftGPU.bindVAOGLArrayBufferNow(vertexBuffer); _wglBufferData(GL_ARRAY_BUFFER, verts, GL_STATIC_DRAW); EagRuntime.freeFloatBuffer(verts); - _wglEnableVertexAttribArray(0); - _wglVertexAttribPointer(0, 2, GL_FLOAT, false, 8, 0); - _wglVertexAttribDivisor(0, 0); + EaglercraftGPU.enableVertexAttribArray(0); + EaglercraftGPU.vertexAttribPointer(0, 2, GL_FLOAT, false, 8, 0); + EaglercraftGPU.vertexAttribDivisor(0, 0); - EaglercraftGPU.bindGLArrayBuffer(instancesBuffer); + EaglercraftGPU.bindVAOGLArrayBufferNow(instancesBuffer); _wglBufferData(GL_ARRAY_BUFFER, particleBuffer.remaining(), GL_STREAM_DRAW); - _wglEnableVertexAttribArray(1); - _wglVertexAttribPointer(1, 3, GL_FLOAT, false, 24, 0); - _wglVertexAttribDivisor(1, 1); + EaglercraftGPU.enableVertexAttribArray(1); + EaglercraftGPU.vertexAttribPointer(1, 3, GL_FLOAT, false, 24, 0); + EaglercraftGPU.vertexAttribDivisor(1, 1); - _wglEnableVertexAttribArray(2); - _wglVertexAttribPointer(2, 2, GL_UNSIGNED_SHORT, false, 24, 12); - _wglVertexAttribDivisor(2, 1); + EaglercraftGPU.enableVertexAttribArray(2); + EaglercraftGPU.vertexAttribPointer(2, 2, GL_UNSIGNED_SHORT, false, 24, 12); + EaglercraftGPU.vertexAttribDivisor(2, 1); - _wglEnableVertexAttribArray(3); - _wglVertexAttribPointer(3, 2, GL_UNSIGNED_BYTE, true, 24, 16); - _wglVertexAttribDivisor(3, 1); + EaglercraftGPU.enableVertexAttribArray(3); + EaglercraftGPU.vertexAttribPointer(3, 2, GL_UNSIGNED_BYTE, true, 24, 16); + EaglercraftGPU.vertexAttribDivisor(3, 1); - _wglEnableVertexAttribArray(4); - _wglVertexAttribPointer(4, 2, GL_UNSIGNED_BYTE, false, 24, 18); - _wglVertexAttribDivisor(4, 1); + EaglercraftGPU.enableVertexAttribArray(4); + EaglercraftGPU.vertexAttribPointer(4, 2, GL_UNSIGNED_BYTE, false, 24, 18); + EaglercraftGPU.vertexAttribDivisor(4, 1); - _wglEnableVertexAttribArray(5); - _wglVertexAttribPointer(5, 4, GL_UNSIGNED_BYTE, true, 24, 20); - _wglVertexAttribDivisor(5, 1); + EaglercraftGPU.enableVertexAttribArray(5); + EaglercraftGPU.vertexAttribPointer(5, 4, GL_UNSIGNED_BYTE, true, 24, 20); + EaglercraftGPU.vertexAttribDivisor(5, 1); } @@ -316,11 +315,43 @@ public class InstancedParticleRenderer { particleBuffer.position(p); particleBuffer.limit(l); - _wglDrawArraysInstanced(GL_TRIANGLES, 0, 6, particleCount); + EaglercraftGPU.doDrawArraysInstanced(GL_TRIANGLES, 0, 6, particleCount); } public static void stupidColorSetHack(IUniformGL color4f) { _wglUniform4f(color4f, GlStateManager.stateColorR, GlStateManager.stateColorG, GlStateManager.stateColorB, GlStateManager.stateColorA); } + public static void destroy() { + if(particleBuffer != null) { + EagRuntime.freeByteBuffer(particleBuffer); + particleBuffer = null; + } + if(shaderProgram != null) { + _wglDeleteProgram(shaderProgram); + shaderProgram = null; + } + if(matrixCopyBuffer != null) { + EagRuntime.freeFloatBuffer(matrixCopyBuffer); + matrixCopyBuffer = null; + } + u_matrixTransform = null; + u_texCoordSize2f_particleSize1f = null; + u_transformParam_1_2_5_f = null; + u_transformParam_3_4_f = null; + u_color4f = null; + if(vertexArray != null) { + EaglercraftGPU.destroyGLBufferArray(vertexArray); + vertexArray = null; + } + if(vertexBuffer != null) { + _wglDeleteBuffers(vertexBuffer); + vertexBuffer = null; + } + if(instancesBuffer != null) { + _wglDeleteBuffers(instancesBuffer); + instancesBuffer = null; + } + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/RealOpenGLEnums.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/RealOpenGLEnums.java index f3064644..a37d0b56 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/RealOpenGLEnums.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/RealOpenGLEnums.java @@ -1,7 +1,7 @@ package net.lax1dude.eaglercraft.v1_8.opengl; /** - * Copyright (c) 2022-2023 lax1dude, ayunami2000. All Rights Reserved. + * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SoftGLBufferArray.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SoftGLBufferArray.java new file mode 100644 index 00000000..1a111a80 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SoftGLBufferArray.java @@ -0,0 +1,225 @@ +package net.lax1dude.eaglercraft.v1_8.opengl; + +import net.lax1dude.eaglercraft.v1_8.internal.IBufferArrayGL; +import net.lax1dude.eaglercraft.v1_8.internal.IBufferGL; + +import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL.*; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +class SoftGLBufferArray implements IBufferArrayGL { + + Attrib[] attribs = new Attrib[4]; + int[] attribDivisors = null; + int hasAttribDivisorMask = 0; + int enabled = 0; + int enabledCnt = -1; + IBufferGL indexBuffer = null; + + SoftGLBufferArray() { + } + + void setAttrib(IBufferGL buffer, int index, int size, int format, boolean normalized, int stride, int offset) { + if(index >= attribs.length) { + int newLen = attribs.length << 1; + while(newLen <= index) { + newLen <<= 1; + } + Attrib[] newAttrib = new Attrib[newLen]; + System.arraycopy(attribs, 0, newAttrib, 0, attribs.length); + attribs = newAttrib; + } + attribs[index] = new Attrib(buffer, size, format, normalized, stride, offset); + } + + void setAttribDivisor(int index, int divisor) { + if(attribDivisors == null) { + if(divisor != 0) { + int newLen = 8; + while(newLen <= index) { + newLen <<= 1; + } + attribDivisors = new int[newLen]; + } + }else if(index >= attribDivisors.length) { + int newLen = attribDivisors.length << 1; + while(newLen <= index) { + newLen <<= 1; + } + int[] newDivisor = new int[newLen]; + System.arraycopy(attribDivisors, 0, newDivisor, 0, attribDivisors.length); + attribDivisors = newDivisor; + } + if(attribDivisors != null) { + attribDivisors[index] = divisor; + if(divisor != 0) { + hasAttribDivisorMask |= (1 << index); + }else { + hasAttribDivisorMask &= ~(1 << index); + } + } + } + + void enableAttrib(int index, boolean en) { + if(en) { + enabled |= (1 << index); + }else { + enabled &= ~(1 << index); + } + enabledCnt = 32 - Integer.numberOfLeadingZeros(enabled); + } + + void setIndexBuffer(IBufferGL buffer) { + indexBuffer = buffer; + } + + void transitionToState(SoftGLBufferState previousState, boolean elements) { + int oldEnabled = previousState.oldEnabled; + int oldEnabledCnt = previousState.oldEnabledCnt; + int[] oldAttribDivisors = previousState.attribDivisors; + int oldHasAttribDivisorMask = previousState.hasAttribDivisorMask; + Attrib[] oldAttrs = previousState.attribs; + boolean instancingCapable = EaglercraftGPU.checkInstancingCapable(); + int enCnt = enabledCnt; + int en = enabled; + Attrib[] attrs = attribs; + int[] divs = attribDivisors; + int hasDivs = hasAttribDivisorMask; + if(oldEnabledCnt >= 0) { + int enMax = Math.max(enCnt, oldEnabledCnt); + for(int i = 0, ii; i < enMax; ++i) { + ii = (1 << i); + boolean old = i < oldEnabledCnt && (oldEnabled & ii) != 0; + boolean _new = i < enCnt && (en & ii) != 0; + if(_new) { + if(!old) { + _wglEnableVertexAttribArray(i); + } + Attrib attr = i < attrs.length ? attrs[i] : null; + if(attr != null) { + Attrib oldAttr = oldAttrs[i]; + if(oldAttr == null || !oldAttr.equalsExplicit(attr)) { + EaglercraftGPU.bindGLArrayBuffer(attr.buffer); + _wglVertexAttribPointer(i, attr.size, attr.format, attr.normalized, attr.stride, attr.offset); + oldAttrs[i] = attr; + } + }else { + oldAttrs[i] = null; + } + if(instancingCapable) { + // instancing is uncommon + if((hasDivs & ii) != 0) { + int newDivisor = divs[i]; + if((oldHasAttribDivisorMask & ii) == 0 || newDivisor != oldAttribDivisors[i]) { + _wglVertexAttribDivisor(i, newDivisor); + oldAttribDivisors[i] = newDivisor; + previousState.hasAttribDivisorMask |= ii; + } + }else { + if((oldHasAttribDivisorMask & ii) != 0) { + _wglVertexAttribDivisor(i, 0); + oldAttribDivisors[i] = 0; + previousState.hasAttribDivisorMask &= ~ii; + } + } + } + }else if(old) { + _wglDisableVertexAttribArray(i); + } + } + }else { + // Bootstrap code for the emulator's first draw + for(int i = 0; i < enCnt; ++i) { + int ii = (1 << i); + if((en & ii) != 0) { + _wglEnableVertexAttribArray(i); + Attrib attr = attrs[i]; + if(attr != null) { + EaglercraftGPU.bindGLArrayBuffer(attr.buffer); + _wglVertexAttribPointer(i, attr.size, attr.format, attr.normalized, attr.stride, attr.offset); + oldAttrs[i] = attr; + }else { + oldAttrs[i] = null; + } + if(instancingCapable) { + if((hasDivs & ii) != 0) { + int newDivisor = divs[i]; + _wglVertexAttribDivisor(i, newDivisor); + oldAttribDivisors[i] = newDivisor; + previousState.hasAttribDivisorMask |= ii; + }else { + _wglVertexAttribDivisor(i, 0); + oldAttribDivisors[i] = 0; + } + } + } + } + } + if(elements) { + IBufferGL indexBufferL = indexBuffer; + if(indexBufferL != null) { + EaglercraftGPU.bindEmulatedVAOIndexBuffer(indexBufferL); + } + } + previousState.oldEnabled = en & ((1 << enCnt) - 1); + previousState.oldEnabledCnt = enCnt; + } + + @Override + public void free() { + } + + static class Attrib { + + final IBufferGL buffer; + final int size; + final int format; + final boolean normalized; + final int stride; + final int offset; + final int hash; + final int checkVal; + + Attrib(IBufferGL buffer, int size, int format, boolean normalized, int stride, int offset) { + this.buffer = buffer; + this.size = size; + this.format = format; + this.normalized = normalized; + this.stride = stride; + this.offset = offset; + this.checkVal = ((size - 1) & 3) | (normalized ? 4 : 0) | (format << 4); + this.hash = (((((31 + buffer.hashCode()) * 31 + size) * 31 + format) * 31 + (normalized ? 1 : 0)) * 31 + + stride) * 31 + offset; + } + + public int hashCode() { + return hash; + } + + public boolean equals(Object obj) { + if(obj == this) return true; + if(!(obj instanceof Attrib)) return false; + Attrib o2 = (Attrib)obj; + return o2.hash == hash && o2.buffer == buffer && o2.checkVal == checkVal && o2.stride == stride && o2.offset == offset; + } + + public boolean equalsExplicit(Attrib o2) { + return o2 == this || (o2.hash == hash && o2.buffer == buffer && o2.checkVal == checkVal && o2.stride == stride && o2.offset == offset); + } + + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SoftGLBufferState.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SoftGLBufferState.java new file mode 100644 index 00000000..611f4352 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SoftGLBufferState.java @@ -0,0 +1,31 @@ +package net.lax1dude.eaglercraft.v1_8.opengl; + +import net.lax1dude.eaglercraft.v1_8.opengl.SoftGLBufferArray.Attrib; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +class SoftGLBufferState { + + final Attrib[] attribs = new Attrib[24]; + int[] attribDivisors = new int[24]; + int hasAttribDivisorMask = 0; + int oldEnabled = 0; + int oldEnabledCnt = -1; + + SoftGLBufferState() { + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SpriteLevelMixer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SpriteLevelMixer.java index 439b314f..e01dbd39 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SpriteLevelMixer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/SpriteLevelMixer.java @@ -10,7 +10,6 @@ import net.lax1dude.eaglercraft.v1_8.internal.IUniformGL; import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.opengl.FixedFunctionShader.FixedFunctionConstants; import net.lax1dude.eaglercraft.v1_8.vector.Matrix3f; /** @@ -33,6 +32,7 @@ public class SpriteLevelMixer { private static final Logger LOGGER = LogManager.getLogger("SpriteLevelMixer"); public static final String fragmentShaderPath = "/assets/eagler/glsl/texture_mix.fsh"; + public static final String fragmentShaderPrecision = "precision lowp int;\nprecision highp float;\nprecision highp sampler2D;\n"; private static IProgramGL shaderProgram = null; @@ -61,15 +61,11 @@ public class SpriteLevelMixer { private static final Matrix3f identityMatrix = new Matrix3f(); static void initialize() { - - String fragmentSource = EagRuntime.getResourceString(fragmentShaderPath); - if(fragmentSource == null) { - throw new RuntimeException("SpriteLevelMixer shader \"" + fragmentShaderPath + "\" is missing!"); - } + String fragmentSource = EagRuntime.getRequiredResourceString(fragmentShaderPath); IShaderGL frag = _wglCreateShader(GL_FRAGMENT_SHADER); - _wglShaderSource(frag, FixedFunctionConstants.VERSION + "\n" + fragmentSource); + _wglShaderSource(frag, GLSLHeader.getFragmentHeaderCompat(fragmentSource, fragmentShaderPrecision)); _wglCompileShader(frag); if(_wglGetShaderi(frag, GL_COMPILE_STATUS) != GL_TRUE) { @@ -89,6 +85,10 @@ public class SpriteLevelMixer { _wglAttachShader(shaderProgram, DrawUtils.vshLocal); _wglAttachShader(shaderProgram, frag); + if(EaglercraftGPU.checkOpenGLESVersion() == 200) { + VSHInputLayoutParser.applyLayout(shaderProgram, DrawUtils.vshLocalLayout); + } + _wglLinkProgram(shaderProgram); _wglDetachShader(shaderProgram, DrawUtils.vshLocal); @@ -155,7 +155,14 @@ public class SpriteLevelMixer { public static void drawSprite(float level) { EaglercraftGPU.bindGLShaderProgram(shaderProgram); - _wglUniform1f(u_textureLod1f, level); + if(EaglercraftGPU.checkTextureLODCapable()) { + _wglUniform1f(u_textureLod1f, level); + }else { + if(level != 0.0f) { + LOGGER.error("Tried to copy from mipmap level {}, but this GPU does not support textureLod!", level); + } + _wglUniform1f(u_textureLod1f, 0.0f); + } if(blendColorChanged) { _wglUniform4f(u_blendFactor4f, blendColorR, blendColorG, blendColorB, blendColorA); @@ -178,4 +185,19 @@ public class SpriteLevelMixer { DrawUtils.drawStandardQuad2D(); } + public static void destroy() { + if(matrixCopyBuffer != null) { + EagRuntime.freeFloatBuffer(matrixCopyBuffer); + matrixCopyBuffer = null; + } + if(shaderProgram != null) { + _wglDeleteProgram(shaderProgram); + shaderProgram = null; + } + u_textureLod1f = null; + u_blendFactor4f = null; + u_blendBias4f = null; + u_matrixTransform = null; + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/StreamBuffer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/StreamBuffer.java index d6a44616..252830eb 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/StreamBuffer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/StreamBuffer.java @@ -74,7 +74,7 @@ public class StreamBuffer { next.vertexBuffer = _wglGenBuffers(); } if(next.vertexArray == null) { - next.vertexArray = _wglGenVertexArrays(); + next.vertexArray = EaglercraftGPU.createGLBufferArray(); initializer.initialize(next.vertexArray, next.vertexBuffer); } if(next.vertexBufferSize < requiredMemory) { @@ -100,7 +100,7 @@ public class StreamBuffer { newArray[i] = buffers[i]; }else { if(buffers[i].vertexArray != null) { - _wglDeleteVertexArrays(buffers[i].vertexArray); + EaglercraftGPU.destroyGLBufferArray(buffers[i].vertexArray); } if(buffers[i].vertexBuffer != null) { _wglDeleteBuffers(buffers[i].vertexBuffer); @@ -135,7 +135,7 @@ public class StreamBuffer { for(int i = 0; i < buffers.length; ++i) { StreamBufferInstance next = buffers[i]; if(next.vertexArray != null) { - _wglDeleteVertexArrays(next.vertexArray); + EaglercraftGPU.destroyGLBufferArray(next.vertexArray); } if(next.vertexBuffer != null) { _wglDeleteBuffers(next.vertexBuffer); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/TextureCopyUtil.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/TextureCopyUtil.java index 32fb3cec..b5f6f9f4 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/TextureCopyUtil.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/TextureCopyUtil.java @@ -3,16 +3,17 @@ package net.lax1dude.eaglercraft.v1_8.opengl; import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL.*; import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; +import java.util.List; + import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.internal.IProgramGL; import net.lax1dude.eaglercraft.v1_8.internal.IShaderGL; import net.lax1dude.eaglercraft.v1_8.internal.IUniformGL; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.opengl.FixedFunctionShader.FixedFunctionConstants; /** - * Copyright (c) 2023 lax1dude. All Rights Reserved. + * Copyright (c) 2023-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -31,9 +32,13 @@ public class TextureCopyUtil { private static final Logger LOGGER = LogManager.getLogger("TextureCopyUtil"); public static final String vertexShaderPath = "/assets/eagler/glsl/texture_blit.vsh"; + public static final String vertexShaderPrecision = "precision lowp int;\nprecision highp float;\nprecision highp sampler2D;\n"; + public static final String fragmentShaderPath = "/assets/eagler/glsl/texture_blit.fsh"; + public static final String fragmentShaderPrecision = "precision lowp int;\nprecision highp float;\nprecision highp sampler2D;\n"; private static String vshSource = null; + private static List vshSourceLayout = null; private static String fshSource = null; private static IShaderGL vshShader = null; @@ -54,6 +59,17 @@ public class TextureCopyUtil { this.u_pixelAlignmentSizes4f = _wglGetUniformLocation(shaderProgram, "u_pixelAlignmentSizes4f"); this.u_pixelAlignmentOffset2f = _wglGetUniformLocation(shaderProgram, "u_pixelAlignmentOffset2f"); } + private void destroy() { + if(shaderProgram != null) { + _wglDeleteProgram(shaderProgram); + shaderProgram = null; + } + u_srcCoords4f = null; + u_dstCoords4f = null; + u_textureLod1f = null; + u_pixelAlignmentSizes4f = null; + u_pixelAlignmentOffset2f = null; + } } private static TextureCopyShader textureBlit = null; @@ -74,19 +90,12 @@ public class TextureCopyUtil { private static float alignOffsetY = 0.0f; static void initialize() { - vshSource = EagRuntime.getResourceString(vertexShaderPath); - if(vshSource == null) { - throw new RuntimeException("TextureCopyUtil shader \"" + vertexShaderPath + "\" is missing!"); - } - - fshSource = EagRuntime.getResourceString(fragmentShaderPath); - if(fshSource == null) { - throw new RuntimeException("TextureCopyUtil shader \"" + fragmentShaderPath + "\" is missing!"); - } + vshSource = EagRuntime.getRequiredResourceString(vertexShaderPath); + fshSource = EagRuntime.getRequiredResourceString(fragmentShaderPath); vshShader = _wglCreateShader(GL_VERTEX_SHADER); - _wglShaderSource(vshShader, FixedFunctionConstants.VERSION + "\n" + vshSource); + _wglShaderSource(vshShader, GLSLHeader.getVertexHeaderCompat(vshSource, vertexShaderPrecision)); _wglCompileShader(vshShader); if(_wglGetShaderi(vshShader, GL_COMPILE_STATUS) != GL_TRUE) { @@ -100,14 +109,17 @@ public class TextureCopyUtil { } throw new IllegalStateException("Vertex shader \"" + vertexShaderPath + "\" could not be compiled!"); } + + if(EaglercraftGPU.checkOpenGLESVersion() == 200) { + vshSourceLayout = VSHInputLayoutParser.getShaderInputs(vshSource); + } } private static TextureCopyShader compileShader(boolean align, boolean depth) { IShaderGL frag = _wglCreateShader(GL_FRAGMENT_SHADER); - _wglShaderSource(frag, - FixedFunctionConstants.VERSION + "\n" + (align ? "#define COMPILE_PIXEL_ALIGNMENT\n" : "") - + (depth ? "#define COMPILE_BLIT_DEPTH\n" : "") + fshSource); + _wglShaderSource(frag, GLSLHeader.getFragmentHeaderCompat(fshSource, fragmentShaderPrecision + + (align ? "#define COMPILE_PIXEL_ALIGNMENT\n" : "") + (depth ? "#define COMPILE_BLIT_DEPTH\n" : ""))); _wglCompileShader(frag); if(_wglGetShaderi(frag, GL_COMPILE_STATUS) != GL_TRUE) { @@ -127,6 +139,10 @@ public class TextureCopyUtil { _wglAttachShader(shaderProgram, vshShader); _wglAttachShader(shaderProgram, frag); + if(EaglercraftGPU.checkOpenGLESVersion() == 200) { + VSHInputLayoutParser.applyLayout(shaderProgram, vshSourceLayout); + } + _wglLinkProgram(shaderProgram); _wglDetachShader(shaderProgram, vshShader); @@ -226,7 +242,14 @@ public class TextureCopyUtil { _wglUniform4f(shaderObj.u_srcCoords4f, (float)srcX / srcViewW, (float)srcY / srcViewH, (float)srcW / srcViewW, (float)srcH / srcViewH); _wglUniform4f(shaderObj.u_dstCoords4f, (float) dstX / dstViewW - 1.0f, (float) dstY / dstViewH - 1.0f, (float) dstW / dstViewW, (float) dstH / dstViewH); - _wglUniform1f(shaderObj.u_textureLod1f, lvl); + if(EaglercraftGPU.checkTextureLODCapable()) { + _wglUniform1f(shaderObj.u_textureLod1f, lvl); + }else { + if(lvl != 0.0f) { + LOGGER.error("Tried to copy from mipmap level {}, but this GPU does not support textureLod!", lvl); + } + _wglUniform1f(shaderObj.u_textureLod1f, 0.0f); + } if(isAligned) { _wglUniform4f(shaderObj.u_pixelAlignmentSizes4f, alignW, alignH, 1.0f / alignW, 1.0f / alignH); _wglUniform2f(shaderObj.u_pixelAlignmentOffset2f, alignOffsetX, alignOffsetY); @@ -244,7 +267,14 @@ public class TextureCopyUtil { EaglercraftGPU.bindGLShaderProgram(shaderObj.shaderProgram); _wglUniform4f(shaderObj.u_srcCoords4f, 0.0f, 0.0f, 1.0f, 1.0f); _wglUniform4f(shaderObj.u_dstCoords4f, -1.0f, -1.0f, 2.0f, 2.0f); - _wglUniform1f(shaderObj.u_textureLod1f, lvl); + if(EaglercraftGPU.checkTextureLODCapable()) { + _wglUniform1f(shaderObj.u_textureLod1f, lvl); + }else { + if(lvl != 0.0f) { + LOGGER.error("Tried to copy from mipmap level {}, but this GPU does not support textureLod!", lvl); + } + _wglUniform1f(shaderObj.u_textureLod1f, 0.0f); + } if(isAligned) { _wglUniform4f(shaderObj.u_pixelAlignmentSizes4f, alignW, alignH, 1.0f / alignW, 1.0f / alignH); _wglUniform2f(shaderObj.u_pixelAlignmentOffset2f, alignOffsetX, alignOffsetY); @@ -271,7 +301,14 @@ public class TextureCopyUtil { GlStateManager.viewport(dstX, dstY, dstW, dstH); _wglUniform4f(shaderObj.u_srcCoords4f, (float)srcX / srcViewW, (float)srcY / srcViewH, (float)srcW / srcViewW, (float)srcH / srcViewH); _wglUniform4f(shaderObj.u_dstCoords4f, -1.0f, -1.0f, 2.0f, 2.0f); - _wglUniform1f(shaderObj.u_textureLod1f, lvl); + if(EaglercraftGPU.checkTextureLODCapable()) { + _wglUniform1f(shaderObj.u_textureLod1f, lvl); + }else { + if(lvl != 0.0f) { + LOGGER.error("Tried to copy from mipmap level {}, but this GPU does not support textureLod!", lvl); + } + _wglUniform1f(shaderObj.u_textureLod1f, 0.0f); + } if(isAligned) { _wglUniform4f(shaderObj.u_pixelAlignmentSizes4f, alignW, alignH, 1.0f / alignW, 1.0f / alignH); _wglUniform2f(shaderObj.u_pixelAlignmentOffset2f, alignOffsetX, alignOffsetY); @@ -298,7 +335,14 @@ public class TextureCopyUtil { _wglUniform4f(shaderObj.u_srcCoords4f, (float)srcX / srcViewW, (float)srcY / srcViewH, (float)srcW / srcViewW, (float)srcH / srcViewH); _wglUniform4f(shaderObj.u_dstCoords4f, (float) dstX / dstViewW - 1.0f, (float) dstY / dstViewH - 1.0f, (float) dstW / dstViewW, (float) dstH / dstViewH); - _wglUniform1f(shaderObj.u_textureLod1f, lvl); + if(EaglercraftGPU.checkTextureLODCapable()) { + _wglUniform1f(shaderObj.u_textureLod1f, lvl); + }else { + if(lvl != 0.0f) { + LOGGER.error("Tried to copy from mipmap level {}, but this GPU does not support textureLod!", lvl); + } + _wglUniform1f(shaderObj.u_textureLod1f, 0.0f); + } if(isAligned) { _wglUniform4f(shaderObj.u_pixelAlignmentSizes4f, alignW, alignH, 1.0f / alignW, 1.0f / alignH); _wglUniform2f(shaderObj.u_pixelAlignmentOffset2f, alignOffsetX, alignOffsetY); @@ -316,7 +360,14 @@ public class TextureCopyUtil { EaglercraftGPU.bindGLShaderProgram(shaderObj.shaderProgram); _wglUniform4f(shaderObj.u_srcCoords4f, 0.0f, 0.0f, 1.0f, 1.0f); _wglUniform4f(shaderObj.u_dstCoords4f, -1.0f, -1.0f, 2.0f, 2.0f); - _wglUniform1f(shaderObj.u_textureLod1f, lvl); + if(EaglercraftGPU.checkTextureLODCapable()) { + _wglUniform1f(shaderObj.u_textureLod1f, lvl); + }else { + if(lvl != 0.0f) { + LOGGER.error("Tried to copy from mipmap level {}, but this GPU does not support textureLod!", lvl); + } + _wglUniform1f(shaderObj.u_textureLod1f, 0.0f); + } if(isAligned) { _wglUniform4f(shaderObj.u_pixelAlignmentSizes4f, alignW, alignH, 1.0f / alignW, 1.0f / alignH); _wglUniform2f(shaderObj.u_pixelAlignmentOffset2f, alignOffsetX, alignOffsetY); @@ -343,7 +394,14 @@ public class TextureCopyUtil { GlStateManager.viewport(dstX, dstY, dstW, dstH); _wglUniform4f(shaderObj.u_srcCoords4f, (float)srcX / srcViewW, (float)srcY / srcViewH, (float)srcW / srcViewW, (float)srcH / srcViewH); _wglUniform4f(shaderObj.u_dstCoords4f, -1.0f, -1.0f, 2.0f, 2.0f); - _wglUniform1f(shaderObj.u_textureLod1f, lvl); + if(EaglercraftGPU.checkTextureLODCapable()) { + _wglUniform1f(shaderObj.u_textureLod1f, lvl); + }else { + if(lvl != 0.0f) { + LOGGER.error("Tried to copy from mipmap level {}, but this GPU does not support textureLod!", lvl); + } + _wglUniform1f(shaderObj.u_textureLod1f, 0.0f); + } if(isAligned) { _wglUniform4f(shaderObj.u_pixelAlignmentSizes4f, alignW, alignH, 1.0f / alignW, 1.0f / alignH); _wglUniform2f(shaderObj.u_pixelAlignmentOffset2f, alignOffsetX, alignOffsetY); @@ -351,4 +409,27 @@ public class TextureCopyUtil { } DrawUtils.drawStandardQuad2D(); } + + public static void destroy() { + if(vshShader != null) { + _wglDeleteShader(vshShader); + vshShader = null; + } + if(textureBlit != null) { + textureBlit.destroy(); + textureBlit = null; + } + if(textureBlitAligned != null) { + textureBlitAligned.destroy(); + textureBlitAligned = null; + } + if(textureBlitDepth != null) { + textureBlitDepth.destroy(); + textureBlitDepth = null; + } + if(textureBlitDepthAligned != null) { + textureBlitDepthAligned.destroy(); + textureBlitDepthAligned = null; + } + } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/TextureFormatHelper.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/TextureFormatHelper.java new file mode 100644 index 00000000..6049022f --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/TextureFormatHelper.java @@ -0,0 +1,81 @@ +package net.lax1dude.eaglercraft.v1_8.opengl; + +import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; +import static net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.ExtGLEnums.*; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class TextureFormatHelper { + + public static int getFormatFromInternal(int internalFormat) { + switch(internalFormat) { + case _GL_R8: + case 0x822D: // GL_R16F + case 0x822E: // GL_R32F + return GL_RED; + case _GL_RG8: + case 0x822F: // GL_RG16F + case 0x8230: // GL_RG32F + return _GL_RG; + case GL_RGB8: + case _GL_RGB16F: + case 0x8815: // GL_RGB32F + return GL_RGB; + case GL_RGBA8: + case 0x881A: // GL_RGBA16F + case 0x8814: // GL_RGBA32F + return GL_RGBA; + default: + throw new UnsupportedOperationException(); + } + } + + public static int getTypeFromInternal(int internalFormat) { + switch(internalFormat) { + case _GL_R8: + case _GL_RG8: + case GL_RGB8: + case GL_RGBA8: + return GL_UNSIGNED_BYTE; + case 0x822D: // GL_R16F + case 0x822F: // GL_RG16F + case _GL_RGB16F: + case 0x881A: // GL_RGBA16F + return _GL_HALF_FLOAT; + case 0x822E: // GL_R32F + case 0x8230: // GL_RG32F + case 0x8815: // GL_RGB32F + case 0x8814: // GL_RGBA32F + return GL_FLOAT; + default: + throw new UnsupportedOperationException(); + } + } + + public static int trivializeInternalFormatToGLES20(int internalFormat) { + switch(internalFormat) { + case _GL_R8: + return GL_LUMINANCE; + case GL_RGB8: + return GL_RGB; + case GL_RGBA8: + return GL_RGBA; + default: + throw new UnsupportedOperationException(); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/VSHInputLayoutParser.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/VSHInputLayoutParser.java new file mode 100644 index 00000000..0eaf2e9a --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/VSHInputLayoutParser.java @@ -0,0 +1,91 @@ +package net.lax1dude.eaglercraft.v1_8.opengl; + +import java.util.ArrayList; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.EagUtils; +import net.lax1dude.eaglercraft.v1_8.internal.IProgramGL; + +import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL.*; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class VSHInputLayoutParser { + + public static class ShaderInput { + + public final int index; + public final String type; + public final String name; + + public ShaderInput(int index, String type, String name) { + this.index = index; + this.type = type; + this.name = name; + } + + } + + public static class ShaderLayoutParseException extends RuntimeException { + + public ShaderLayoutParseException(String message, Throwable cause) { + super(message, cause); + } + + public ShaderLayoutParseException(String message) { + super(message); + } + + } + + public static List getShaderInputs(String vshSource) { + int idx1 = vshSource.indexOf("EAGLER_VSH_LAYOUT_BEGIN()"); + if(idx1 == -1) { + throw new ShaderLayoutParseException("Could not find \"EAGLER_VSH_LAYOUT_BEGIN()\" delimiter!"); + } + int idx2 = vshSource.indexOf("EAGLER_VSH_LAYOUT_END()", idx1 + 25); + if(idx2 == -1) { + throw new ShaderLayoutParseException("Could not find \"EAGLER_VSH_LAYOUT_END()\" delimiter!"); + } + List lines = EagUtils.linesList(vshSource.substring(idx1 + 25, idx2)); + List ret = new ArrayList<>(); + for(int i = 0, l = lines.size(); i < l; ++i) { + String ln = lines.get(i); + ln = ln.trim(); + if(ln.startsWith("EAGLER_IN(") && ln.endsWith(")")) { + String[] tokens = ln.substring(10, ln.length() - 1).split(",", 3); + if(tokens.length == 3) { + int idx; + try { + idx = Integer.parseInt(tokens[0].trim()); + }catch(NumberFormatException ex) { + continue; + } + ret.add(new ShaderInput(idx, tokens[1].trim(), tokens[2].trim())); + } + } + } + return ret; + } + + public static void applyLayout(IProgramGL program, List layout) { + for(int i = 0, l = layout.size(); i < l; ++i) { + ShaderInput itm = layout.get(i); + _wglBindAttribLocation(program, itm.index, itm.name); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/WorldRenderer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/WorldRenderer.java index a9555f5f..30056597 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/WorldRenderer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/WorldRenderer.java @@ -8,7 +8,6 @@ import java.util.BitSet; import java.util.Comparator; import net.lax1dude.eaglercraft.v1_8.EagRuntime; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformBufferFunctions; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.vector.Vector3f; import net.minecraft.client.renderer.GLAllocation; @@ -121,7 +120,7 @@ public class WorldRenderer { for (int k1 = ainteger[i1].intValue(); j1 != l1; k1 = ainteger[k1].intValue()) { this.intBuffer.limit(k1 * l + l); this.intBuffer.position(k1 * l); - IntBuffer intbuffer = this.intBuffer.slice(); + IntBuffer intbuffer = this.intBuffer.duplicate(); this.intBuffer.limit(j1 * l + l); this.intBuffer.position(j1 * l); this.intBuffer.put(intbuffer); @@ -178,7 +177,10 @@ public class WorldRenderer { */ public void setVertexState(WorldRenderer.State state) { this.grow(state.getRawBuffer().length); - PlatformBufferFunctions.put(this.intBuffer, 0, state.getRawBuffer()); + int p = intBuffer.position(); + this.intBuffer.position(0); + this.intBuffer.put(state.getRawBuffer()); + this.intBuffer.position(p); this.vertexCount = state.getVertexCount(); this.vertexFormat = state.getVertexFormat(); } @@ -339,7 +341,10 @@ public class WorldRenderer { */ public void addVertexData(int[] vertexData) { this.grow(vertexData.length); - PlatformBufferFunctions.put(this.intBuffer, (this.vertexCount * this.vertexFormat.attribStride) >> 2, vertexData); + int p = this.intBuffer.position(); + this.intBuffer.position((this.vertexCount * this.vertexFormat.attribStride) >> 2); + this.intBuffer.put(vertexData); + this.intBuffer.position(p); this.vertexCount += vertexData.length / (this.vertexFormat.attribStride >> 2); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/BlockVertexIDs.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/BlockVertexIDs.java index 2a00144a..2b5a2730 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/BlockVertexIDs.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/BlockVertexIDs.java @@ -32,7 +32,7 @@ public class BlockVertexIDs implements IResourceManagerReloadListener { private static final Logger logger = LogManager.getLogger("BlockVertexIDsCSV"); - public static final Map modelToID = new HashMap(); + public static final Map modelToID = new HashMap<>(); public static int builtin_water_still_vertex_id = 0; public static int builtin_water_flow_vertex_id = 0; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/CloudRenderWorker.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/CloudRenderWorker.java index 789e7fe9..05698c41 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/CloudRenderWorker.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/CloudRenderWorker.java @@ -103,7 +103,7 @@ public class CloudRenderWorker { static void initialize() { destroy(); - cloudStartTimer = System.currentTimeMillis(); + cloudStartTimer = EagRuntime.steadyTimeMillis(); cloudRenderProgress = 0; cloudRenderPeriod = 500; cloudRenderPhase = 0; @@ -168,7 +168,7 @@ public class CloudRenderWorker { _wglTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); _wglTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); _wglTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - byte[] cloudShapeTexture = EagRuntime.getResourceBytes("/assets/eagler/glsl/deferred/clouds_shapes.bmp"); + byte[] cloudShapeTexture = EagRuntime.getRequiredResourceBytes("/assets/eagler/glsl/deferred/clouds_shapes.bmp"); cloudNoiseDatBuffer = EagRuntime.allocateByteBuffer(cloudShapeTexture.length); cloudNoiseDatBuffer.put(cloudShapeTexture); cloudNoiseDatBuffer.flip(); @@ -207,7 +207,7 @@ public class CloudRenderWorker { } static void update() { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); int cloudProgress = (int)(millis - cloudStartTimer); int totalCloudSteps = 32 + 32 - 1; int currentCloudStep = cloudProgress * totalCloudSteps / cloudRenderPeriod; @@ -268,7 +268,7 @@ public class CloudRenderWorker { cloudColorB += (currentSunAngle.z - cloudColorB) * 0.1f; _wglUniform3f(shader_clouds_sample.uniforms.u_sunColor3f, cloudColorR, cloudColorG, cloudColorB); - float cloudDensityTimer = (float)((System.currentTimeMillis() % 10000000l) * 0.001); + float cloudDensityTimer = (float)((EagRuntime.steadyTimeMillis() % 10000000l) * 0.001); cloudDensityTimer += MathHelper.sin(cloudDensityTimer * 1.5f) * 1.5f; float x = cloudDensityTimer * 0.004f; float f1 = MathHelper.sin(x + 0.322f) * 0.544f + MathHelper.sin(x * 4.5f + 1.843f) * 0.69f + MathHelper.sin(x * 3.4f + 0.8f) * 0.6f + MathHelper.sin(x * 6.1f + 1.72f) * 0.7f; @@ -404,7 +404,7 @@ public class CloudRenderWorker { if(b) { cloudRenderProgress = 0; - cloudStartTimer = System.currentTimeMillis(); + cloudStartTimer = EagRuntime.steadyTimeMillis(); cloudProgress = 0; cloudRenderPhase = (cloudRenderPhase + 1) % 3; }else { @@ -539,7 +539,7 @@ public class CloudRenderWorker { } private static void updateShape() { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); float dt = (float)((millis - shapeUpdateTimer) * 0.001); shapeUpdateTimer = millis; if(millis > nextShapeAppearance) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DebugFramebufferView.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DebugFramebufferView.java index dc19044d..1a0913b7 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DebugFramebufferView.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DebugFramebufferView.java @@ -4,12 +4,12 @@ import java.util.Arrays; import java.util.List; import java.util.function.Consumer; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.opengl.DrawUtils; import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; import net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.program.PipelineShaderGBufferDebugView; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; -import net.minecraft.client.gui.ScaledResolution; import static net.lax1dude.eaglercraft.v1_8.internal.PlatformOpenGL.*; import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; @@ -249,7 +249,7 @@ public class DebugFramebufferView { PipelineShaderGBufferDebugView dbg = pipeline.useDebugViewShader(18); GlStateManager.setActiveTexture(GL_TEXTURE0); GlStateManager.bindTexture3D(CloudRenderWorker.cloud3DSamplesTexture); - _wglUniform1f(_wglGetUniformLocation(dbg.program, "u_fuckU1f"), (float)((System.currentTimeMillis() % 5000l) / 5000.0)); + _wglUniform1f(_wglGetUniformLocation(dbg.program, "u_fuckU1f"), (float)((EagRuntime.steadyTimeMillis() % 5000l) / 5000.0)); DrawUtils.drawStandardQuad2D(); })), (new DebugFramebufferView("Clouds Back Buffer", (pipeline) -> { @@ -449,19 +449,18 @@ public class DebugFramebufferView { GlStateManager.clear(GL_COLOR_BUFFER_BIT); noData = true; } - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); long elapsed = millis - debugViewNameTimer; if(elapsed < 2000l || noData) { GlStateManager.matrixMode(GL_PROJECTION); GlStateManager.pushMatrix(); GlStateManager.matrixMode(GL_MODELVIEW); GlStateManager.pushMatrix(); - ScaledResolution scaledresolution = new ScaledResolution(mc); - int w = scaledresolution.getScaledWidth(); + int w = mc.scaledResolution.getScaledWidth(); mc.entityRenderer.setupOverlayRendering(); GlStateManager.enableBlend(); GlStateManager.blendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - int h = scaledresolution.getScaledHeight() / 2; + int h = mc.scaledResolution.getScaledHeight() / 2; if(noData) { String noDataTxt = "No Data"; @@ -507,13 +506,13 @@ public class DebugFramebufferView { public static void toggleDebugView() { debugViewShown = !debugViewShown; if(debugViewShown) { - debugViewNameTimer = System.currentTimeMillis(); + debugViewNameTimer = EagRuntime.steadyTimeMillis(); } } public static void switchView(int dir) { if(!debugViewShown) return; - debugViewNameTimer = System.currentTimeMillis(); + debugViewNameTimer = EagRuntime.steadyTimeMillis(); currentDebugView += dir; if(currentDebugView < 0) currentDebugView = views.size() - 1; if(currentDebugView >= views.size()) currentDebugView = 0; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DeferredStateManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DeferredStateManager.java index 68d5d0a7..e55faddd 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DeferredStateManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DeferredStateManager.java @@ -148,7 +148,7 @@ public class DeferredStateManager { if(!cfg.is_rendering_dynamicLights || !cfg.shaderPackInfo.DYNAMIC_LIGHTS) { return; } - instance.loadLightSourceBucket(centerX, centerY, centerZ); + instance.bindLightSourceBucket(centerX, centerY, centerZ, 1); } } @@ -162,7 +162,7 @@ public class DeferredStateManager { float posX = (float)((x + TileEntityRendererDispatcher.staticPlayerX) - (MathHelper.floor_double(TileEntityRendererDispatcher.staticPlayerX / 16.0) << 4)); float posY = (float)((y + TileEntityRendererDispatcher.staticPlayerY) - (MathHelper.floor_double(TileEntityRendererDispatcher.staticPlayerY / 16.0) << 4)); float posZ = (float)((z + TileEntityRendererDispatcher.staticPlayerZ) - (MathHelper.floor_double(TileEntityRendererDispatcher.staticPlayerZ / 16.0) << 4)); - instance.loadLightSourceBucket((int)posX, (int)posY, (int)posZ); + instance.bindLightSourceBucket((int)posX, (int)posY, (int)posZ, 1); } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DynamicLightInstance.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DynamicLightInstance.java index e0c45ca7..a846bae8 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DynamicLightInstance.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DynamicLightInstance.java @@ -1,5 +1,7 @@ package net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; + /** * Copyright (c) 2023 lax1dude. All Rights Reserved. * @@ -35,7 +37,7 @@ class DynamicLightInstance { } public void updateLight(double posX, double posY, double posZ, float red, float green, float blue) { - this.lastCacheHit = System.currentTimeMillis(); + this.lastCacheHit = EagRuntime.steadyTimeMillis(); this.posX = posX; this.posY = posY; this.posZ = posZ; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DynamicLightManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DynamicLightManager.java index 1315b344..5b91886f 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DynamicLightManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/DynamicLightManager.java @@ -6,6 +6,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; + /** * Copyright (c) 2023 lax1dude. All Rights Reserved. * @@ -23,8 +25,8 @@ import java.util.Map; */ public class DynamicLightManager { - static final Map lightRenderers = new HashMap(); - static final List lightRenderList = new LinkedList(); + static final Map lightRenderers = new HashMap<>(); + static final List lightRenderList = new LinkedList<>(); static long renderTimeout = 5000l; static boolean isRenderLightsPass = false; @@ -51,7 +53,7 @@ public class DynamicLightManager { } static void updateTimers() { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - lastTick > 1000l) { lastTick = millis; Iterator itr = lightRenderers.values().iterator(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/EaglerDeferredPipeline.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/EaglerDeferredPipeline.java index 6e0fa7e7..4e9b63f5 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/EaglerDeferredPipeline.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/EaglerDeferredPipeline.java @@ -71,7 +71,7 @@ import java.util.Iterator; import java.util.List; /** - * Copyright (c) 2023 lax1dude. All Rights Reserved. + * Copyright (c) 2023-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -112,6 +112,7 @@ public class EaglerDeferredPipeline { public double currentRenderX = 0.0; public double currentRenderY = 0.0; public double currentRenderZ = 0.0; + public int currentRenderPosSerial = 0; public IFramebufferGL gBufferFramebuffer = null; @@ -307,6 +308,7 @@ public class EaglerDeferredPipeline { private ByteBuffer worldLightingDataCopyBuffer; public IBufferGL buffer_chunkLightingData; + public IBufferGL buffer_chunkLightingDataZero; private ByteBuffer chunkLightingDataCopyBuffer; private boolean isChunkLightingEnabled = false; public ListSerial currentBoundLightSourceBucket; @@ -336,9 +338,20 @@ public class EaglerDeferredPipeline { public static final Vector3f tmpVector4 = new Vector3f(); public final ListSerial[] lightSourceBuckets; + private final int[] lightSourceBucketSerials; + private final int[] lightSourceRenderPosSerials; public ListSerial currentLightSourceBucket; + private int currentLightSourceBucketId = -1; + private int lightingBufferSliceLength = -1; public static final int MAX_LIGHTS_PER_CHUNK = 12; + public static final int LIGHTING_BUFFER_LENGTH = 32 * MAX_LIGHTS_PER_CHUNK + 16; + + private int uniformBufferOffsetAlignment = -1; + + private int uboAlign(int offset) { + return MathHelper.ceiling_float_int((float)offset / (float)uniformBufferOffsetAlignment) * uniformBufferOffsetAlignment; + } private final int lightSourceBucketsWidth; private final int lightSourceBucketsHeight; @@ -372,13 +385,18 @@ public class EaglerDeferredPipeline { this.lightSourceBucketsHeight = 3; int cnt = 5 * 3 * 5; this.lightSourceBuckets = new ListSerial[cnt]; + this.lightSourceBucketSerials = new int[cnt]; + this.lightSourceRenderPosSerials = new int[cnt]; for(int i = 0; i < cnt; ++i) { - this.lightSourceBuckets[i] = new ArrayListSerial(16); + this.lightSourceBuckets[i] = new ArrayListSerial<>(16); + this.lightSourceBucketSerials[i] = -1; + this.lightSourceRenderPosSerials[i] = -1; } } public void rebuild(EaglerDeferredConfig config) { destroy(); + uniformBufferOffsetAlignment = EaglercraftGPU.getUniformBufferOffsetAlignment(); DeferredStateManager.doCheckErrors = EagRuntime.getConfiguration().isCheckShaderGLErrors(); DeferredStateManager.checkGLError("Pre: rebuild pipeline"); this.config = config; @@ -544,7 +562,7 @@ public class EaglerDeferredPipeline { GlStateManager.bindTexture(ssaoNoiseTexture); setNearest(); int noiseTexSize = 64, noiseTexLen = 16384; - byte[] noiseTexDat = EagRuntime.getResourceBytes("assets/eagler/glsl/deferred/ssao_noise.bmp"); + byte[] noiseTexDat = EagRuntime.getRequiredResourceBytes("assets/eagler/glsl/deferred/ssao_noise.bmp"); if(noiseTexDat == null || noiseTexDat.length != noiseTexLen) { noiseTexDat = new byte[noiseTexLen]; for(int i = 0; i < 4096; ++i) { @@ -592,7 +610,7 @@ public class EaglerDeferredPipeline { GlStateManager.bindTexture(brdfTexture); setLinear(); int brdfLutW = 64, brdfLutH = 64, brdfLutLen = 8192; - byte[] brdfLutDat = EagRuntime.getResourceBytes("assets/eagler/glsl/deferred/brdf_lut.bmp"); + byte[] brdfLutDat = EagRuntime.getRequiredResourceBytes("assets/eagler/glsl/deferred/brdf_lut.bmp"); if(brdfLutDat == null || brdfLutDat.length != brdfLutLen) { brdfLutDat = new byte[brdfLutLen]; for(int i = 0; i < 4096; ++i) { @@ -748,7 +766,9 @@ public class EaglerDeferredPipeline { _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); ByteBuffer copyBuffer = EagRuntime.allocateByteBuffer(262144); int mip = 0; - try(DataInputStream dis = new DataInputStream(EagRuntime.getResourceStream("/assets/eagler/glsl/deferred/eagler_moon.bmp"))) { + + try (DataInputStream dis = new DataInputStream(mc.getResourceManager() + .getResource(new ResourceLocation("eagler:glsl/deferred/eagler_moon.bmp")).getInputStream())) { while(dis.read() == 'E') { int w = dis.readShort(); int h = dis.readShort(); @@ -873,7 +893,7 @@ public class EaglerDeferredPipeline { _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); _wglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); String realistic_water_noise_filename = "assets/eagler/glsl/deferred/realistic_water_noise.bmp"; - byte[] bitmapBytes = EagRuntime.getResourceBytes(realistic_water_noise_filename); + byte[] bitmapBytes = EagRuntime.getRequiredResourceBytes(realistic_water_noise_filename); try { if(bitmapBytes.length != 32768) { throw new IOException("File is length " + bitmapBytes.length + ", expected " + 32768); @@ -1008,15 +1028,22 @@ public class EaglerDeferredPipeline { shader_lighting_point = PipelineShaderLightingPoint.compile(false); shader_lighting_point.loadUniforms(); - buffer_chunkLightingData = _wglGenBuffers(); - EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); - int lightingDataLength = 8 * MAX_LIGHTS_PER_CHUNK + 4; - chunkLightingDataCopyBuffer = EagRuntime.allocateByteBuffer(lightingDataLength << 2); - for(int i = 0; i < lightingDataLength; ++i) { + lightingBufferSliceLength = uboAlign(LIGHTING_BUFFER_LENGTH); + + chunkLightingDataCopyBuffer = EagRuntime.allocateByteBuffer(LIGHTING_BUFFER_LENGTH); + for(int i = 0; i < LIGHTING_BUFFER_LENGTH; i += 4) { chunkLightingDataCopyBuffer.putInt(0); } chunkLightingDataCopyBuffer.flip(); - _wglBufferData(_GL_UNIFORM_BUFFER, chunkLightingDataCopyBuffer, GL_DYNAMIC_DRAW); + + buffer_chunkLightingData = _wglGenBuffers(); + EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); + int cnt = lightSourceBucketsWidth * lightSourceBucketsHeight * lightSourceBucketsWidth; + _wglBufferData(_GL_UNIFORM_BUFFER, cnt * lightingBufferSliceLength, GL_DYNAMIC_DRAW); + + buffer_chunkLightingDataZero = _wglGenBuffers(); + EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingDataZero); + _wglBufferData(_GL_UNIFORM_BUFFER, chunkLightingDataCopyBuffer, GL_STATIC_DRAW); DeferredStateManager.checkGLError("Post: rebuild pipeline: dynamic lights"); } @@ -1042,6 +1069,16 @@ public class EaglerDeferredPipeline { DeferredStateManager.checkGLError("Post: rebuild pipeline"); } + public void setRenderPosGlobal(double renderPosX, double renderPosY, double renderPosZ) { + if (renderPosX != currentRenderX || renderPosY != currentRenderY || renderPosZ != currentRenderZ + || currentRenderPosSerial == 0) { + currentRenderX = renderPosX; + currentRenderY = renderPosY; + currentRenderZ = renderPosZ; + ++currentRenderPosSerial; + } + } + public void updateReprojectionCoordinates(double worldX, double worldY, double worldZ) { double distX = worldX - reprojectionOriginCoordinateX; double distY = worldY - reprojectionOriginCoordinateY; @@ -1467,7 +1504,7 @@ public class EaglerDeferredPipeline { float sunKelvin = 1500.0f + (2500.0f * Math.max(-currentSunAngle.y, 0.0f)); float fff = mc.theWorld.getRainStrength(partialTicks); float ff2 = mc.theWorld.getThunderStrength(partialTicks); - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); int dim = Minecraft.getMinecraft().theWorld.provider.getDimensionId(); // ==================== UPDATE CLOUD RENDERER ===================== // @@ -2172,7 +2209,7 @@ public class EaglerDeferredPipeline { GlStateManager.disableBlend(); } - public void loadLightSourceBucket(int relativeBlockX, int relativeBlockY, int relativeBlockZ) { + public void bindLightSourceBucket(int relativeBlockX, int relativeBlockY, int relativeBlockZ, int uboIndex) { int hw = lightSourceBucketsWidth / 2; int hh = lightSourceBucketsHeight / 2; int bucketX = (relativeBlockX >> 4) + hw; @@ -2180,12 +2217,51 @@ public class EaglerDeferredPipeline { int bucketZ = (relativeBlockZ >> 4) + hw; if(bucketX >= 0 && bucketY >= 0 && bucketZ >= 0 && bucketX < lightSourceBucketsWidth && bucketY < lightSourceBucketsHeight && bucketZ < lightSourceBucketsWidth) { - currentLightSourceBucket = lightSourceBuckets[bucketY * lightSourceBucketsWidth * lightSourceBucketsWidth - + bucketZ * lightSourceBucketsWidth + bucketX]; + currentLightSourceBucketId = bucketY * lightSourceBucketsWidth * lightSourceBucketsWidth + + bucketZ * lightSourceBucketsWidth + bucketX; + currentLightSourceBucket = lightSourceBuckets[currentLightSourceBucketId]; + int ser = currentLightSourceBucket.getEaglerSerial(); + int max = currentLightSourceBucket.size(); + if(max > 0) { + EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); + int offset = currentLightSourceBucketId * lightingBufferSliceLength; + if (lightSourceBucketSerials[currentLightSourceBucketId] != ser + || lightSourceRenderPosSerials[currentLightSourceBucketId] != currentRenderPosSerial) { + lightSourceBucketSerials[currentLightSourceBucketId] = ser; + lightSourceRenderPosSerials[currentLightSourceBucketId] = currentRenderPosSerial; + if(max > MAX_LIGHTS_PER_CHUNK) { + max = MAX_LIGHTS_PER_CHUNK; + } + chunkLightingDataCopyBuffer.clear(); + chunkLightingDataCopyBuffer.putInt(max); + chunkLightingDataCopyBuffer.putInt(0); //padding + chunkLightingDataCopyBuffer.putInt(0); //padding + chunkLightingDataCopyBuffer.putInt(0); //padding + for(int i = 0; i < max; ++i) { + DynamicLightInstance dl = currentLightSourceBucket.get(i); + chunkLightingDataCopyBuffer.putFloat((float)(dl.posX - currentRenderX)); + chunkLightingDataCopyBuffer.putFloat((float)(dl.posY - currentRenderY)); + chunkLightingDataCopyBuffer.putFloat((float)(dl.posZ - currentRenderZ)); + chunkLightingDataCopyBuffer.putInt(0); //padding + chunkLightingDataCopyBuffer.putFloat(dl.red); + chunkLightingDataCopyBuffer.putFloat(dl.green); + chunkLightingDataCopyBuffer.putFloat(dl.blue); + chunkLightingDataCopyBuffer.putInt(0); //padding + } + chunkLightingDataCopyBuffer.flip(); + _wglBufferSubData(_GL_UNIFORM_BUFFER, offset, chunkLightingDataCopyBuffer); + } + EaglercraftGPU.bindUniformBufferRange(uboIndex, buffer_chunkLightingData, offset, LIGHTING_BUFFER_LENGTH); + }else { + EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingDataZero); + EaglercraftGPU.bindUniformBufferRange(uboIndex, buffer_chunkLightingDataZero, 0, LIGHTING_BUFFER_LENGTH); + } }else { + currentLightSourceBucketId = -1; currentLightSourceBucket = null; + EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingDataZero); + EaglercraftGPU.bindUniformBufferRange(uboIndex, buffer_chunkLightingDataZero, 0, LIGHTING_BUFFER_LENGTH); } - updateLightSourceUBO(); } public ListSerial getLightSourceBucketRelativeChunkCoords(int cx, int cy, int cz) { @@ -2319,65 +2395,10 @@ public class EaglerDeferredPipeline { } } - public void updateLightSourceUBO() { - if(currentLightSourceBucket == null) { - currentBoundLightSourceBucket = null; - if(isChunkLightingEnabled) { - isChunkLightingEnabled = false; - EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); - chunkLightingDataCopyBuffer.clear(); - chunkLightingDataCopyBuffer.putInt(0); - chunkLightingDataCopyBuffer.flip(); - _wglBufferSubData(_GL_UNIFORM_BUFFER, 0, chunkLightingDataCopyBuffer); - } - }else { - boolean isNew; - if(!isChunkLightingEnabled) { - isChunkLightingEnabled = true; - isNew = true; - }else { - isNew = currentLightSourceBucket != currentBoundLightSourceBucket; - } - currentBoundLightSourceBucket = currentLightSourceBucket; - if(isNew || currentBoundLightSourceBucket.eaglerCheck()) { - populateLightSourceUBOFromBucket(currentBoundLightSourceBucket); - currentBoundLightSourceBucket.eaglerResetCheck(); - } - } - } - private static final Comparator comparatorLightRadius = (l1, l2) -> { return l1.radius < l2.radius ? 1 : -1; }; - private void populateLightSourceUBOFromBucket(List lights) { - int max = lights.size(); - if(max > MAX_LIGHTS_PER_CHUNK) { - max = MAX_LIGHTS_PER_CHUNK; - } - chunkLightingDataCopyBuffer.clear(); - chunkLightingDataCopyBuffer.putInt(max); - if(max > 0) { - chunkLightingDataCopyBuffer.putInt(0); //padding - chunkLightingDataCopyBuffer.putInt(0); //padding - chunkLightingDataCopyBuffer.putInt(0); //padding - for(int i = 0; i < max; ++i) { - DynamicLightInstance dl = lights.get(i); - chunkLightingDataCopyBuffer.putFloat((float)(dl.posX - currentRenderX)); - chunkLightingDataCopyBuffer.putFloat((float)(dl.posY - currentRenderY)); - chunkLightingDataCopyBuffer.putFloat((float)(dl.posZ - currentRenderZ)); - chunkLightingDataCopyBuffer.putInt(0); //padding - chunkLightingDataCopyBuffer.putFloat(dl.red); - chunkLightingDataCopyBuffer.putFloat(dl.green); - chunkLightingDataCopyBuffer.putFloat(dl.blue); - chunkLightingDataCopyBuffer.putInt(0); //padding - } - } - chunkLightingDataCopyBuffer.flip(); - EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); - _wglBufferSubData(_GL_UNIFORM_BUFFER, 0, chunkLightingDataCopyBuffer); - } - public void beginDrawEnvMap() { DeferredStateManager.checkGLError("Pre: beginDrawEnvMap()"); GlStateManager.enableDepth(); @@ -2797,7 +2818,7 @@ public class EaglerDeferredPipeline { GlStateManager.bindTexture(realisticWaterNoiseMap); shader_realistic_water_noise.useProgram(); - float waveTimer = (float)((System.currentTimeMillis() % 600000l) * 0.001); + float waveTimer = (float)((EagRuntime.steadyTimeMillis() % 600000l) * 0.001); _wglUniform4f(shader_realistic_water_noise.uniforms.u_waveTimer4f, waveTimer, 0.0f, 0.0f, 0.0f); DrawUtils.drawStandardQuad2D(); @@ -3147,7 +3168,7 @@ public class EaglerDeferredPipeline { // ================ DOWNSCALE AND AVERAGE LUMA =============== // - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - lastExposureUpdate > 33l) { if(lumaAvgDownscaleFramebuffers.length == 0) { _wglBindFramebuffer(_GL_FRAMEBUFFER, exposureBlendFramebuffer); @@ -3925,6 +3946,10 @@ public class EaglerDeferredPipeline { _wglDeleteBuffers(buffer_chunkLightingData); buffer_chunkLightingData = null; } + if(buffer_chunkLightingDataZero != null) { + _wglDeleteBuffers(buffer_chunkLightingDataZero); + buffer_chunkLightingDataZero = null; + } if(buffer_worldLightingData != null) { _wglDeleteBuffers(buffer_worldLightingData); buffer_worldLightingData = null; @@ -3939,8 +3964,11 @@ public class EaglerDeferredPipeline { } for(int i = 0; i < lightSourceBuckets.length; ++i) { lightSourceBuckets[i].clear(); + lightSourceBucketSerials[i] = -1; + lightSourceRenderPosSerials[i] = -1; } currentLightSourceBucket = null; + currentLightSourceBucketId = -1; currentBoundLightSourceBucket = null; isChunkLightingEnabled = false; for(int i = 0; i < shader_gbuffer_debug_view.length; ++i) { @@ -3993,11 +4021,13 @@ public class EaglerDeferredPipeline { } public static final boolean isSupported() { - return EaglercraftGPU.checkHasHDRFramebufferSupportWithFilter(); + return EaglercraftGPU.checkOpenGLESVersion() >= 300 && EaglercraftGPU.checkHasHDRFramebufferSupportWithFilter(); } public static final String getReasonUnsupported() { - if(!EaglercraftGPU.checkHasHDRFramebufferSupportWithFilter()) { + if(EaglercraftGPU.checkOpenGLESVersion() < 300) { + return I18n.format("shaders.gui.unsupported.reason.oldOpenGLVersion"); + }else if(!EaglercraftGPU.checkHasHDRFramebufferSupportWithFilter()) { return I18n.format("shaders.gui.unsupported.reason.hdrFramebuffer"); }else { return null; @@ -4015,7 +4045,7 @@ public class EaglerDeferredPipeline { GlStateManager.pushMatrix(); GlStateManager.matrixMode(GL_MODELVIEW); GlStateManager.pushMatrix(); - ScaledResolution scaledresolution = new ScaledResolution(mc); + ScaledResolution scaledresolution = mc.scaledResolution; int w = scaledresolution.getScaledWidth(); mc.entityRenderer.setupOverlayRendering(); GlStateManager.enableAlpha(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/ForwardRenderCallbackHandler.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/ForwardRenderCallbackHandler.java index 699e7474..7dd380c0 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/ForwardRenderCallbackHandler.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/ForwardRenderCallbackHandler.java @@ -22,7 +22,7 @@ import java.util.List; */ public class ForwardRenderCallbackHandler { - public final List renderPassList = new ArrayList(1024); + public final List renderPassList = new ArrayList<>(1024); public void push(ShadersRenderPassFuture f) { renderPassList.add(f); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/LensFlareMeshRenderer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/LensFlareMeshRenderer.java index d258636d..a49c1a8d 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/LensFlareMeshRenderer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/LensFlareMeshRenderer.java @@ -8,7 +8,6 @@ import java.io.DataInputStream; import java.io.IOException; import net.lax1dude.eaglercraft.v1_8.EagRuntime; -import net.lax1dude.eaglercraft.v1_8.EaglerInputStream; import net.lax1dude.eaglercraft.v1_8.internal.IBufferArrayGL; import net.lax1dude.eaglercraft.v1_8.internal.IBufferGL; import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer; @@ -20,6 +19,7 @@ import net.lax1dude.eaglercraft.v1_8.vector.Matrix3f; import net.lax1dude.eaglercraft.v1_8.vector.Vector3f; import net.minecraft.client.Minecraft; import net.minecraft.util.MathHelper; +import net.minecraft.util.ResourceLocation; /** * Copyright (c) 2023 lax1dude. All Rights Reserved. @@ -38,8 +38,8 @@ import net.minecraft.util.MathHelper; */ public class LensFlareMeshRenderer { - public static final String streaksTextureLocation ="assets/eagler/glsl/deferred/lens_streaks.bmp"; - public static final String ghostsTextureLocation = "assets/eagler/glsl/deferred/lens_ghosts.bmp"; + public static final ResourceLocation streaksTextureLocation = new ResourceLocation("eagler:glsl/deferred/lens_streaks.bmp"); + public static final ResourceLocation ghostsTextureLocation = new ResourceLocation("eagler:glsl/deferred/lens_ghosts.bmp"); public static final int ghostsSpriteCount = 4; static IBufferArrayGL streaksVertexArray = null; @@ -157,11 +157,8 @@ public class LensFlareMeshRenderer { streaksTexture = GlStateManager.generateTexture(); GlStateManager.bindTexture(streaksTexture); - byte[] flareTex = EagRuntime.getResourceBytes(streaksTextureLocation); - if(flareTex == null) { - throw new RuntimeException("Could not locate: " + streaksTextureLocation); - } - try(DataInputStream dis = new DataInputStream(new EaglerInputStream(flareTex))) { + try (DataInputStream dis = new DataInputStream( + Minecraft.getMinecraft().getResourceManager().getResource(streaksTextureLocation).getInputStream())) { loadFlareTexture(copyBuffer, dis); }catch(IOException ex) { EagRuntime.freeByteBuffer(copyBuffer); @@ -170,11 +167,8 @@ public class LensFlareMeshRenderer { ghostsTexture = GlStateManager.generateTexture(); GlStateManager.bindTexture(ghostsTexture); - flareTex = EagRuntime.getResourceBytes(ghostsTextureLocation); - if(flareTex == null) { - throw new RuntimeException("Could not locate: " + ghostsTextureLocation); - } - try(DataInputStream dis = new DataInputStream(new EaglerInputStream(flareTex))) { + try (DataInputStream dis = new DataInputStream( + Minecraft.getMinecraft().getResourceManager().getResource(ghostsTextureLocation).getInputStream())) { loadFlareTexture(copyBuffer, dis); }catch(IOException ex) { EagRuntime.freeByteBuffer(copyBuffer); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/ShaderPackInfo.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/ShaderPackInfo.java index df2e38de..4837b044 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/ShaderPackInfo.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/ShaderPackInfo.java @@ -52,7 +52,7 @@ public class ShaderPackInfo { vers = json.optString("vers", "Unknown"); author = json.optString("author", "Unknown"); apiVers = json.optInt("api_vers", -1); - supportedFeatures = new HashSet(); + supportedFeatures = new HashSet<>(); JSONArray features = json.getJSONArray("features"); if(features.length() == 0) { throw new JSONException("No supported features list has been defined for this shader pack!"); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/gui/GuiShaderConfig.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/gui/GuiShaderConfig.java index 7b29066b..6451750f 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/gui/GuiShaderConfig.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/gui/GuiShaderConfig.java @@ -103,6 +103,11 @@ public class GuiShaderConfig extends GuiScreen { listView.handleMouseInput(); } + public void handleTouchInput() throws IOException { + super.handleTouchInput(); + listView.handleTouchInput(); + } + protected void mouseClicked(int parInt1, int parInt2, int parInt3) { super.mouseClicked(parInt1, parInt2, parInt3); listView.mouseClicked(parInt1, parInt2, parInt3); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/gui/GuiShaderConfigList.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/gui/GuiShaderConfigList.java index 680297d1..9d62b7f0 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/gui/GuiShaderConfigList.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/gui/GuiShaderConfigList.java @@ -35,7 +35,7 @@ public class GuiShaderConfigList extends GuiListExtended { private final GuiShaderConfig screen; - private final List list = new ArrayList(); + private final List list = new ArrayList<>(); private static abstract class ShaderOption { @@ -56,7 +56,7 @@ public class GuiShaderConfigList extends GuiListExtended { } private static List loadDescription(String key) { - List ret = new ArrayList(); + List ret = new ArrayList<>(); String msg; int i = 0; while(true) { @@ -112,7 +112,7 @@ public class GuiShaderConfigList extends GuiListExtended { this.list.add(new ListEntrySpacing()); this.list.add(new ListEntrySpacing()); this.list.add(new ListEntryHeader(I18n.format("shaders.gui.headerTier1"))); - List opts = new ArrayList(); + List opts = new ArrayList<>(); EaglerDeferredConfig conf = mcIn.gameSettings.deferredShaderConf; if(conf.shaderPackInfo.WAVING_BLOCKS) { opts.add(new ShaderOption(loadShaderLbl("WAVING_BLOCKS"), loadShaderDesc("WAVING_BLOCKS")) { @@ -550,6 +550,7 @@ public class GuiShaderConfigList extends GuiListExtended { @Override public boolean mousePressed(int var1, int var2, int var3, int var4, int var5, int var6) { + if(var4 != 0) return false; if(this.button1 != null) { if(this.button1.yPosition + 15 < bottom && this.button1.yPosition + 5 > top) { if(this.button1.mousePressed(mc, var2, var3)) { @@ -610,7 +611,7 @@ public class GuiShaderConfigList extends GuiListExtended { } private void renderTooltip(int x, int y, int width, List msg) { - ArrayList tooltipList = new ArrayList(msg.size() * 2); + List tooltipList = new ArrayList<>(msg.size() * 2); for(int i = 0, l = msg.size(); i < l; ++i) { String s = msg.get(i); if(s.length() > 0) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderAccelParticleForward.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderAccelParticleForward.java index a9f18bcf..53933529 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderAccelParticleForward.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderAccelParticleForward.java @@ -32,7 +32,7 @@ public class PipelineShaderAccelParticleForward extends ShaderProgram lst = new ArrayList(2); + List lst = new ArrayList<>(2); if(dynamicLights) { lst.add("COMPILE_DYNAMIC_LIGHTS"); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderGBufferCombine.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderGBufferCombine.java index 497b4900..db4925de 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderGBufferCombine.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderGBufferCombine.java @@ -30,7 +30,7 @@ public class PipelineShaderGBufferCombine extends ShaderProgram compileFlags = new ArrayList(2); + List compileFlags = new ArrayList<>(2); if(ssao) { compileFlags.add("COMPILE_GLOBAL_AMBIENT_OCCLUSION"); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderGBufferFog.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderGBufferFog.java index a158c5d8..0827606b 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderGBufferFog.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderGBufferFog.java @@ -28,7 +28,7 @@ import net.lax1dude.eaglercraft.v1_8.internal.IUniformGL; public class PipelineShaderGBufferFog extends ShaderProgram { public static PipelineShaderGBufferFog compile(boolean linear, boolean atmosphere, boolean lightShafts) { - List macros = new ArrayList(3); + List macros = new ArrayList<>(3); if(linear) { macros.add("COMPILE_FOG_LINEAR"); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderLightingPoint.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderLightingPoint.java index 73f58745..8fc6aa10 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderLightingPoint.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderLightingPoint.java @@ -29,7 +29,7 @@ public class PipelineShaderLightingPoint extends ShaderProgram compileFlags = new ArrayList(2); + List compileFlags = new ArrayList<>(2); if(shadows) { compileFlags.add("COMPILE_PARABOLOID_SHADOW"); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderLightingSun.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderLightingSun.java index 0932c5ac..9e725375 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderLightingSun.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderLightingSun.java @@ -29,7 +29,7 @@ public class PipelineShaderLightingSun extends ShaderProgram compileFlags = new ArrayList(1); + List compileFlags = new ArrayList<>(1); if(shadowsSun > 0) { compileFlags.add("COMPILE_SUN_SHADOW"); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderPostExposureAvg.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderPostExposureAvg.java index e4a092c0..6a267c9a 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderPostExposureAvg.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderPostExposureAvg.java @@ -28,7 +28,7 @@ import net.lax1dude.eaglercraft.v1_8.internal.IUniformGL; public class PipelineShaderPostExposureAvg extends ShaderProgram { public static PipelineShaderPostExposureAvg compile(boolean luma) throws ShaderException { - List compileFlags = new ArrayList(1); + List compileFlags = new ArrayList<>(1); if(luma) { compileFlags.add("CALCULATE_LUMINANCE"); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderReprojControl.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderReprojControl.java index ac9f6c94..36be41ef 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderReprojControl.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderReprojControl.java @@ -28,7 +28,7 @@ import net.lax1dude.eaglercraft.v1_8.internal.IUniformGL; public class PipelineShaderReprojControl extends ShaderProgram { public static PipelineShaderReprojControl compile(boolean ssao, boolean ssr) throws ShaderException { - List compileFlags = new ArrayList(2); + List compileFlags = new ArrayList<>(2); if(ssao) { compileFlags.add("COMPILE_REPROJECT_SSAO"); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderShadowsSun.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderShadowsSun.java index bd318ab1..2164b71d 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderShadowsSun.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderShadowsSun.java @@ -30,7 +30,7 @@ public class PipelineShaderShadowsSun extends ShaderProgram compileFlags = new ArrayList(2); + List compileFlags = new ArrayList<>(2); if(shadowsSun == 0) { throw new IllegalStateException("Enable shadows to compile this shader"); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderSkyboxRender.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderSkyboxRender.java index df798ac8..14e4f458 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderSkyboxRender.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/PipelineShaderSkyboxRender.java @@ -28,7 +28,7 @@ import java.util.List; public class PipelineShaderSkyboxRender extends ShaderProgram { public static PipelineShaderSkyboxRender compile(boolean paraboloid, boolean clouds) throws ShaderException { - List compileFlags = new ArrayList(); + List compileFlags = new ArrayList<>(); if(paraboloid) { compileFlags.add("COMPILE_PARABOLOID_SKY"); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/ShaderCompiler.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/ShaderCompiler.java index ae431957..83b3f572 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/ShaderCompiler.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/ShaderCompiler.java @@ -10,7 +10,7 @@ import net.lax1dude.eaglercraft.v1_8.internal.IProgramGL; import net.lax1dude.eaglercraft.v1_8.internal.IShaderGL; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.opengl.FixedFunctionShader; +import net.lax1dude.eaglercraft.v1_8.opengl.GLSLHeader; import net.minecraft.util.ResourceLocation; /** @@ -47,7 +47,7 @@ public class ShaderCompiler { public static IShaderGL compileShader(String name, int stage, String filename, String source, List compileFlags) throws ShaderCompileException { logger.info("Compiling Shader: " + filename); StringBuilder srcCat = new StringBuilder(); - srcCat.append(FixedFunctionShader.FixedFunctionConstants.VERSION).append('\n'); + srcCat.append(GLSLHeader.getHeader()).append('\n'); if(compileFlags != null && compileFlags.size() > 0) { for(int i = 0, l = compileFlags.size(); i < l; ++i) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/ShaderSource.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/ShaderSource.java index d28f8170..17b21299 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/ShaderSource.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/program/ShaderSource.java @@ -97,7 +97,7 @@ public class ShaderSource { public static final ResourceLocation accel_particle_dynamiclights_vsh = new ResourceLocation("eagler:glsl/dynamiclights/accel_particle_dynamiclights.vsh"); public static final ResourceLocation accel_particle_dynamiclights_fsh = new ResourceLocation("eagler:glsl/dynamiclights/accel_particle_dynamiclights.fsh"); - private static final Map sourceCache = new HashMap(); + private static final Map sourceCache = new HashMap<>(); private static boolean isHighP = false; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/EaglerTextureAtlasSpritePBR.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/EaglerTextureAtlasSpritePBR.java index b095929e..a2074e2d 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/EaglerTextureAtlasSpritePBR.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/EaglerTextureAtlasSpritePBR.java @@ -1,7 +1,6 @@ package net.lax1dude.eaglercraft.v1_8.opengl.ext.deferred.texture; import java.io.IOException; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.Callable; @@ -121,7 +120,7 @@ public class EaglerTextureAtlasSpritePBR extends EaglerTextureAtlasSprite { this.animationMetadata = meta; } else { - ArrayList arraylist = Lists.newArrayList(); + List arraylist = Lists.newArrayList(); for (int l1 = 0; l1 < j1; ++l1) { this.frameTextureDataPBR[0].add(getFrameTextureData(aint[0], k1, l, l1)); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/EmissiveItems.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/EmissiveItems.java index ef2fdb4b..8e9eb1e6 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/EmissiveItems.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/EmissiveItems.java @@ -34,7 +34,7 @@ public class EmissiveItems implements IResourceManagerReloadListener { private static final Logger logger = LogManager.getLogger("EmissiveItemsCSV"); - private static final Map entries = new HashMap(); + private static final Map entries = new HashMap<>(); public static float[] getItemEmission(ItemStack itemStack) { return getItemEmission(itemStack.getItem(), itemStack.getItemDamage()); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/PBRMaterialConstants.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/PBRMaterialConstants.java index 4bd425c8..88fc72e6 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/PBRMaterialConstants.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/deferred/texture/PBRMaterialConstants.java @@ -34,7 +34,7 @@ public class PBRMaterialConstants implements IResourceManagerReloadListener { public static final Logger logger = LogManager.getLogger("PBRMaterialConstants"); public final ResourceLocation resourceLocation; - public final Map spriteNameToMaterialConstants = new HashMap(); + public final Map spriteNameToMaterialConstants = new HashMap<>(); public int defaultMaterial = 0x00000A77; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/dynamiclights/DynamicLightBucketLoader.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/dynamiclights/DynamicLightBucketLoader.java index 8f062140..5a17d1f8 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/dynamiclights/DynamicLightBucketLoader.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/dynamiclights/DynamicLightBucketLoader.java @@ -34,14 +34,19 @@ import net.minecraft.util.MathHelper; public class DynamicLightBucketLoader { public IBufferGL buffer_chunkLightingData; + public IBufferGL buffer_chunkLightingDataZero; private ByteBuffer chunkLightingDataCopyBuffer; - private boolean isChunkLightingEnabled = false; public ListSerial currentBoundLightSourceBucket; public final ListSerial[] lightSourceBuckets; + private final int[] lightSourceBucketsSerials; + private final int[] lightSourceRenderPosSerials; public ListSerial currentLightSourceBucket; + private int currentLightSourceBucketId = -1; + private int lightingBufferSliceLength = -1; public static final int MAX_LIGHTS_PER_CHUNK = 12; + public static final int LIGHTING_BUFFER_LENGTH = 16 * MAX_LIGHTS_PER_CHUNK + 16; private final int lightSourceBucketsWidth; private final int lightSourceBucketsHeight; @@ -49,29 +54,42 @@ public class DynamicLightBucketLoader { private double currentRenderX = 0.0; private double currentRenderY = 0.0; private double currentRenderZ = 0.0; + private int currentRenderPosSerial = 0; public DynamicLightBucketLoader() { this.lightSourceBucketsWidth = 5; this.lightSourceBucketsHeight = 3; int cnt = 5 * 3 * 5; this.lightSourceBuckets = new ListSerial[cnt]; + this.lightSourceBucketsSerials = new int[cnt]; + this.lightSourceRenderPosSerials = new int[cnt]; } public void initialize() { destroy(); - buffer_chunkLightingData = _wglGenBuffers(); - EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); - int lightingDataLength = 4 * MAX_LIGHTS_PER_CHUNK + 4; - chunkLightingDataCopyBuffer = EagRuntime.allocateByteBuffer(lightingDataLength << 2); - for(int i = 0; i < lightingDataLength; ++i) { + int alignment = EaglercraftGPU.getUniformBufferOffsetAlignment(); + lightingBufferSliceLength = MathHelper.ceiling_float_int((float)LIGHTING_BUFFER_LENGTH / (float)alignment) * alignment; + + chunkLightingDataCopyBuffer = EagRuntime.allocateByteBuffer(LIGHTING_BUFFER_LENGTH); + for(int i = 0; i < LIGHTING_BUFFER_LENGTH; i += 4) { chunkLightingDataCopyBuffer.putInt(0); } chunkLightingDataCopyBuffer.flip(); - _wglBufferData(_GL_UNIFORM_BUFFER, chunkLightingDataCopyBuffer, GL_DYNAMIC_DRAW); + + buffer_chunkLightingData = _wglGenBuffers(); + EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); + int cnt = lightSourceBucketsWidth * lightSourceBucketsHeight * lightSourceBucketsWidth; + _wglBufferData(_GL_UNIFORM_BUFFER, cnt * lightingBufferSliceLength, GL_DYNAMIC_DRAW); + + buffer_chunkLightingDataZero = _wglGenBuffers(); + EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingDataZero); + _wglBufferData(_GL_UNIFORM_BUFFER, chunkLightingDataCopyBuffer, GL_STATIC_DRAW); for(int i = 0; i < this.lightSourceBuckets.length; ++i) { - this.lightSourceBuckets[i] = new ArrayListSerial(16); + this.lightSourceBuckets[i] = new ArrayListSerial<>(16); + this.lightSourceBucketsSerials[i] = -1; + this.lightSourceRenderPosSerials[i] = -1; } } @@ -81,7 +99,7 @@ public class DynamicLightBucketLoader { } } - public void loadLightSourceBucket(int relativeBlockX, int relativeBlockY, int relativeBlockZ) { + public void bindLightSourceBucket(int relativeBlockX, int relativeBlockY, int relativeBlockZ, int uboIndex) { int hw = lightSourceBucketsWidth / 2; int hh = lightSourceBucketsHeight / 2; int bucketX = (relativeBlockX >> 4) + hw; @@ -89,12 +107,47 @@ public class DynamicLightBucketLoader { int bucketZ = (relativeBlockZ >> 4) + hw; if(bucketX >= 0 && bucketY >= 0 && bucketZ >= 0 && bucketX < lightSourceBucketsWidth && bucketY < lightSourceBucketsHeight && bucketZ < lightSourceBucketsWidth) { - currentLightSourceBucket = lightSourceBuckets[bucketY * lightSourceBucketsWidth * lightSourceBucketsWidth - + bucketZ * lightSourceBucketsWidth + bucketX]; + currentLightSourceBucketId = bucketY * lightSourceBucketsWidth * lightSourceBucketsWidth + + bucketZ * lightSourceBucketsWidth + bucketX; + currentLightSourceBucket = lightSourceBuckets[currentLightSourceBucketId]; + int ser = currentLightSourceBucket.getEaglerSerial(); + int max = currentLightSourceBucket.size(); + if(max > 0) { + EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); + int offset = currentLightSourceBucketId * lightingBufferSliceLength; + if (lightSourceBucketsSerials[currentLightSourceBucketId] != ser + || lightSourceRenderPosSerials[currentLightSourceBucketId] != currentRenderPosSerial) { + lightSourceBucketsSerials[currentLightSourceBucketId] = ser; + lightSourceRenderPosSerials[currentLightSourceBucketId] = currentRenderPosSerial; + if(max > MAX_LIGHTS_PER_CHUNK) { + max = MAX_LIGHTS_PER_CHUNK; + } + chunkLightingDataCopyBuffer.clear(); + chunkLightingDataCopyBuffer.putInt(max); + chunkLightingDataCopyBuffer.putInt(0); //padding + chunkLightingDataCopyBuffer.putInt(0); //padding + chunkLightingDataCopyBuffer.putInt(0); //padding + for(int i = 0; i < max; ++i) { + DynamicLightInstance dl = currentLightSourceBucket.get(i); + chunkLightingDataCopyBuffer.putFloat((float)(dl.posX - currentRenderX)); + chunkLightingDataCopyBuffer.putFloat((float)(dl.posY - currentRenderY)); + chunkLightingDataCopyBuffer.putFloat((float)(dl.posZ - currentRenderZ)); + chunkLightingDataCopyBuffer.putFloat(dl.radius); + } + chunkLightingDataCopyBuffer.flip(); + _wglBufferSubData(_GL_UNIFORM_BUFFER, offset, chunkLightingDataCopyBuffer); + } + EaglercraftGPU.bindUniformBufferRange(uboIndex, buffer_chunkLightingData, offset, LIGHTING_BUFFER_LENGTH); + }else { + EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingDataZero); + EaglercraftGPU.bindUniformBufferRange(uboIndex, buffer_chunkLightingDataZero, 0, LIGHTING_BUFFER_LENGTH); + } }else { + currentLightSourceBucketId = -1; currentLightSourceBucket = null; + EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingDataZero); + EaglercraftGPU.bindUniformBufferRange(uboIndex, buffer_chunkLightingDataZero, 0, LIGHTING_BUFFER_LENGTH); } - updateLightSourceUBO(); } public ListSerial getLightSourceBucketRelativeChunkCoords(int cx, int cy, int cz) { @@ -188,8 +241,8 @@ public class DynamicLightBucketLoader { } public void truncateOverflowingBuffers() { - for(int i = 0; i < this.lightSourceBuckets.length; ++i) { - List lst = this.lightSourceBuckets[i]; + for(int i = 0; i < lightSourceBuckets.length; ++i) { + List lst = lightSourceBuckets[i]; int k = lst.size(); if(k > MAX_LIGHTS_PER_CHUNK) { lst.sort(comparatorLightRadius); @@ -200,74 +253,18 @@ public class DynamicLightBucketLoader { } } - public void updateLightSourceUBO() { - if(currentLightSourceBucket == null) { - currentBoundLightSourceBucket = null; - if(isChunkLightingEnabled) { - isChunkLightingEnabled = false; - EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); - chunkLightingDataCopyBuffer.clear(); - chunkLightingDataCopyBuffer.putInt(0); - chunkLightingDataCopyBuffer.flip(); - _wglBufferSubData(_GL_UNIFORM_BUFFER, 0, chunkLightingDataCopyBuffer); - } - }else { - boolean isNew; - if(!isChunkLightingEnabled) { - isChunkLightingEnabled = true; - isNew = true; - }else { - isNew = currentLightSourceBucket != currentBoundLightSourceBucket; - } - currentBoundLightSourceBucket = currentLightSourceBucket; - if(isNew || currentBoundLightSourceBucket.eaglerCheck()) { - populateLightSourceUBOFromBucket(currentBoundLightSourceBucket); - currentBoundLightSourceBucket.eaglerResetCheck(); - } - } - } - private static final Comparator comparatorLightRadius = (l1, l2) -> { return l1.radius < l2.radius ? 1 : -1; }; - private void populateLightSourceUBOFromBucket(List lights) { - int max = lights.size(); - if(max > MAX_LIGHTS_PER_CHUNK) { - //tmpListLights.clear(); - //tmpListLights.addAll(lights); - //lights = tmpListLights; - //lights.sort(comparatorLightRadius); - max = MAX_LIGHTS_PER_CHUNK; - } - chunkLightingDataCopyBuffer.clear(); - chunkLightingDataCopyBuffer.putInt(max); - if(max > 0) { - chunkLightingDataCopyBuffer.putInt(0); //padding - chunkLightingDataCopyBuffer.putInt(0); //padding - chunkLightingDataCopyBuffer.putInt(0); //padding - for(int i = 0; i < max; ++i) { - DynamicLightInstance dl = lights.get(i); - chunkLightingDataCopyBuffer.putFloat((float)(dl.posX - currentRenderX)); - chunkLightingDataCopyBuffer.putFloat((float)(dl.posY - currentRenderY)); - chunkLightingDataCopyBuffer.putFloat((float)(dl.posZ - currentRenderZ)); - chunkLightingDataCopyBuffer.putFloat(dl.radius); - } - } - chunkLightingDataCopyBuffer.flip(); - EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); - _wglBufferSubData(_GL_UNIFORM_BUFFER, 0, chunkLightingDataCopyBuffer); - } - public void setRenderPos(double currentRenderX, double currentRenderY, double currentRenderZ) { - this.currentRenderX = currentRenderX; - this.currentRenderY = currentRenderY; - this.currentRenderZ = currentRenderZ; - } - - public void bindUniformBuffer(int index) { - EaglercraftGPU.bindGLUniformBuffer(buffer_chunkLightingData); - EaglercraftGPU.bindUniformBufferRange(index, buffer_chunkLightingData, 0, chunkLightingDataCopyBuffer.capacity()); + if (this.currentRenderX != currentRenderX || this.currentRenderY != currentRenderY + || this.currentRenderZ != currentRenderZ || this.currentRenderPosSerial == 0) { + this.currentRenderX = currentRenderX; + this.currentRenderY = currentRenderY; + this.currentRenderZ = currentRenderZ; + ++this.currentRenderPosSerial; + } } public void destroy() { @@ -279,8 +276,16 @@ public class DynamicLightBucketLoader { _wglDeleteBuffers(buffer_chunkLightingData); buffer_chunkLightingData = null; } - for(int i = 0; i < this.lightSourceBuckets.length; ++i) { - this.lightSourceBuckets[i] = null; + if(buffer_chunkLightingDataZero != null) { + _wglDeleteBuffers(buffer_chunkLightingDataZero); + buffer_chunkLightingDataZero = null; } + for(int i = 0; i < lightSourceBuckets.length; ++i) { + lightSourceBuckets[i] = null; + lightSourceBucketsSerials[i] = -1; + lightSourceRenderPosSerials[i] = -1; + } + currentLightSourceBucket = null; + currentLightSourceBucketId = -1; } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/dynamiclights/DynamicLightsStateManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/dynamiclights/DynamicLightsStateManager.java index 0495c299..bf47ed13 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/dynamiclights/DynamicLightsStateManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/opengl/ext/dynamiclights/DynamicLightsStateManager.java @@ -5,6 +5,8 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; import net.lax1dude.eaglercraft.v1_8.opengl.FixedFunctionPipeline; import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; import net.lax1dude.eaglercraft.v1_8.vector.Matrix4f; @@ -30,10 +32,10 @@ import net.minecraft.util.MathHelper; public class DynamicLightsStateManager { static final DynamicLightsPipelineCompiler deferredExtPipeline = new DynamicLightsPipelineCompiler(); - private static List lightInstancePool = new ArrayList(); + private static List lightInstancePool = new ArrayList<>(); private static int instancePoolIndex = 0; private static int maxListLengthTracker = 0; - static final List lightRenderList = new LinkedList(); + static final List lightRenderList = new LinkedList<>(); static final Matrix4f inverseViewMatrix = new Matrix4f(); static int inverseViewMatrixSerial = 0; static DynamicLightBucketLoader bucketLoader = null; @@ -45,7 +47,7 @@ public class DynamicLightsStateManager { if(bucketLoader == null) { bucketLoader = new DynamicLightBucketLoader(); bucketLoader.initialize(); - bucketLoader.bindUniformBuffer(0); + bucketLoader.bindLightSourceBucket(-999, -999, -999, 0); FixedFunctionPipeline.loadExtensionPipeline(deferredExtPipeline); } if(accelParticleRenderer == null) { @@ -89,7 +91,7 @@ public class DynamicLightsStateManager { public static final void reportForwardRenderObjectPosition(int centerX, int centerY, int centerZ) { if(bucketLoader != null) { - bucketLoader.loadLightSourceBucket(centerX, centerY, centerZ); + bucketLoader.bindLightSourceBucket(centerX, centerY, centerZ, 0); } } @@ -98,7 +100,7 @@ public class DynamicLightsStateManager { float posX = (float)((x + TileEntityRendererDispatcher.staticPlayerX) - (MathHelper.floor_double(TileEntityRendererDispatcher.staticPlayerX / 16.0) << 4)); float posY = (float)((y + TileEntityRendererDispatcher.staticPlayerY) - (MathHelper.floor_double(TileEntityRendererDispatcher.staticPlayerY / 16.0) << 4)); float posZ = (float)((z + TileEntityRendererDispatcher.staticPlayerZ) - (MathHelper.floor_double(TileEntityRendererDispatcher.staticPlayerZ / 16.0) << 4)); - bucketLoader.loadLightSourceBucket((int)posX, (int)posY, (int)posZ); + bucketLoader.bindLightSourceBucket((int)posX, (int)posY, (int)posZ, 0); } } @@ -152,11 +154,11 @@ public class DynamicLightsStateManager { } private static final void updateTimers() { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - lastTick > 5000l) { lastTick = millis; if(maxListLengthTracker < (lightInstancePool.size() >> 1)) { - List newPool = new ArrayList(Math.max(maxListLengthTracker, 16)); + List newPool = new ArrayList<>(Math.max(maxListLengthTracker, 16)); for(int i = 0; i < maxListLengthTracker; ++i) { newPool.add(lightInstancePool.get(i)); } @@ -167,11 +169,15 @@ public class DynamicLightsStateManager { } public static final void destroyAll() { - lightInstancePool = new ArrayList(); + lightInstancePool = new ArrayList<>(); } public static String getF3String() { return "DynamicLightsTotal: " + lastTotal; } + public static boolean isSupported() { + return EaglercraftGPU.checkOpenGLESVersion() >= 300; + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/GuiScreenContentWarning.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/GuiScreenContentWarning.java new file mode 100644 index 00000000..2a339996 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/GuiScreenContentWarning.java @@ -0,0 +1,63 @@ +package net.lax1dude.eaglercraft.v1_8.profanity_filter; + +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.EnumChatFormatting; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenContentWarning extends GuiScreen { + + private final GuiScreen cont; + private boolean enableState; + private GuiButton optButton; + + public GuiScreenContentWarning(GuiScreen cont) { + this.cont = cont; + } + + public void initGui() { + this.buttonList.clear(); + enableState = mc.gameSettings.enableProfanityFilter; + this.buttonList.add(optButton = new GuiButton(1, this.width / 2 - 100, this.height / 6 + 108, I18n.format("options.profanityFilterButton") + ": " + I18n.format(enableState ? "gui.yes" : "gui.no"))); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 138, I18n.format("gui.done"))); + } + + @Override + protected void actionPerformed(GuiButton parGuiButton) { + if(parGuiButton.id == 0) { + mc.gameSettings.enableProfanityFilter = enableState; + mc.gameSettings.hasShownProfanityFilter = true; + mc.gameSettings.saveOptions(); + mc.displayGuiScreen(cont); + }else if(parGuiButton.id == 1) { + enableState = !enableState; + optButton.displayString = I18n.format("options.profanityFilterButton") + ": " + I18n.format(enableState ? "gui.yes" : "gui.no"); + } + } + + public void drawScreen(int mx, int my, float pt) { + this.drawDefaultBackground(); + this.drawCenteredString(fontRendererObj, EnumChatFormatting.BOLD + I18n.format("profanityFilterWarning.title"), this.width / 2, 50, 0xFF4444); + this.drawCenteredString(fontRendererObj, I18n.format("profanityFilterWarning.text0"), this.width / 2, 70, 16777215); + this.drawCenteredString(fontRendererObj, I18n.format("profanityFilterWarning.text1"), this.width / 2, 82, 16777215); + this.drawCenteredString(fontRendererObj, I18n.format("profanityFilterWarning.text2"), this.width / 2, 94, 16777215); + this.drawCenteredString(fontRendererObj, I18n.format("profanityFilterWarning.text4"), this.width / 2, 116, 0xCCCCCC); + super.drawScreen(mx, my, pt); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/LookAlikeUnicodeConv.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/LookAlikeUnicodeConv.java new file mode 100644 index 00000000..5756218b --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/LookAlikeUnicodeConv.java @@ -0,0 +1,1028 @@ +package net.lax1dude.eaglercraft.v1_8.profanity_filter; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class LookAlikeUnicodeConv { + + public static String convertString(String stringIn) { + char[] ret = null; + char c1, c2; + for(int i = 0, l = stringIn.length(); i < l; ++i) { + c1 = stringIn.charAt(i); + c2 = convertChar(c1); + if(c2 != 0) { + if(ret == null) { + ret = stringIn.toCharArray(); + } + ret[i] = c2; + } + } + return ret != null ? new String(ret) : stringIn; + } + + public static char convertChar(char charIn) { + switch (charIn) { + case 9313: + case 9333: + case 65298: + return '2'; + case 9315: + case 9335: + case 65300: + return '4'; + case 9316: + case 9336: + case 65301: + return '5'; + case 9317: + case 9337: + case 65302: + return '6'; + case 9319: + case 9339: + case 65304: + return '8'; + case 9320: + case 9340: + case 65305: + return '9'; + case 192: + case 193: + case 194: + case 195: + case 196: + case 197: + case 256: + case 258: + case 260: + case 461: + case 478: + case 480: + case 506: + case 512: + case 514: + case 550: + case 570: + case 913: + case 1040: + case 1232: + case 1234: + case 7680: + case 7840: + case 7842: + case 7844: + case 7846: + case 7848: + case 7850: + case 7852: + case 7854: + case 7856: + case 7858: + case 7860: + case 7862: + case 7944: + case 7945: + case 7946: + case 7947: + case 7948: + case 7949: + case 7950: + case 7951: + case 8072: + case 8073: + case 8074: + case 8075: + case 8076: + case 8077: + case 8078: + case 8079: + case 8120: + case 8121: + case 8122: + case 8123: + case 8124: + case 9424: + case 65313: + return 'A'; + case 385: + case 386: + case 579: + case 914: + case 1042: + case 7682: + case 7684: + case 7686: + case 9425: + case 65314: + return 'B'; + case 199: + case 262: + case 264: + case 266: + case 268: + case 391: + case 571: + case 1057: + case 1194: + case 7688: + case 9426: + case 65315: + return 'C'; + case 270: + case 272: + case 393: + case 394: + case 7690: + case 7692: + case 7694: + case 7696: + case 7698: + case 9427: + case 65316: + return 'D'; + case 200: + case 201: + case 202: + case 203: + case 274: + case 276: + case 278: + case 280: + case 282: + case 516: + case 518: + case 552: + case 582: + case 904: + case 917: + case 7700: + case 7702: + case 7704: + case 7706: + case 7708: + case 7864: + case 7866: + case 7868: + case 7870: + case 7872: + case 7874: + case 7876: + case 7878: + case 7960: + case 7961: + case 7962: + case 7963: + case 7964: + case 7965: + case 8136: + case 8137: + case 9428: + case 65317: + return 'E'; + case 401: + case 7710: + case 9429: + case 65318: + return 'F'; + case 284: + case 286: + case 288: + case 290: + case 403: + case 484: + case 486: + case 500: + case 7712: + case 9430: + case 65319: + return 'G'; + case 292: + case 294: + case 542: + case 7714: + case 7716: + case 7718: + case 7720: + case 7722: + case 7976: + case 7977: + case 7978: + case 7979: + case 7980: + case 7981: + case 7982: + case 7983: + case 9431: + case 65320: + return 'H'; + case 204: + case 205: + case 206: + case 207: + case 296: + case 298: + case 300: + case 302: + case 304: + case 406: + case 407: + case 463: + case 520: + case 522: + case 906: + case 921: + case 938: + case 1030: + case 7724: + case 7726: + case 7880: + case 7882: + case 7992: + case 7993: + case 7994: + case 7995: + case 7996: + case 7997: + case 7998: + case 7999: + case 9432: + case 65321: + return 'I'; + case 308: + case 584: + case 1032: + case 9433: + case 65322: + return 'J'; + case 310: + case 408: + case 488: + case 922: + case 1036: + case 1050: + case 1178: + case 1180: + case 1182: + case 7728: + case 7730: + case 7732: + case 9434: + case 65323: + return 'K'; + case 313: + case 315: + case 317: + case 319: + case 321: + case 573: + case 7734: + case 7736: + case 7738: + case 7740: + case 9435: + case 65324: + return 'L'; + case 924: + case 7742: + case 7744: + case 7746: + case 9436: + case 65325: + return 'M'; + case 209: + case 323: + case 325: + case 327: + case 413: + case 504: + case 544: + case 925: + case 7748: + case 7750: + case 7752: + case 7754: + case 9437: + case 65326: + return 'N'; + case 48: + case 210: + case 211: + case 212: + case 213: + case 214: + case 332: + case 334: + case 336: + case 416: + case 465: + case 490: + case 492: + case 510: + case 524: + case 526: + case 554: + case 556: + case 558: + case 560: + case 908: + case 927: + case 1054: + case 1254: + case 7756: + case 7758: + case 7760: + case 7762: + case 7884: + case 7886: + case 7888: + case 7890: + case 7892: + case 7894: + case 7896: + case 7898: + case 7900: + case 7902: + case 7904: + case 7906: + case 8008: + case 8009: + case 8010: + case 8011: + case 8012: + case 8013: + case 8184: + case 8185: + case 9438: + case 65296: + case 65327: + return 'O'; + case 420: + case 929: + case 7764: + case 7766: + case 8172: + case 9439: + case 65328: + return 'P'; + case 9440: + case 65329: + return 'Q'; + case 340: + case 342: + case 344: + case 528: + case 530: + case 588: + case 7768: + case 7770: + case 7772: + case 7774: + case 9441: + case 65330: + return 'R'; + case 36: + case 346: + case 348: + case 350: + case 352: + case 536: + case 1029: + case 7776: + case 7778: + case 7780: + case 7782: + case 7784: + case 9442: + case 65331: + return 'S'; + case 354: + case 356: + case 358: + case 428: + case 430: + case 538: + case 574: + case 932: + case 7786: + case 7788: + case 7790: + case 7792: + case 9443: + case 65332: + return 'T'; + case 217: + case 218: + case 219: + case 220: + case 360: + case 362: + case 364: + case 366: + case 368: + case 370: + case 431: + case 467: + case 469: + case 471: + case 473: + case 475: + case 532: + case 534: + case 580: + case 1329: + case 1357: + case 7794: + case 7796: + case 7798: + case 7800: + case 7802: + case 7908: + case 7910: + case 7912: + case 7914: + case 7916: + case 7918: + case 7920: + case 9444: + case 65333: + return 'U'; + case 1140: + case 1142: + case 7804: + case 7806: + case 9445: + case 65334: + return 'V'; + case 372: + case 7808: + case 7810: + case 7812: + case 7814: + case 7816: + case 9446: + case 65335: + return 'W'; + case 935: + case 1061: + case 1202: + case 1276: + case 1278: + case 7818: + case 7820: + case 9447: + case 65336: + return 'X'; + case 221: + case 374: + case 376: + case 435: + case 562: + case 590: + case 933: + case 939: + case 7822: + case 7922: + case 7924: + case 7926: + case 7928: + case 8025: + case 8027: + case 8029: + case 8031: + case 8168: + case 8169: + case 8170: + case 8171: + case 9448: + case 65337: + return 'Y'; + case 377: + case 379: + case 381: + case 437: + case 548: + case 918: + case 7824: + case 7826: + case 7828: + case 9449: + case 65338: + return 'Z'; + case 64: + case 224: + case 225: + case 226: + case 227: + case 228: + case 229: + case 257: + case 259: + case 261: + case 462: + case 479: + case 481: + case 507: + case 513: + case 515: + case 551: + case 940: + case 945: + case 1072: + case 1233: + case 1235: + case 7681: + case 7834: + case 7841: + case 7843: + case 7845: + case 7847: + case 7849: + case 7851: + case 7853: + case 7855: + case 7857: + case 7859: + case 7861: + case 7863: + case 7936: + case 7937: + case 7938: + case 7939: + case 7940: + case 7941: + case 7942: + case 7943: + case 8048: + case 8049: + case 8064: + case 8065: + case 8066: + case 8067: + case 8068: + case 8069: + case 8070: + case 8071: + case 8112: + case 8113: + case 8114: + case 8115: + case 8116: + case 8118: + case 8119: + case 9372: + case 9398: + case 65345: + return 'a'; + case 384: + case 387: + case 388: + case 389: + case 595: + case 946: + case 7683: + case 7685: + case 7687: + case 9373: + case 9399: + case 65346: + return 'b'; + case 231: + case 263: + case 265: + case 267: + case 269: + case 392: + case 572: + case 1089: + case 7689: + case 9374: + case 9400: + case 65347: + return 'c'; + case 271: + case 273: + case 396: + case 598: + case 599: + case 7691: + case 7693: + case 7695: + case 7697: + case 7699: + case 9375: + case 9401: + case 65348: + return 'd'; + case 51: + case 232: + case 233: + case 234: + case 235: + case 275: + case 277: + case 279: + case 281: + case 283: + case 517: + case 519: + case 553: + case 583: + case 1239: + case 7701: + case 7703: + case 7705: + case 7707: + case 7709: + case 7865: + case 7867: + case 7869: + case 7871: + case 7873: + case 7875: + case 7877: + case 7879: + case 9314: + case 9334: + case 9376: + case 9402: + case 65299: + case 65349: + return 'e'; + case 402: + case 7711: + case 9377: + case 9403: + case 65350: + return 'f'; + case 285: + case 287: + case 289: + case 291: + case 485: + case 487: + case 7713: + case 9378: + case 9404: + case 65351: + return 'g'; + case 293: + case 295: + case 543: + case 614: + case 7715: + case 7717: + case 7719: + case 7721: + case 7723: + case 7830: + case 9379: + case 9405: + case 65352: + return 'h'; + case 33: + case 49: + case 236: + case 237: + case 238: + case 239: + case 297: + case 299: + case 301: + case 303: + case 464: + case 521: + case 523: + case 616: + case 617: + case 943: + case 953: + case 970: + case 1110: + case 7725: + case 7727: + case 7881: + case 7883: + case 9312: + case 9332: + case 9380: + case 9406: + case 65297: + case 65353: + return 'i'; + case 309: + case 496: + case 585: + case 669: + case 1112: + case 9381: + case 9407: + case 65354: + return 'j'; + case 311: + case 312: + case 409: + case 489: + case 954: + case 1116: + case 1179: + case 1181: + case 1183: + case 7729: + case 7731: + case 7733: + case 9382: + case 9408: + case 65355: + return 'k'; + case 314: + case 316: + case 318: + case 320: + case 322: + case 410: + case 619: + case 620: + case 621: + case 7735: + case 7737: + case 7739: + case 7741: + case 9383: + case 9409: + case 65356: + return 'l'; + case 625: + case 7743: + case 7745: + case 7747: + case 9384: + case 9410: + case 65357: + return 'm'; + case 241: + case 324: + case 326: + case 328: + case 329: + case 414: + case 505: + case 565: + case 626: + case 627: + case 7749: + case 7751: + case 7753: + case 7755: + case 9385: + case 9411: + case 65358: + return 'n'; + case 242: + case 243: + case 244: + case 245: + case 246: + case 248: + case 333: + case 335: + case 337: + case 417: + case 466: + case 491: + case 493: + case 511: + case 525: + case 527: + case 555: + case 557: + case 559: + case 561: + case 959: + case 972: + case 1086: + case 1255: + case 7757: + case 7759: + case 7761: + case 7763: + case 7885: + case 7887: + case 7889: + case 7891: + case 7893: + case 7895: + case 7897: + case 7899: + case 7901: + case 7903: + case 7905: + case 7907: + case 8000: + case 8001: + case 8002: + case 8003: + case 8004: + case 8005: + case 9386: + case 9412: + case 65359: + return 'o'; + case 421: + case 961: + case 7765: + case 7767: + case 9387: + case 9413: + case 65360: + return 'p'; + case 587: + case 672: + case 9388: + case 9414: + case 65361: + return 'q'; + case 341: + case 343: + case 345: + case 529: + case 531: + case 589: + case 7769: + case 7771: + case 7773: + case 7775: + case 9389: + case 9415: + case 65362: + return 'r'; + case 347: + case 349: + case 351: + case 353: + case 537: + case 575: + case 1109: + case 7777: + case 7779: + case 7781: + case 7783: + case 7785: + case 9390: + case 9416: + case 65363: + return 's'; + case 55: + case 355: + case 357: + case 359: + case 427: + case 429: + case 539: + case 566: + case 964: + case 1090: + case 1197: + case 7787: + case 7789: + case 7791: + case 7793: + case 7831: + case 9318: + case 9338: + case 9391: + case 9417: + case 65303: + case 65364: + return 't'; + case 249: + case 250: + case 251: + case 252: + case 361: + case 363: + case 365: + case 367: + case 369: + case 371: + case 432: + case 468: + case 470: + case 472: + case 474: + case 476: + case 533: + case 535: + case 649: + case 965: + case 973: + case 7795: + case 7797: + case 7799: + case 7801: + case 7803: + case 7909: + case 7911: + case 7913: + case 7915: + case 7917: + case 7919: + case 7921: + case 8016: + case 8017: + case 8018: + case 8019: + case 8020: + case 8021: + case 8022: + case 8023: + case 8160: + case 8161: + case 8162: + case 8163: + case 9392: + case 9418: + case 65365: + return 'u'; + case 1141: + case 1143: + case 7805: + case 7807: + case 9393: + case 9419: + case 65366: + return 'v'; + case 373: + case 7809: + case 7811: + case 7813: + case 7815: + case 7817: + case 7832: + case 9394: + case 9420: + case 65367: + return 'w'; + case 215: + case 967: + case 1093: + case 1203: + case 1277: + case 1279: + case 7819: + case 7821: + case 9395: + case 9421: + case 65368: + return 'x'; + case 253: + case 255: + case 375: + case 436: + case 563: + case 591: + case 947: + case 1118: + case 7823: + case 7833: + case 7923: + case 7925: + case 7927: + case 7929: + case 7935: + case 9396: + case 9422: + case 65369: + return 'y'; + case 378: + case 380: + case 382: + case 438: + case 549: + case 576: + case 656: + case 657: + case 7825: + case 7827: + case 7829: + case 9397: + case 9423: + case 65370: + return 'z'; + } + return 0; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/ProfanityFilter.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/ProfanityFilter.java new file mode 100644 index 00000000..94e35832 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profanity_filter/ProfanityFilter.java @@ -0,0 +1,534 @@ +package net.lax1dude.eaglercraft.v1_8.profanity_filter; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.minecraft.event.HoverEvent; +import net.minecraft.util.ChatComponentScore; +import net.minecraft.util.ChatComponentSelector; +import net.minecraft.util.ChatComponentStyle; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.ChatComponentTranslation; +import net.minecraft.util.ChatStyle; +import net.minecraft.util.IChatComponent; +import net.minecraft.util.MathHelper; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ProfanityFilter { + + private static final Logger logger = LogManager.getLogger("ProfanityFilter"); + + protected final Set[] bannedWords; + + private static ProfanityFilter instance = null; + + public static ProfanityFilter getInstance() { + if(instance == null) { + instance = new ProfanityFilter(); + } + return instance; + } + + private ProfanityFilter() { + logger.info("Loading profanity filter hash set..."); + List strs = EagRuntime.getResourceLines("profanity_filter.wlist"); + if(strs == null) { + throw new RuntimeException("File is missing: profanity_filter.wlist"); + } + long start = EagRuntime.steadyTimeMillis(); + Set[] sets = new Set[0]; + int j = 0; + for(String str : strs) { + if(nextDelimiter(str, 0) != -1) continue; + str = LookAlikeUnicodeConv.convertString(str); + int l = str.length(); + if(l == 0) continue; + if(sets.length < l) { + Set[] setsNew = new Set[l]; + System.arraycopy(sets, 0, setsNew, 0, sets.length); + sets = setsNew; + } + --l; + if(sets[l] == null) { + sets[l] = new HashSet(); + } + if(sets[l].add(str)) { + ++j; + } + } + bannedWords = sets; + logger.info("Processed {} entries after {}ms", j, (EagRuntime.steadyTimeMillis() - start)); + } + + public IChatComponent profanityFilterChatComponent(IChatComponent componentIn) { + if(componentIn == null) return null; + IChatComponent cmp = null; + //long start = System.nanoTime(); + try { + cmp = profanityFilterChatComponent0(componentIn); + }catch(Throwable t) { + logger.error("Profanity filter raised an exception on chat component!"); + logger.error(t); + } + //logger.info("Took: {}", ((System.nanoTime() - start) / 1000000.0)); + return cmp != null ? cmp : componentIn; + } + + protected IChatComponent profanityFilterChatComponent0(IChatComponent componentIn) { + if(componentIn instanceof ChatComponentStyle) { + boolean flag = false; + if(componentIn instanceof ChatComponentText) { + ChatComponentText comp = (ChatComponentText)componentIn; + String str = comp.getChatComponentText_TextValue(); + if(StringUtils.isEmpty(str) && componentIn.getSiblings().isEmpty()) { + return componentIn; + } + str = profanityFilterString0(str); + if(str != null) { + IChatComponent replacedComponent = new ChatComponentText(str); + replacedComponent.setChatStyle(comp.getChatStyleIfPresent()); + replacedComponent.getSiblings().addAll(componentIn.getSiblings()); + componentIn = replacedComponent; + flag = true; + } + }else if(componentIn instanceof ChatComponentTranslation) { + ChatComponentTranslation comp = (ChatComponentTranslation)componentIn; + IChatComponent replacedComponent = new ChatComponentTranslation(comp.getKey(), profanityFilterFormatArgs(comp.getFormatArgs())); + replacedComponent.setChatStyle(comp.getChatStyleIfPresent()); + replacedComponent.getSiblings().addAll(componentIn.getSiblings()); + componentIn = replacedComponent; + flag = true; + } + List siblings = componentIn.getSiblings(); + for(int i = 0, l = siblings.size(); i < l; ++i) { + IChatComponent cmp = profanityFilterChatComponent0(siblings.get(i)); + if(cmp != null) { + if(!flag) { + componentIn = shallowCopy(componentIn); + siblings = componentIn.getSiblings(); + flag = true; + } + siblings.set(i, cmp); + } + } + ChatStyle styleOpt = ((ChatComponentStyle)componentIn).getChatStyleIfPresent(); + if(styleOpt != null) { + HoverEvent hoverEvent = styleOpt.getChatHoverEvent(); + if(hoverEvent != null) { + HoverEvent filteredHoverEvent = profanityFilterHoverEvent(hoverEvent); + if(filteredHoverEvent != null) { + if(!flag) { + componentIn = shallowCopy(componentIn); + flag = true; + } + ChatStyle newStyle = styleOpt.createShallowCopy(); + newStyle.setChatHoverEvent(filteredHoverEvent); + componentIn.setChatStyle(newStyle); + } + } + } + return flag ? componentIn : null; + }else { + return null; + } + } + + private Object[] profanityFilterFormatArgs(Object[] formatArgs) { + Object[] ret = profanityFilterFormatArgs0(formatArgs); + return ret != null ? ret : formatArgs; + } + + private Object[] profanityFilterFormatArgs0(Object[] formatArgs) { + Object[] ret = null; + for(int i = 0; i < formatArgs.length; ++i) { + if(formatArgs[i] != null) { + String arg = formatArgs[i].toString(); + arg = profanityFilterString0(arg); + if(arg != null) { + if(ret == null) { + ret = new Object[formatArgs.length]; + System.arraycopy(formatArgs, 0, ret, 0, ret.length); + } + ret[i] = arg; + } + } + + } + return ret; + } + + protected HoverEvent profanityFilterHoverEvent(HoverEvent evtIn) { + if(evtIn.getAction() == HoverEvent.Action.SHOW_TEXT) { + IChatComponent filtered = evtIn.getValue(); + if(filtered != null) { + filtered = profanityFilterChatComponent0(filtered); + if(filtered != null) { + return new HoverEvent(evtIn.getAction(), filtered); + } + } + } + return null; + } + + private static IChatComponent shallowCopy(IChatComponent comp) { + if(comp instanceof ChatComponentStyle) { + if(comp instanceof ChatComponentText) { + ChatComponentText old = (ChatComponentText)comp; + IChatComponent ret = new ChatComponentText(old.getChatComponentText_TextValue()); + ret.setChatStyle(old.getChatStyleIfPresent()); + ret.getSiblings().addAll(comp.getSiblings()); + return ret; + }else if(comp instanceof ChatComponentTranslation) { + ChatComponentTranslation old = (ChatComponentTranslation)comp; + IChatComponent ret = new ChatComponentTranslation(old.getKey(), old.getFormatArgs()); + ret.setChatStyle(old.getChatStyleIfPresent()); + ret.getSiblings().addAll(comp.getSiblings()); + return ret; + }else if(comp instanceof ChatComponentScore) { + ChatComponentScore old = (ChatComponentScore)comp; + IChatComponent ret = new ChatComponentScore(old.getName(), old.getObjective()); + ret.setChatStyle(old.getChatStyleIfPresent()); + ret.getSiblings().addAll(comp.getSiblings()); + return ret; + }else if(comp instanceof ChatComponentSelector) { + ChatComponentSelector old = (ChatComponentSelector)comp; + IChatComponent ret = new ChatComponentSelector(old.getSelector()); + ret.setChatStyle(old.getChatStyleIfPresent()); + ret.getSiblings().addAll(comp.getSiblings()); + return ret; + } + } + return comp.createCopy(); + } + + private static final char[] stars = new char[] { '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', + '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', + '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', + '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*' }; + + public String profanityFilterString(String stringIn) { + if(stringIn == null) return null; + String str = null; + //long start = System.nanoTime(); + try { + str = profanityFilterString0(stringIn); + }catch(Throwable t) { + logger.error("Profanity filter raised an exception on string!"); + logger.error(t); + } + //logger.info("Took: {}", ((System.nanoTime() - start) / 1000000.0)); + return str != null ? str : stringIn; + } + + protected String profanityFilterString0(String stringIn) { + if(StringUtils.isAllBlank(stringIn)) { + return null; + } + int inLen = stringIn.length(); + boolean flag = false; + int i = 0, j; + int k = -1; + StringBuilder b = null; + String str; + List rangeList = new ArrayList(4); + int minCoverage = 40; + int pieceLen; + while((j = nextDelimiter(stringIn, i)) != -1) { + if(j - i > 2) { + if(b != null) { + str = LookAlikeUnicodeConv.convertString(stripColorCodes(b.toString())).toLowerCase(); + pieceLen = str.length(); + b = null; + //System.out.println("\"" + str + "\""); + rangeList.clear(); + if(isBanned(str, rangeList) && evaluateCoverage(pieceLen, rangeList) > (pieceLen * minCoverage / 100)) { + flag = true; + for(int m = 0, n = rangeList.size(); m < n; ++m) { + stringIn = doStar(stringIn, k, i - 1, rangeList.get(m)); + } + } + k = -1; + } + str = LookAlikeUnicodeConv.convertString(stripColorCodes(stringIn.substring(i, j))).toLowerCase(); + pieceLen = str.length(); + //System.out.println("\"" + str + "\""); + rangeList.clear(); + if(isBanned(str, rangeList) && evaluateCoverage(pieceLen, rangeList) > (pieceLen * minCoverage / 100)) { + flag = true; + for(int m = 0, n = rangeList.size(); m < n; ++m) { + stringIn = doStar(stringIn, i, j, rangeList.get(m)); + } + } + }else if(j - i > 0) { + if(b == null) { + k = i; + b = new StringBuilder(stringIn.substring(i, j)); + }else { + b.append(stringIn.substring(i, j)); + } + } + i = j + 1; + } + j = inLen; + if(j - i > 2) { + if(b != null) { + str = LookAlikeUnicodeConv.convertString(stripColorCodes(b.toString())).toLowerCase(); + pieceLen = str.length(); + b = null; + //System.out.println("\"" + str + "\""); + rangeList.clear(); + if(isBanned(str, rangeList) && evaluateCoverage(pieceLen, rangeList) > (pieceLen * minCoverage / 100)) { + flag = true; + for(int m = 0, n = rangeList.size(); m < n; ++m) { + stringIn = doStar(stringIn, k, i - 1, rangeList.get(m)); + } + } + k = -1; + } + str = LookAlikeUnicodeConv.convertString(stripColorCodes(stringIn.substring(i, j))).toLowerCase(); + pieceLen = str.length(); + //System.out.println("\"" + str + "\""); + rangeList.clear(); + if(isBanned(str, rangeList) && evaluateCoverage(pieceLen, rangeList) > (pieceLen * minCoverage / 100)) { + flag = true; + for(int m = 0, n = rangeList.size(); m < n; ++m) { + stringIn = doStar(stringIn, i, j, rangeList.get(m)); + } + } + }else if(j - i > 0) { + if(b == null) { + k = i; + b = new StringBuilder(stringIn.substring(i, j)); + }else { + b.append(stringIn.substring(i, j)); + } + } + if(b != null) { + str = LookAlikeUnicodeConv.convertString(stripColorCodes(b.toString())).toLowerCase(); + pieceLen = str.length(); + b = null; + //System.out.println("\"" + str + "\""); + rangeList.clear(); + if(isBanned(str, rangeList) && evaluateCoverage(pieceLen, rangeList) > (pieceLen * minCoverage / 100)) { + flag = true; + for(int m = 0, n = rangeList.size(); m < n; ++m) { + stringIn = doStar(stringIn, k, stringIn.length(), rangeList.get(m)); + } + } + } + return flag ? stringIn : null; + } + + protected String doStar(String stringIn, int start, int end, int[] rangeReturn) { + int strlen = stringIn.length(); + start += rangeReturn[0]; + end -= rangeReturn[1]; + int len = end - start; + if(len <= 0 || start < 0 || end > strlen) { + logger.warn("Profanity filter attempted out of bounds substring @ strlen: {}, start: {}, end: {}, len: {}", strlen, start, end, len); + return stringIn; + } + StringBuilder fixed = new StringBuilder(stringIn.substring(0, start)); + fixed.append(stars, 0, MathHelper.clamp_int(len, 1, stars.length)); + fixed.append(stringIn, end, stringIn.length()); + return fixed.toString(); + } + + protected static final int[] zeroZero = new int[2]; + + protected boolean isBanned(String word, List rangeReturn) { + int l = word.length(); + if(l == 0) return false; + int i = bannedWords.length; + int k; + boolean flag = false; + for(int j = Math.min(l, i); j >= 3; --j) { + if(j == l) { + Set set = bannedWords[j - 1]; + //System.out.println(" - \"" + word + "\""); + if(set != null && set.contains(word)) { + rangeReturn.add(zeroZero); + return true; + } + }else { + Set set = bannedWords[j - 1]; + if(set != null) { + int m = l - j; + for(int n = 0; n <= m; ++n) { + if(overlaps(n, n + j, l, rangeReturn)) { + //System.out.println("Skipping overlap: " + n + " -> " + (n + j)); + continue; + } + String ss = word.substring(n, n + j); + //System.out.println(" - \"" + ss + "\""); + if(set.contains(ss)) { + rangeReturn.add(new int[] { n, m - n }); + flag = true; + } + } + } + } + } + return flag; + } + + protected static boolean overlaps(int min, int max, int fullLen, List rangeReturn) { + int[] ii; + int j, k; + for(int i = 0, l = rangeReturn.size(); i < l; ++i) { + ii = rangeReturn.get(i); + j = ii[0]; + k = fullLen - ii[1]; + if(min < k && j < max) { + return true; + } + } + return false; + } + + protected static int evaluateCoverage(int fullLen, List rangeReturn) { + int coverage = 0; + int[] ii; + for(int i = 0, l = rangeReturn.size(); i < l; ++i) { + ii = rangeReturn.get(i); + coverage += fullLen - ii[0] - ii[1] - 1; + } + return coverage; + } + + protected static String stripColorCodes(String stringIn) { + int i = stringIn.indexOf('\u00a7'); + if(i != -1) { + int j = 0; + int l = stringIn.length(); + StringBuilder ret = new StringBuilder(l); + do { + if(i - j > 0) { + ret.append(stringIn, j, i); + } + j = i + 2; + }while(j < l && (i = stringIn.indexOf('\u00a7', j)) != -1); + if(l - j > 0) { + ret.append(stringIn, j, l); + } + return ret.toString(); + }else { + return stringIn; + } + } + + private static int nextDelimiter(String stringIn, int startIndex) { + int len = stringIn.length(); + while(startIndex < len) { + char c = stringIn.charAt(startIndex); + switch(c) { + case ' ': + case '.': + case ',': + case '_': + case '+': + case '-': + case '=': + case '|': + case '?': + case '<': + case '>': + case '/': + case '\\': + case '\'': + case '\"': + case '@': + case '#': + case '%': + case '^': + case '&': + case '*': + case '(': + case ')': + case '{': + case '}': + case ':': + case ';': + case '`': + case '~': + case '\n': + case '\r': + case '\t': + case '\u00a0': + return startIndex; + case '!': + if (!(startIndex > 0 && !isDelimiter(stringIn.charAt(startIndex - 1))) + || !(startIndex + 1 < len && !isDelimiter(stringIn.charAt(startIndex + 1)))) { + return startIndex; + } + default: + break; + } + ++startIndex; + } + return -1; + } + + private static boolean isDelimiter(char c) { + switch(c) { + case ' ': + case '.': + case ',': + case '_': + case '+': + case '-': + case '=': + case '|': + case '?': + case '!': + case '<': + case '>': + case '/': + case '\\': + case '\'': + case '\"': + case '@': + case '#': + case '%': + case '^': + case '&': + case '*': + case '(': + case ')': + case '{': + case '}': + case ':': + case ';': + case '`': + case '~': + case '\n': + case '\r': + case '\t': + case '\u00a0': + return true; + } + return false; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/CapePackets.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/CapePackets.java index b8df6591..af6327a8 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/CapePackets.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/CapePackets.java @@ -1,11 +1,5 @@ package net.lax1dude.eaglercraft.v1_8.profile; -import java.io.IOException; - -import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; -import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; -import net.minecraft.network.PacketBuffer; - /** * Copyright (c) 2024 lax1dude. All Rights Reserved. * @@ -25,42 +19,6 @@ public class CapePackets { public static final int PACKET_MY_CAPE_PRESET = 0x01; public static final int PACKET_MY_CAPE_CUSTOM = 0x02; - public static final int PACKET_GET_OTHER_CAPE = 0x03; - public static final int PACKET_OTHER_CAPE_PRESET = 0x04; - public static final int PACKET_OTHER_CAPE_CUSTOM = 0x05; - - public static void readPluginMessage(PacketBuffer buffer, ServerCapeCache capeCache) throws IOException { - try { - int type = (int)buffer.readByte() & 0xFF; - switch(type) { - case PACKET_OTHER_CAPE_PRESET: { - EaglercraftUUID responseUUID = buffer.readUuid(); - int responsePreset = buffer.readInt(); - if(buffer.isReadable()) { - throw new IOException("PACKET_OTHER_CAPE_PRESET had " + buffer.readableBytes() + " remaining bytes!"); - } - capeCache.cacheCapePreset(responseUUID, responsePreset); - break; - } - case PACKET_OTHER_CAPE_CUSTOM: { - EaglercraftUUID responseUUID = buffer.readUuid(); - byte[] readCape = new byte[1173]; - buffer.readBytes(readCape); - if(buffer.isReadable()) { - throw new IOException("PACKET_OTHER_CAPE_CUSTOM had " + buffer.readableBytes() + " remaining bytes!"); - } - capeCache.cacheCapeCustom(responseUUID, readCape); - break; - } - default: - throw new IOException("Unknown skin packet type: " + type); - } - }catch(IOException ex) { - throw ex; - }catch(Throwable t) { - throw new IOException("Failed to parse cape packet!", t); - } - } public static byte[] writeMyCapePreset(int capeId) { return new byte[] { (byte) PACKET_MY_CAPE_PRESET, (byte) (capeId >>> 24), (byte) (capeId >>> 16), @@ -74,10 +32,4 @@ public class CapePackets { return packet; } - public static PacketBuffer writeGetOtherCape(EaglercraftUUID playerId) throws IOException { - PacketBuffer ret = new PacketBuffer(Unpooled.buffer(17, 17)); - ret.writeByte(PACKET_GET_OTHER_CAPE); - ret.writeUuid(playerId); - return ret; - } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/EaglerProfile.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/EaglerProfile.java index 13bd51d4..d5edfaa0 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/EaglerProfile.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/EaglerProfile.java @@ -40,12 +40,34 @@ public class EaglerProfile { public static int presetCapeId; public static int customCapeId; - public static final List customSkins = new ArrayList(); - public static final List customCapes = new ArrayList(); + public static boolean isServerSkinOverride = false; + public static int overridePresetSkinId = -1; + public static final ResourceLocation overrideCustomSkinTexture = new ResourceLocation("eagler:skins/custom/tex_server_override"); + public static EaglerSkinTexture overrideCustomSkin = null; + public static SkinModel overrideCustomSkinModel = SkinModel.STEVE; + + public static boolean isServerCapeOverride = false; + public static int overridePresetCapeId = -1; + public static final ResourceLocation overrideCustomCapeTexture = new ResourceLocation("eagler:capes/custom/tex_server_override"); + public static EaglerSkinTexture overrideCustomCape = null; + + public static final List customSkins = new ArrayList<>(); + public static final List customCapes = new ArrayList<>(); public static final EaglercraftRandom rand; public static ResourceLocation getActiveSkinResourceLocation() { + if(isServerSkinOverride) { + if(overridePresetSkinId == -1) { + return overrideCustomSkinTexture; + }else { + if(overridePresetSkinId >= 0 && overridePresetSkinId < DefaultSkins.defaultSkinsMap.length) { + return DefaultSkins.defaultSkinsMap[overridePresetSkinId].location; + }else { + return DefaultSkins.defaultSkinsMap[0].location; + } + } + } if(presetSkinId == -1) { if(customSkinId >= 0 && customSkinId < customSkins.size()) { return customSkins.get(customSkinId).getResource(); @@ -65,6 +87,17 @@ public class EaglerProfile { } public static SkinModel getActiveSkinModel() { + if(isServerSkinOverride) { + if(overridePresetSkinId == -1) { + return overrideCustomSkinModel; + }else { + if(overridePresetSkinId >= 0 && overridePresetSkinId < DefaultSkins.defaultSkinsMap.length) { + return DefaultSkins.defaultSkinsMap[overridePresetSkinId].model; + }else { + return DefaultSkins.defaultSkinsMap[0].model; + } + } + } if(presetSkinId == -1) { if(customSkinId >= 0 && customSkinId < customSkins.size()) { return customSkins.get(customSkinId).model; @@ -84,6 +117,17 @@ public class EaglerProfile { } public static ResourceLocation getActiveCapeResourceLocation() { + if(isServerCapeOverride) { + if(overridePresetCapeId == -1) { + return overrideCustomCapeTexture; + }else { + if(overridePresetCapeId >= 0 && overridePresetCapeId < DefaultCapes.defaultCapesMap.length) { + return DefaultCapes.defaultCapesMap[overridePresetCapeId].location; + }else { + return DefaultCapes.defaultCapesMap[0].location; + } + } + } if(presetCapeId == -1) { if(customCapeId >= 0 && customCapeId < customCapes.size()) { return customCapes.get(customCapeId).getResource(); @@ -118,10 +162,15 @@ public class EaglerProfile { } } - public static byte[] getSkinPacket() { + public static byte[] getSkinPacket(int vers) { if(presetSkinId == -1) { if(customSkinId >= 0 && customSkinId < customSkins.size()) { - return SkinPackets.writeMySkinCustom(customSkins.get(customSkinId)); + CustomSkin toSend = customSkins.get(customSkinId); + if(vers <= 3) { + return SkinPackets.writeMySkinCustomV3(toSend); + }else { + return SkinPackets.writeMySkinCustomV4(toSend); + } }else { customSkinId = -1; presetSkinId = 0; @@ -156,6 +205,59 @@ public class EaglerProfile { } } + public static void handleForceSkinPreset(int preset) { + isServerSkinOverride = true; + overridePresetSkinId = preset; + ServerSkinCache.needReloadClientSkin = true; + } + + public static void handleForceSkinCustom(int modelID, byte[] datav3) { + if(datav3.length != 16384) { + return; + } + isServerSkinOverride = true; + overridePresetSkinId = -1; + overrideCustomSkinModel = SkinModel.getModelFromId(modelID); + if(overrideCustomSkinModel.highPoly != null) { + overrideCustomSkinModel = SkinModel.STEVE; + } + if(overrideCustomSkin == null) { + overrideCustomSkin = new EaglerSkinTexture(datav3, 64, 64); + Minecraft.getMinecraft().getTextureManager().loadTexture(overrideCustomSkinTexture, overrideCustomSkin); + }else { + overrideCustomSkin.copyPixelsIn(datav3); + } + ServerSkinCache.needReloadClientSkin = true; + } + + public static void handleForceCapePreset(int preset) { + isServerCapeOverride = true; + overridePresetCapeId = preset; + ServerCapeCache.needReloadClientCape = true; + } + + public static void handleForceCapeCustom(byte[] custom) { + if(custom.length != 1173) { + return; + } + byte[] pixels32x32 = new byte[4096]; + SkinConverter.convertCape23x17RGBto32x32RGBA(custom, pixels32x32); + isServerCapeOverride = true; + overridePresetCapeId = -1; + if(overrideCustomCape == null) { + overrideCustomCape = new EaglerSkinTexture(pixels32x32, 32, 32); + Minecraft.getMinecraft().getTextureManager().loadTexture(overrideCustomCapeTexture, overrideCustomCape); + }else { + overrideCustomCape.copyPixelsIn(pixels32x32); + } + ServerCapeCache.needReloadClientCape = true; + } + + public static void clearServerSkinOverride() { + isServerSkinOverride = false; + isServerCapeOverride = false; + } + private static boolean doesSkinExist(String name) { for(int i = 0, l = customSkins.size(); i < l; ++i) { if(customSkins.get(i).name.equalsIgnoreCase(name)) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/EaglerSkinTexture.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/EaglerSkinTexture.java index bea6cf60..025a0149 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/EaglerSkinTexture.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/EaglerSkinTexture.java @@ -44,15 +44,23 @@ public class EaglerSkinTexture implements ITextureObject { if(pixels.length != width * height * 4) { throw new IllegalArgumentException("Wrong data length " + pixels.length + " for " + width + "x" + height + " texture"); } + this.pixels = convertToInt(pixels); + this.width = width; + this.height = height; + } + + public static int[] convertToInt(byte[] pixels) { int[] p = new int[pixels.length >> 2]; for(int i = 0, j; i < p.length; ++i) { j = i << 2; p[i] = (((int) pixels[j] & 0xFF) << 24) | (((int) pixels[j + 1] & 0xFF) << 16) | (((int) pixels[j + 2] & 0xFF) << 8) | ((int) pixels[j + 3] & 0xFF); } - this.pixels = p; - this.width = width; - this.height = height; + return p; + } + + public void copyPixelsIn(byte[] pixels) { + copyPixelsIn(convertToInt(pixels)); } public void copyPixelsIn(int[] pixels) { @@ -93,4 +101,16 @@ public class EaglerSkinTexture implements ITextureObject { textureId = -1; } + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int[] getData() { + return pixels; + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiAuthenticationScreen.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiAuthenticationScreen.java index 3dc82f46..bec4f424 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiAuthenticationScreen.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiAuthenticationScreen.java @@ -2,7 +2,7 @@ package net.lax1dude.eaglercraft.v1_8.profile; import net.lax1dude.eaglercraft.v1_8.Keyboard; import net.lax1dude.eaglercraft.v1_8.internal.KeyboardConstants; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformNetworking; +import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; import net.lax1dude.eaglercraft.v1_8.socket.ConnectionHandshake; import net.lax1dude.eaglercraft.v1_8.socket.HandshakePacketTypes; import net.minecraft.client.gui.GuiButton; @@ -92,9 +92,6 @@ public class GuiAuthenticationScreen extends GuiScreen { this.mc.displayGuiScreen(new GuiConnecting(retAfterAuthScreen, password.getText())); }else { this.mc.displayGuiScreen(parent); - if (!PlatformNetworking.playConnectionState().isClosed()) { - PlatformNetworking.playDisconnect(); - } } } @@ -122,4 +119,14 @@ public class GuiAuthenticationScreen extends GuiScreen { this.password.mouseClicked(parInt1, parInt2, parInt3); } + @Override + public boolean showCopyPasteButtons() { + return password.isFocused(); + } + + @Override + public void fireInputEvent(EnumInputEvent event, String param) { + password.fireInputEvent(event, param); + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenEditCape.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenEditCape.java index 6e50f1ab..e4cee9f7 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenEditCape.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenEditCape.java @@ -3,6 +3,7 @@ package net.lax1dude.eaglercraft.v1_8.profile; import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.Keyboard; import net.lax1dude.eaglercraft.v1_8.Mouse; +import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; import net.lax1dude.eaglercraft.v1_8.internal.EnumCursorType; import net.lax1dude.eaglercraft.v1_8.internal.FileChooserResult; import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; @@ -51,7 +52,6 @@ public class GuiScreenEditCape extends GuiScreen { public GuiScreenEditCape(GuiScreenEditProfile parent) { this.parent = parent; - updateOptions(); } public void initGui() { @@ -61,6 +61,7 @@ public class GuiScreenEditCape extends GuiScreen { buttonList.add(new GuiButton(0, width / 2 - 100, height / 6 + 168, I18n.format("gui.done"))); buttonList.add(new GuiButton(1, width / 2 - 21, height / 6 + 80, 71, 20, I18n.format("editCape.addCape"))); buttonList.add(new GuiButton(2, width / 2 - 21 + 71, height / 6 + 80, 72, 20, I18n.format("editCape.clearCape"))); + updateOptions(); } private void updateOptions() { @@ -243,7 +244,7 @@ public class GuiScreenEditCape extends GuiScreen { if(EagRuntime.fileChooserHasResult()) { FileChooserResult result = EagRuntime.getFileChooserResult(); if(result != null) { - ImageData loadedCape = ImageData.loadImageFile(result.fileData); + ImageData loadedCape = ImageData.loadImageFile(result.fileData, ImageData.getMimeFromType(result.fileName)); if(loadedCape != null) { if((loadedCape.width == 32 || loadedCape.width == 64) && loadedCape.height == 32) { byte[] resized = new byte[1173]; @@ -258,12 +259,12 @@ public class GuiScreenEditCape extends GuiScreen { EagRuntime.showPopup("The selected image '" + result.fileName + "' is not the right size!\nEaglercraft only supports 32x32 or 64x32 capes"); } }else { - EagRuntime.showPopup("The selected file '" + result.fileName + "' is not a PNG file!"); + EagRuntime.showPopup("The selected file '" + result.fileName + "' is not a supported format!"); } } } if(dropDownOpen) { - if(Mouse.isButtonDown(0)) { + if(PointerInputAbstraction.getVCursorButtonDown(0)) { int skinX = width / 2 - 20; int skinY = height / 6 + 73; int skinWidth = 140; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenEditProfile.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenEditProfile.java index 8ed345bf..4910f395 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenEditProfile.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenEditProfile.java @@ -3,8 +3,10 @@ package net.lax1dude.eaglercraft.v1_8.profile; import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.Keyboard; import net.lax1dude.eaglercraft.v1_8.Mouse; +import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; import net.lax1dude.eaglercraft.v1_8.internal.EnumCursorType; import net.lax1dude.eaglercraft.v1_8.internal.FileChooserResult; +import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; import net.lax1dude.eaglercraft.v1_8.opengl.ImageData; import net.minecraft.client.audio.PositionedSoundRecord; @@ -57,7 +59,6 @@ public class GuiScreenEditProfile extends GuiScreen { public GuiScreenEditProfile(GuiScreen parent) { this.parent = parent; - updateOptions(); } public void initGui() { @@ -66,10 +67,12 @@ public class GuiScreenEditProfile extends GuiScreen { usernameField = new GuiTextField(0, fontRendererObj, width / 2 - 20 + 1, height / 6 + 24 + 1, 138, 20); usernameField.setFocused(true); usernameField.setText(EaglerProfile.getName()); + usernameField.setMaxStringLength(16); selectedSlot = EaglerProfile.presetSkinId == -1 ? EaglerProfile.customSkinId : (EaglerProfile.presetSkinId + EaglerProfile.customSkins.size()); buttonList.add(new GuiButton(0, width / 2 - 100, height / 6 + 168, I18n.format("gui.done"))); buttonList.add(new GuiButton(1, width / 2 - 21, height / 6 + 110, 71, 20, I18n.format("editProfile.addSkin"))); buttonList.add(new GuiButton(2, width / 2 - 21 + 71, height / 6 + 110, 72, 20, I18n.format("editProfile.clearSkin"))); + updateOptions(); } private void updateOptions() { @@ -330,7 +333,7 @@ public class GuiScreenEditProfile extends GuiScreen { if(EagRuntime.fileChooserHasResult()) { FileChooserResult result = EagRuntime.getFileChooserResult(); if(result != null) { - ImageData loadedSkin = ImageData.loadImageFile(result.fileData); + ImageData loadedSkin = ImageData.loadImageFile(result.fileData, ImageData.getMimeFromType(result.fileName)); if(loadedSkin != null) { boolean isLegacy = loadedSkin.width == 64 && loadedSkin.height == 32; boolean isModern = loadedSkin.width == 64 && loadedSkin.height == 64; @@ -367,12 +370,12 @@ public class GuiScreenEditProfile extends GuiScreen { EagRuntime.showPopup("The selected image '" + result.fileName + "' is not the right size!\nEaglercraft only supports 64x32 or 64x64 skins"); } }else { - EagRuntime.showPopup("The selected file '" + result.fileName + "' is not a PNG file!"); + EagRuntime.showPopup("The selected file '" + result.fileName + "' is not a supported format!"); } } } if(dropDownOpen) { - if(Mouse.isButtonDown(0)) { + if(PointerInputAbstraction.getVCursorButtonDown(0)) { int skinX = width / 2 - 20; int skinY = height / 6 + 103; int skinWidth = 140; @@ -532,4 +535,14 @@ public class GuiScreenEditProfile extends GuiScreen { EaglerProfile.setName(name); } + @Override + public boolean showCopyPasteButtons() { + return usernameField.isFocused(); + } + + @Override + public void fireInputEvent(EnumInputEvent event, String param) { + usernameField.fireInputEvent(event, param); + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenImportProfile.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenImportProfile.java index 825974a9..a08dc915 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenImportProfile.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/GuiScreenImportProfile.java @@ -77,8 +77,8 @@ public class GuiScreenImportProfile extends GuiScreen { }else { mc.loadingScreen.eaglerShow(I18n.format("settingsBackup.importing.1"), I18n.format("settingsBackup.importing.2")); try { - List list1 = new ArrayList(mc.gameSettings.resourcePacks); - List list2 = new ArrayList(mc.gameSettings.field_183018_l); + List list1 = new ArrayList<>(mc.gameSettings.resourcePacks); + List list2 = new ArrayList<>(mc.gameSettings.field_183018_l); importer.importProfileAndSettings(doImportProfile, doImportSettings, doImportServers, doImportResourcePacks); boolean resourcePacksChanged = !mc.gameSettings.resourcePacks.equals(list1) || !mc.gameSettings.field_183018_l.equals(list2); if(resourcePacksChanged || (doImportResourcePacks && (list1.size() > 0 || list2.size() > 0))) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/RenderHighPoly.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/RenderHighPoly.java index 515b1ea6..176c9907 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/RenderHighPoly.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/RenderHighPoly.java @@ -2,6 +2,7 @@ package net.lax1dude.eaglercraft.v1_8.profile; import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.opengl.EaglerMeshLoader; @@ -158,7 +159,7 @@ public class RenderHighPoly extends RenderPlayer { if(highPolySkin.headModel != null) { if(highPolySkin == HighPolySkin.BABY_CHARLES) { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); float partialTicks = (float) ((millis - abstractclientplayer.eaglerHighPolyAnimationTick) * 0.02); //long l50 = millis / 50l * 50l; //boolean runTick = par1EntityPlayer.eaglerHighPolyAnimationTick < l50 && millis >= l50; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/ServerCapeCache.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/ServerCapeCache.java index 77fb2766..ffad6327 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/ServerCapeCache.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/ServerCapeCache.java @@ -1,17 +1,16 @@ package net.lax1dude.eaglercraft.v1_8.profile; -import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.socket.EaglercraftNetworkManager; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.CPacketGetOtherCapeEAG; +import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.client.renderer.texture.TextureManager; -import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.client.C17PacketCustomPayload; import net.minecraft.util.ResourceLocation; /** @@ -39,7 +38,7 @@ public class ServerCapeCache { protected final int presetCapeId; protected final CacheCustomCape customCape; - protected long lastCacheHit = System.currentTimeMillis(); + protected long lastCacheHit = EagRuntime.steadyTimeMillis(); protected CapeCacheEntry(EaglerSkinTexture textureInstance, ResourceLocation resourceLocation) { this.isPresetCape = false; @@ -96,26 +95,32 @@ public class ServerCapeCache { } private final CapeCacheEntry defaultCacheEntry = new CapeCacheEntry(0); - private final Map capesCache = new HashMap(); - private final Map waitingCapes = new HashMap(); - private final Map evictedCapes = new HashMap(); + private final Map capesCache = new HashMap<>(); + private final Map waitingCapes = new HashMap<>(); + private final Map evictedCapes = new HashMap<>(); - private final EaglercraftNetworkManager networkManager; + private final NetHandlerPlayClient netHandler; protected final TextureManager textureManager; private final EaglercraftUUID clientPlayerId; - private final CapeCacheEntry clientPlayerCacheEntry; + private CapeCacheEntry clientPlayerCacheEntry; - private long lastFlush = System.currentTimeMillis(); - private long lastFlushReq = System.currentTimeMillis(); - private long lastFlushEvict = System.currentTimeMillis(); + private long lastFlush = EagRuntime.steadyTimeMillis(); + private long lastFlushReq = EagRuntime.steadyTimeMillis(); + private long lastFlushEvict = EagRuntime.steadyTimeMillis(); private static int texId = 0; + public static boolean needReloadClientCape = false; - public ServerCapeCache(EaglercraftNetworkManager networkManager, TextureManager textureManager) { - this.networkManager = networkManager; + public ServerCapeCache(NetHandlerPlayClient netHandler, TextureManager textureManager) { + this.netHandler = netHandler; this.textureManager = textureManager; this.clientPlayerId = EaglerProfile.getPlayerUUID(); + reloadClientPlayerCape(); + } + + public void reloadClientPlayerCape() { + needReloadClientCape = false; this.clientPlayerCacheEntry = new CapeCacheEntry(EaglerProfile.getActiveCapeResourceLocation()); } @@ -130,20 +135,12 @@ public class ServerCapeCache { CapeCacheEntry etr = capesCache.get(player); if(etr == null) { if(!waitingCapes.containsKey(player) && !evictedCapes.containsKey(player)) { - waitingCapes.put(player, System.currentTimeMillis()); - PacketBuffer buffer; - try { - buffer = CapePackets.writeGetOtherCape(player); - }catch(IOException ex) { - logger.error("Could not write cape request packet!"); - logger.error(ex); - return defaultCacheEntry; - } - networkManager.sendPacket(new C17PacketCustomPayload("EAG|Capes-1.8", buffer)); + waitingCapes.put(player, EagRuntime.steadyTimeMillis()); + netHandler.sendEaglerMessage(new CPacketGetOtherCapeEAG(player.msb, player.lsb)); } return defaultCacheEntry; }else { - etr.lastCacheHit = System.currentTimeMillis(); + etr.lastCacheHit = EagRuntime.steadyTimeMillis(); return etr; } } @@ -183,7 +180,7 @@ public class ServerCapeCache { } public void flush() { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - lastFlushReq > 5000l) { lastFlushReq = millis; if(!waitingCapes.isEmpty()) { @@ -219,6 +216,9 @@ public class ServerCapeCache { } } } + if(needReloadClientCape) { + reloadClientPlayerCape(); + } } public void destroy() { @@ -232,7 +232,14 @@ public class ServerCapeCache { } public void evictCape(EaglercraftUUID uuid) { - evictedCapes.put(uuid, Long.valueOf(System.currentTimeMillis())); + evictedCapes.put(uuid, Long.valueOf(EagRuntime.steadyTimeMillis())); + CapeCacheEntry etr = capesCache.remove(uuid); + if(etr != null) { + etr.free(); + } + } + + public void handleInvalidate(EaglercraftUUID uuid) { CapeCacheEntry etr = capesCache.remove(uuid); if(etr != null) { etr.free(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/ServerSkinCache.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/ServerSkinCache.java index 5a88c280..f11cf4ba 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/ServerSkinCache.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/ServerSkinCache.java @@ -1,19 +1,19 @@ package net.lax1dude.eaglercraft.v1_8.profile; -import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.mojang.authlib.GameProfile; import net.lax1dude.eaglercraft.v1_8.mojang.authlib.TexturesProperty; -import net.lax1dude.eaglercraft.v1_8.socket.EaglercraftNetworkManager; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.CPacketGetOtherSkinEAG; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.CPacketGetSkinByURLEAG; +import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.client.renderer.texture.TextureManager; -import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.client.C17PacketCustomPayload; import net.minecraft.util.ResourceLocation; /** @@ -41,7 +41,7 @@ public class ServerSkinCache { protected final int presetSkinId; protected final CacheCustomSkin customSkin; - protected long lastCacheHit = System.currentTimeMillis(); + protected long lastCacheHit = EagRuntime.steadyTimeMillis(); protected SkinCacheEntry(EaglerSkinTexture textureInstance, ResourceLocation resourceLocation, SkinModel model) { this.isPresetSkin = false; @@ -125,26 +125,32 @@ public class ServerSkinCache { private final SkinCacheEntry defaultCacheEntry = new SkinCacheEntry(0); private final SkinCacheEntry defaultSlimCacheEntry = new SkinCacheEntry(1); - private final Map skinsCache = new HashMap(); - private final Map waitingSkins = new HashMap(); - private final Map evictedSkins = new HashMap(); + private final Map skinsCache = new HashMap<>(); + private final Map waitingSkins = new HashMap<>(); + private final Map evictedSkins = new HashMap<>(); - private final EaglercraftNetworkManager networkManager; + private final NetHandlerPlayClient netHandler; protected final TextureManager textureManager; private final EaglercraftUUID clientPlayerId; - private final SkinCacheEntry clientPlayerCacheEntry; + private SkinCacheEntry clientPlayerCacheEntry; - private long lastFlush = System.currentTimeMillis(); - private long lastFlushReq = System.currentTimeMillis(); - private long lastFlushEvict = System.currentTimeMillis(); + private long lastFlush = EagRuntime.steadyTimeMillis(); + private long lastFlushReq = EagRuntime.steadyTimeMillis(); + private long lastFlushEvict = EagRuntime.steadyTimeMillis(); private static int texId = 0; + public static boolean needReloadClientSkin = false; - public ServerSkinCache(EaglercraftNetworkManager networkManager, TextureManager textureManager) { - this.networkManager = networkManager; + public ServerSkinCache(NetHandlerPlayClient netHandler, TextureManager textureManager) { + this.netHandler = netHandler; this.textureManager = textureManager; this.clientPlayerId = EaglerProfile.getPlayerUUID(); + reloadClientPlayerSkin(); + } + + public void reloadClientPlayerSkin() { + needReloadClientSkin = false; this.clientPlayerCacheEntry = new SkinCacheEntry(EaglerProfile.getActiveSkinResourceLocation(), EaglerProfile.getActiveSkinModel()); } @@ -184,20 +190,12 @@ public class ServerSkinCache { SkinCacheEntry etr = skinsCache.get(player); if(etr == null) { if(!waitingSkins.containsKey(player) && !evictedSkins.containsKey(player)) { - waitingSkins.put(player, new WaitingSkin(System.currentTimeMillis(), null)); - PacketBuffer buffer; - try { - buffer = SkinPackets.writeGetOtherSkin(player); - }catch(IOException ex) { - logger.error("Could not write skin request packet!"); - logger.error(ex); - return defaultCacheEntry; - } - networkManager.sendPacket(new C17PacketCustomPayload("EAG|Skins-1.8", buffer)); + waitingSkins.put(player, new WaitingSkin(EagRuntime.steadyTimeMillis(), null)); + netHandler.sendEaglerMessage(new CPacketGetOtherSkinEAG(player.msb, player.lsb)); } return defaultCacheEntry; }else { - etr.lastCacheHit = System.currentTimeMillis(); + etr.lastCacheHit = EagRuntime.steadyTimeMillis(); return etr; } } @@ -209,20 +207,12 @@ public class ServerSkinCache { EaglercraftUUID generatedUUID = SkinPackets.createEaglerURLSkinUUID(url); SkinCacheEntry etr = skinsCache.get(generatedUUID); if(etr != null) { - etr.lastCacheHit = System.currentTimeMillis(); + etr.lastCacheHit = EagRuntime.steadyTimeMillis(); return etr; }else { if(!waitingSkins.containsKey(generatedUUID) && !evictedSkins.containsKey(generatedUUID)) { - waitingSkins.put(generatedUUID, new WaitingSkin(System.currentTimeMillis(), skinModelResponse)); - PacketBuffer buffer; - try { - buffer = SkinPackets.writeGetSkinByURL(generatedUUID, url); - }catch(IOException ex) { - logger.error("Could not write skin request packet!"); - logger.error(ex); - return skinModelResponse == SkinModel.ALEX ? defaultSlimCacheEntry : defaultCacheEntry; - } - networkManager.sendPacket(new C17PacketCustomPayload("EAG|Skins-1.8", buffer)); + waitingSkins.put(generatedUUID, new WaitingSkin(EagRuntime.steadyTimeMillis(), skinModelResponse)); + netHandler.sendEaglerMessage(new CPacketGetSkinByURLEAG(generatedUUID.msb, generatedUUID.lsb, url)); } } return skinModelResponse == SkinModel.ALEX ? defaultSlimCacheEntry : defaultCacheEntry; @@ -276,13 +266,13 @@ public class ServerSkinCache { } public void flush() { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - lastFlushReq > 5000l) { lastFlushReq = millis; if(!waitingSkins.isEmpty()) { Iterator waitingItr = waitingSkins.values().iterator(); while(waitingItr.hasNext()) { - if(millis - waitingItr.next().timeout > 30000l) { + if(millis - waitingItr.next().timeout > 20000l) { waitingItr.remove(); } } @@ -312,6 +302,9 @@ public class ServerSkinCache { } } } + if(needReloadClientSkin) { + reloadClientPlayerSkin(); + } } public void destroy() { @@ -325,7 +318,14 @@ public class ServerSkinCache { } public void evictSkin(EaglercraftUUID uuid) { - evictedSkins.put(uuid, Long.valueOf(System.currentTimeMillis())); + evictedSkins.put(uuid, Long.valueOf(EagRuntime.steadyTimeMillis())); + SkinCacheEntry etr = skinsCache.remove(uuid); + if(etr != null) { + etr.free(); + } + } + + public void handleInvalidate(EaglercraftUUID uuid) { SkinCacheEntry etr = skinsCache.remove(uuid); if(etr != null) { etr.free(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinModel.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinModel.java index 735deea5..541c51e9 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinModel.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinModel.java @@ -32,7 +32,7 @@ public enum SkinModel { public final HighPolySkin highPoly; public static final SkinModel[] skinModels = new SkinModel[8]; - private static final Map skinModelsByName = new HashMap(); + private static final Map skinModelsByName = new HashMap<>(); private SkinModel(int id, int w, int h, String profileSkinType, boolean sanitize) { this.id = id; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinPackets.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinPackets.java index 7616c6b0..b91d7013 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinPackets.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinPackets.java @@ -1,12 +1,8 @@ package net.lax1dude.eaglercraft.v1_8.profile; -import java.io.IOException; - import net.lax1dude.eaglercraft.v1_8.ArrayUtils; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; import net.lax1dude.eaglercraft.v1_8.crypto.MD5Digest; -import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; -import net.minecraft.network.PacketBuffer; /** * Copyright (c) 2022-2023 lax1dude, ayunami2000. All Rights Reserved. @@ -27,97 +23,33 @@ public class SkinPackets { public static final int PACKET_MY_SKIN_PRESET = 0x01; public static final int PACKET_MY_SKIN_CUSTOM = 0x02; - public static final int PACKET_GET_OTHER_SKIN = 0x03; - public static final int PACKET_OTHER_SKIN_PRESET = 0x04; - public static final int PACKET_OTHER_SKIN_CUSTOM = 0x05; - public static final int PACKET_GET_SKIN_BY_URL = 0x06; - public static final int PACKET_INSTALL_NEW_SKIN = 0x07; - - public static void readPluginMessage(PacketBuffer buffer, ServerSkinCache skinCache) throws IOException { - try { - int type = (int)buffer.readByte() & 0xFF; - switch(type) { - case PACKET_OTHER_SKIN_PRESET: { - EaglercraftUUID responseUUID = buffer.readUuid(); - int responsePreset = buffer.readInt(); - if(buffer.isReadable()) { - throw new IOException("PACKET_OTHER_SKIN_PRESET had " + buffer.readableBytes() + " remaining bytes!"); - } - skinCache.cacheSkinPreset(responseUUID, responsePreset); - break; - } - case PACKET_OTHER_SKIN_CUSTOM: { - EaglercraftUUID responseUUID = buffer.readUuid(); - int model = (int)buffer.readByte() & 0xFF; - SkinModel modelId; - if(model == (byte)0xFF) { - modelId = skinCache.getRequestedSkinType(responseUUID); - }else { - modelId = SkinModel.getModelFromId(model & 0x7F); - if((model & 0x80) != 0 && modelId.sanitize) { - modelId = SkinModel.STEVE; - } - } - if(modelId.highPoly != null) { - modelId = SkinModel.STEVE; - } - int bytesToRead = modelId.width * modelId.height * 4; - byte[] readSkin = new byte[bytesToRead]; - buffer.readBytes(readSkin); - if(buffer.isReadable()) { - throw new IOException("PACKET_MY_SKIN_CUSTOM had " + buffer.readableBytes() + " remaining bytes!"); - } - skinCache.cacheSkinCustom(responseUUID, readSkin, modelId); - break; - } - default: - throw new IOException("Unknown skin packet type: " + type); - } - }catch(IOException ex) { - throw ex; - }catch(Throwable t) { - throw new IOException("Failed to parse skin packet!", t); - } - } public static byte[] writeMySkinPreset(int skinId) { return new byte[] { (byte) PACKET_MY_SKIN_PRESET, (byte) (skinId >>> 24), (byte) (skinId >>> 16), (byte) (skinId >>> 8), (byte) (skinId & 0xFF) }; } - public static byte[] writeMySkinCustom(CustomSkin customSkin) { - byte[] packet = new byte[2 + customSkin.texture.length]; + public static byte[] writeMySkinCustomV3(CustomSkin customSkin) { + byte[] packet = new byte[2 + 16384]; packet[0] = (byte) PACKET_MY_SKIN_CUSTOM; packet[1] = (byte) customSkin.model.id; - System.arraycopy(customSkin.texture, 0, packet, 2, customSkin.texture.length); + System.arraycopy(customSkin.texture, 0, packet, 2, 16384); return packet; } - public static PacketBuffer writeCreateCustomSkull(byte[] customSkin) { - int len = 3 + customSkin.length; - PacketBuffer ret = new PacketBuffer(Unpooled.buffer(len, len)); - ret.writeByte(PACKET_INSTALL_NEW_SKIN); - ret.writeShort(customSkin.length); - ret.writeBytes(customSkin); - return ret; - } - - public static PacketBuffer writeGetOtherSkin(EaglercraftUUID skinId) throws IOException { - PacketBuffer ret = new PacketBuffer(Unpooled.buffer(17, 17)); - ret.writeByte(PACKET_GET_OTHER_SKIN); - ret.writeUuid(skinId); - return ret; - } - - public static PacketBuffer writeGetSkinByURL(EaglercraftUUID skinId, String skinUrl) throws IOException { - int len = 19 + skinUrl.length(); - PacketBuffer ret = new PacketBuffer(Unpooled.buffer(len, len)); - ret.writeByte(PACKET_GET_SKIN_BY_URL); - ret.writeUuid(skinId); - byte[] url = ArrayUtils.asciiString(skinUrl); - ret.writeShort((int)url.length); - ret.writeBytes(url); - return ret; + public static byte[] writeMySkinCustomV4(CustomSkin customSkin) { + byte[] packet = new byte[2 + 12288]; + packet[0] = (byte) PACKET_MY_SKIN_CUSTOM; + packet[1] = (byte) customSkin.model.id; + byte[] v3data = customSkin.texture; + for(int i = 0, j, k; i < 4096; ++i) { + j = i << 2; + k = i * 3 + 2; + packet[k] = v3data[j + 1]; + packet[k + 1] = v3data[j + 2]; + packet[k + 2] = (byte)((v3data[j + 3] >>> 1) | (v3data[j] & 0x80)); + } + return packet; } public static EaglercraftUUID createEaglerURLSkinUUID(String skinUrl){ diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinPreviewRenderer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinPreviewRenderer.java index 36a20895..999c396a 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinPreviewRenderer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/profile/SkinPreviewRenderer.java @@ -1,5 +1,6 @@ package net.lax1dude.eaglercraft.v1_8.profile; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.opengl.EaglerMeshLoader; import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; @@ -98,7 +99,7 @@ public class SkinPreviewRenderer { Minecraft.getMinecraft().getTextureManager().bindTexture(skinTexture); } - model.render(null, 0.0f, 0.0f, (float)(System.currentTimeMillis() % 2000000) / 50f, ((x - mx) * 0.06f), ((y - my) * -0.1f), 0.0625f); + model.render(null, 0.0f, 0.0f, (float)(EagRuntime.steadyTimeMillis() % 2000000) / 50f, ((x - mx) * 0.06f), ((y - my) * -0.1f), 0.0625f); if(capeTexture != null && model instanceof ModelPlayer) { Minecraft.getMinecraft().getTextureManager().bindTexture(capeTexture); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/EnumScreenRecordingCodec.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/EnumScreenRecordingCodec.java new file mode 100644 index 00000000..3c1b4d95 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/EnumScreenRecordingCodec.java @@ -0,0 +1,152 @@ +package net.lax1dude.eaglercraft.v1_8.recording; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public enum EnumScreenRecordingCodec { + + CODEC_MP4_H264_GENERIC_AAC("MP4 (video: H.264 Default, audio: AAC LC)", "mp4", "video/mp4", "avc1", "mp4a.40.2", false), + CODEC_MP4_H264_GENERIC_OPUS("MP4 (video: H.264 Default, audio: Opus)", "mp4", "video/mp4", "avc1", "opus", false), + CODEC_MP4_H264_L40_AAC("MP4 (video: H.264 CBP L4.0, audio: AAC LC)", "mp4", "video/mp4", "avc1.424028", "mp4a.40.2", true), + CODEC_MP4_H264_L40_OPUS("MP4 (video: H.264 CBP L4.0, audio: Opus)", "mp4", "video/mp4", "avc1.424028", "opus", true), + CODEC_MP4_H264_L42_AAC("MP4 (video: H.264 CBP L4.2, audio: AAC LC)", "mp4", "video/mp4", "avc1.42402A", "mp4a.40.2", true), + CODEC_MP4_H264_L42_OPUS("MP4 (video: H.264 CBP L4.2, audio: Opus)", "mp4", "video/mp4", "avc1.42402A", "opus", true), + CODEC_MP4_H264_L50_AAC("MP4 (video: H.264 CBP L5.0, audio: AAC LC)", "mp4", "video/mp4", "avc1.424032", "mp4a.40.2", true), + CODEC_MP4_H264_L50_OPUS("MP4 (video: H.264 CBP L5.0, audio: Opus)", "mp4", "video/mp4", "avc1.424032", "opus", true), + CODEC_MP4_H264_L52_AAC("MP4 (video: H.264 CBP L5.2, audio: AAC LC)", "mp4", "video/mp4", "avc1.424034", "mp4a.40.2", true), + CODEC_MP4_H264_L52_OPUS("MP4 (video: H.264 CBP L5.2, audio: Opus)", "mp4", "video/mp4", "avc1.424034", "opus", true), + + CODEC_MP4_VP9_GENERIC_AAC("MP4 (video: VP9 Default, audio: AAC LC)", "mp4", "video/mp4", "vp9", "mp4a.40.2", false), + CODEC_MP4_VP9_GENERIC_OPUS("MP4 (video: VP9 Default, audio: Opus)", "mp4", "video/mp4", "vp9", "opus", false), + CODEC_MP4_VP9_L40_AAC("MP4 (video: VP9 8-bit L4.0, audio: AAC LC)", "mp4", "video/mp4", "vp9.00.40.08", "mp4a.40.2", true), + CODEC_MP4_VP9_L40_OPUS("MP4 (video: VP9 8-bit L4.0, audio: Opus)", "mp4", "video/mp4", "vp9.00.40.08", "opus", true), + CODEC_MP4_VP9_L41_AAC("MP4 (video: VP9 8-bit L4.1, audio: AAC LC)", "mp4", "video/mp4", "vp9.00.41.08", "mp4a.40.2", true), + CODEC_MP4_VP9_L41_OPUS("MP4 (video: VP9 8-bit L4.1, audio: Opus)", "mp4", "video/mp4", "vp9.00.41.08", "opus", true), + CODEC_MP4_VP9_L50_AAC("MP4 (video: VP9 8-bit L5.0, audio: AAC LC)", "mp4", "video/mp4", "vp9.00.50.08", "mp4a.40.2", true), + CODEC_MP4_VP9_L50_OPUS("MP4 (video: VP9 8-bit L5.0, audio: Opus)", "mp4", "video/mp4", "vp9.00.50.08", "opus", true), + CODEC_MP4_VP9_L51_AAC("MP4 (video: VP9 8-bit L5.1, audio: AAC LC)", "mp4", "video/mp4", "vp9.00.51.08", "mp4a.40.2", true), + CODEC_MP4_VP9_L51_OPUS("MP4 (video: VP9 8-bit L5.1, audio: Opus)", "mp4", "video/mp4", "vp9.00.51.08", "opus", true), + + CODEC_MP4_GENERIC("MP4 (Default Codecs)", "mp4", "video/mp4", null, null, false), + + CODEC_WEBM_H264_GENERIC_OPUS("WEBM (video: H.264 Default, audio: Opus)", "webm", "video/webm", "avc1", "opus", false), + CODEC_WEBM_H264_GENERIC_VORBIS("WEBM (video: H.264 Default, audio: Vorbis)", "webm", "video/webm", "avc1", "vorbis", false), + CODEC_WEBM_H264_L40_OPUS("WEBM (video: H.264 CBP L4.0, audio: Opus)", "webm", "video/webm", "avc1.424028", "opus", true), + CODEC_WEBM_H264_L40_VORBIS("WEBM (video: H.264 CBP L4.0, audio: Vorbis)", "webm", "video/webm", "avc1.424028", "vorbis", true), + CODEC_WEBM_H264_L42_OPUS("WEBM (video: H.264 CBP L4.2, audio: Opus)", "webm", "video/webm", "avc1.42402A", "opus", true), + CODEC_WEBM_H264_L42_VORBIS("WEBM (video: H.264 CBP L4.2, audio: Vorbis)", "webm", "video/webm", "avc1.42402A", "vorbis", true), + CODEC_WEBM_H264_L50_OPUS("WEBM (video: H.264 CBP L5.0, audio: Opus)", "webm", "video/webm", "avc1.424032", "opus", true), + CODEC_WEBM_H264_L50_VORBIS("WEBM (video: H.264 CBP L5.0, audio: Vorbis)", "webm", "video/webm", "avc1.424032", "vorbis", true), + CODEC_WEBM_H264_L52_OPUS("WEBM (video: H.264 CBP L5.2, audio: Opus)", "webm", "video/webm", "avc1.424034", "opus", true), + CODEC_WEBM_H264_L52_VORBIS("WEBM (video: H.264 CBP L5.2, audio: Vorbis)", "webm", "video/webm", "avc1.424034", "vorbis", true), + + CODEC_WEBM_VP9_GENERIC_OPUS("WEBM (video: VP9 Default, audio: Opus)", "webm", "video/webm", "vp9", "opus", false), + CODEC_WEBM_VP9_GENERIC_VORBIS("WEBM (video: VP9 Default, audio: Vorbis)", "webm", "video/webm", "vp9", "vorbis", false), + CODEC_WEBM_VP9_L40_OPUS("WEBM (video: VP9 8-bit L4.0, audio: Opus)", "webm", "video/webm", "vp9.00.40.08", "opus", true), + CODEC_WEBM_VP9_L40_VORBIS("WEBM (video: VP9 8-bit L4.0, audio: Vorbis)", "webm", "video/webm", "vp9.00.40.08", "vorbis", true), + CODEC_WEBM_VP9_L41_OPUS("WEBM (video: VP9 8-bit L4.1, audio: Opus)", "webm", "video/webm", "vp9.00.41.08", "opus", true), + CODEC_WEBM_VP9_L41_VORBIS("WEBM (video: VP9 8-bit L4.1, audio: Vorbis)", "webm", "video/webm", "vp9.00.41.08", "vorbis", true), + CODEC_WEBM_VP9_L50_OPUS("WEBM (video: VP9 8-bit L5.0, audio: Opus)", "webm", "video/webm", "vp9.00.50.08", "opus", true), + CODEC_WEBM_VP9_L50_VORBIS("WEBM (video: VP9 8-bit L5.0, audio: Vorbis)", "webm", "video/webm", "vp9.00.50.08", "vorbis", true), + CODEC_WEBM_VP9_L51_OPUS("WEBM (video: VP9 8-bit L5.1, audio: Opus)", "webm", "video/webm", "vp9.00.51.08", "opus", true), + CODEC_WEBM_VP9_L51_VORBIS("WEBM (video: VP9 8-bit L5.1, audio: Vorbis)", "webm", "video/webm", "vp9.00.51.08", "vorbis", true), + + CODEC_WEBM_VP8_GENERIC_OPUS("WEBM (video: VP8 Default, audio: Opus)", "webm", "video/webm", "vp8", "opus", false), + CODEC_WEBM_VP8_GENERIC_VORBIS("WEBM (video: VP8 Default, audio: Vorbis)", "webm", "video/webm", "vp8", "vorbis", false), + + CODEC_WEBM_GENERIC("WEBM (Default Codecs)", "webm", "video/webm", null, null, false); + + public static final EnumScreenRecordingCodec[] preferred_codec_order = new EnumScreenRecordingCodec[] { + CODEC_MP4_H264_GENERIC_AAC, + CODEC_MP4_H264_L52_AAC, + CODEC_MP4_H264_L50_AAC, + CODEC_MP4_H264_L42_AAC, + CODEC_MP4_H264_L40_AAC, + CODEC_MP4_H264_GENERIC_OPUS, + CODEC_MP4_H264_L40_OPUS, + CODEC_MP4_H264_L42_OPUS, + CODEC_MP4_H264_L50_OPUS, + CODEC_MP4_H264_L52_OPUS, + CODEC_WEBM_H264_GENERIC_OPUS, + CODEC_WEBM_H264_L52_OPUS, + CODEC_WEBM_H264_L50_OPUS, + CODEC_WEBM_H264_L42_OPUS, + CODEC_WEBM_H264_L40_OPUS, + CODEC_WEBM_H264_GENERIC_VORBIS, + CODEC_WEBM_H264_L52_VORBIS, + CODEC_WEBM_H264_L50_VORBIS, + CODEC_WEBM_H264_L42_VORBIS, + CODEC_WEBM_H264_L40_VORBIS, + CODEC_MP4_VP9_GENERIC_AAC, + CODEC_MP4_VP9_L51_AAC, + CODEC_MP4_VP9_L50_AAC, + CODEC_MP4_VP9_L41_AAC, + CODEC_MP4_VP9_L40_AAC, + CODEC_MP4_VP9_GENERIC_OPUS, + CODEC_MP4_VP9_L51_OPUS, + CODEC_MP4_VP9_L50_OPUS, + CODEC_MP4_VP9_L41_OPUS, + CODEC_MP4_VP9_L40_OPUS, + CODEC_WEBM_VP9_GENERIC_OPUS, + CODEC_WEBM_VP9_L51_OPUS, + CODEC_WEBM_VP9_L50_OPUS, + CODEC_WEBM_VP9_L41_OPUS, + CODEC_WEBM_VP9_L40_OPUS, + CODEC_WEBM_VP9_GENERIC_VORBIS, + CODEC_WEBM_VP9_L51_VORBIS, + CODEC_WEBM_VP9_L50_VORBIS, + CODEC_WEBM_VP9_L41_VORBIS, + CODEC_WEBM_VP9_L40_VORBIS, + CODEC_WEBM_VP8_GENERIC_OPUS, + CODEC_WEBM_VP8_GENERIC_VORBIS, + CODEC_MP4_GENERIC, + CODEC_WEBM_GENERIC + }; + + public final String name; + public final String fileExt; + public final String container; + public final String videoCodec; + public final String audioCodec; + public final boolean advanced; + public final String mimeType; + + private EnumScreenRecordingCodec(String name, String fileExt, String container, String videoCodec, String audioCodec, boolean advanced) { + this.name = name; + this.fileExt = fileExt; + this.container = container; + this.videoCodec = videoCodec; + this.audioCodec = audioCodec; + this.advanced = advanced; + if(videoCodec != null || audioCodec != null) { + StringBuilder mimeBuilder = new StringBuilder(container); + mimeBuilder.append(";codecs=\""); + if(videoCodec != null) { + mimeBuilder.append(videoCodec); + if(audioCodec != null) { + mimeBuilder.append(','); + } + } + if(audioCodec != null) { + mimeBuilder.append(audioCodec); + } + mimeBuilder.append("\""); + this.mimeType = mimeBuilder.toString(); + }else { + this.mimeType = container; + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenRecordingNote.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenRecordingNote.java new file mode 100644 index 00000000..1dc251a5 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenRecordingNote.java @@ -0,0 +1,52 @@ +package net.lax1dude.eaglercraft.v1_8.recording; + +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenRecordingNote extends GuiScreen { + + private GuiScreen cont; + + public static boolean hasShown = false; + + public GuiScreenRecordingNote(GuiScreen cont) { + this.cont = cont; + } + + public void initGui() { + this.buttonList.clear(); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 108, I18n.format("gui.done"))); + } + + public void drawScreen(int par1, int par2, float par3) { + this.drawDefaultBackground(); + this.drawCenteredString(fontRendererObj, I18n.format("options.recordingNote.title"), this.width / 2, 70, 11184810); + this.drawCenteredString(fontRendererObj, I18n.format("options.recordingNote.text0"), this.width / 2, 90, 16777215); + this.drawCenteredString(fontRendererObj, I18n.format("options.recordingNote.text1"), this.width / 2, 102, 16777215); + super.drawScreen(par1, par2, par3); + } + + protected void actionPerformed(GuiButton par1GuiButton) { + if(par1GuiButton.id == 0) { + hasShown = true; + this.mc.displayGuiScreen(cont); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenRecordingSettings.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenRecordingSettings.java new file mode 100644 index 00000000..096a2de0 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenRecordingSettings.java @@ -0,0 +1,201 @@ +package net.lax1dude.eaglercraft.v1_8.recording; + +import net.lax1dude.eaglercraft.v1_8.HString; +import net.lax1dude.eaglercraft.v1_8.internal.ScreenRecordParameters; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.minecraft.GuiScreenGenericErrorMessage; +import net.lax1dude.eaglercraft.v1_8.sp.gui.GuiSlider2; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.MathHelper; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenRecordingSettings extends GuiScreen { + + private static final Logger logger = LogManager.getLogger("GuiScreenRecordingSettings"); + + protected final GuiScreen parent; + + protected GuiButton recordButton; + protected GuiButton codecButton; + protected GuiSlider2 videoResolutionSlider; + protected GuiSlider2 videoFrameRateSlider; + protected GuiSlider2 audioBitrateSlider; + protected GuiSlider2 videoBitrateSlider; + protected GuiSlider2 microphoneVolumeSlider; + protected GuiSlider2 gameVolumeSlider; + protected boolean dirty = false; + + public GuiScreenRecordingSettings(GuiScreen parent) { + this.parent = parent; + } + + public void initGui() { + buttonList.clear(); + buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 168, I18n.format("gui.done"))); + buttonList.add(codecButton = new GuiButton(1, this.width / 2 + 65, this.height / 6 - 2, 75, 20, I18n.format("options.screenRecording.codecButton"))); + boolean isRecording = ScreenRecordingController.isRecording(); + buttonList.add(recordButton = new GuiButton(2, this.width / 2 + 15, this.height / 6 + 28, 125, 20, + I18n.format(isRecording ? "options.screenRecording.stop" : "options.screenRecording.start"))); + buttonList.add(videoResolutionSlider = new GuiSlider2(3, this.width / 2 - 155, this.height / 6 + 64, 150, 20, (mc.gameSettings.screenRecordResolution - 1) / 3.999f, 1.0f) { + @Override + protected String updateDisplayString() { + int i = (int)(sliderValue * 3.999f); + return I18n.format("options.screenRecording.videoResolution") + ": x" + HString.format("%.2f", 1.0f / (int)Math.pow(2.0, i)); + } + @Override + protected void onChange() { + mc.gameSettings.screenRecordResolution = 1 + (int)(sliderValue * 3.999f); + dirty = true; + } + }); + buttonList.add(videoFrameRateSlider = new GuiSlider2(4, this.width / 2 + 5, this.height / 6 + 64, 150, 20, (Math.max(mc.gameSettings.screenRecordFPS, 9) - 9) / 51.999f, 1.0f) { + @Override + protected String updateDisplayString() { + int i = (int)(sliderValue * 51.999f); + return I18n.format("options.screenRecording.videoFPS") + ": " + (i <= 0 ? I18n.format("options.screenRecording.onVSync") : 9 + i); + } + @Override + protected void onChange() { + int i = (int)(sliderValue * 51.999f); + mc.gameSettings.screenRecordFPS = i <= 0 ? -1 : 9 + i; + dirty = true; + } + }); + buttonList.add(videoBitrateSlider = new GuiSlider2(5, this.width / 2 - 155, this.height / 6 + 98, 150, 20, MathHelper.sqrt_float(MathHelper.clamp_float((mc.gameSettings.screenRecordVideoBitrate - 250) / 19750.999f, 0.0f, 1.0f)), 1.0f) { + @Override + protected String updateDisplayString() { + return I18n.format("options.screenRecording.videoBitrate") + ": " + (250 + (int)(sliderValue * sliderValue * 19750.999f)) + "kbps"; + } + @Override + protected void onChange() { + mc.gameSettings.screenRecordVideoBitrate = 250 + (int)(sliderValue * sliderValue * 19750.999f); + dirty = true; + } + }); + buttonList.add(audioBitrateSlider = new GuiSlider2(6, this.width / 2 + 5, this.height / 6 + 98, 150, 20, MathHelper.sqrt_float(MathHelper.clamp_float((mc.gameSettings.screenRecordAudioBitrate - 24) / 232.999f, 0.0f, 1.0f)), 1.0f) { + @Override + protected String updateDisplayString() { + return I18n.format("options.screenRecording.audioBitrate") + ": " + (24 + (int)(sliderValue * sliderValue * 232.999f)) + "kbps"; + } + @Override + protected void onChange() { + mc.gameSettings.screenRecordAudioBitrate = 24 + (int)(sliderValue * sliderValue * 232.999f); + dirty = true; + } + }); + buttonList.add(gameVolumeSlider = new GuiSlider2(7, this.width / 2 - 155, this.height / 6 + 130, 150, 20, mc.gameSettings.screenRecordGameVolume, 1.0f) { + @Override + protected String updateDisplayString() { + return I18n.format("options.screenRecording.gameVolume") + ": " + (int)(sliderValue * 100.999f) + "%"; + } + @Override + protected void onChange() { + mc.gameSettings.screenRecordGameVolume = sliderValue; + ScreenRecordingController.setGameVolume(sliderValue); + dirty = true; + } + }); + buttonList.add(microphoneVolumeSlider = new GuiSlider2(8, this.width / 2 + 5, this.height / 6 + 130, 150, 20, mc.gameSettings.screenRecordMicVolume, 1.0f) { + @Override + protected String updateDisplayString() { + return I18n.format("options.screenRecording.microphoneVolume") + ": " + (int)(sliderValue * 100.999f) + "%"; + } + @Override + protected void onChange() { + mc.gameSettings.screenRecordMicVolume = sliderValue; + ScreenRecordingController.setMicrophoneVolume(sliderValue); + dirty = true; + } + }); + codecButton.enabled = !isRecording; + videoResolutionSlider.enabled = !isRecording; + videoFrameRateSlider.enabled = !isRecording; + audioBitrateSlider.enabled = !isRecording; + videoBitrateSlider.enabled = !isRecording; + microphoneVolumeSlider.enabled = !ScreenRecordingController.isMicVolumeLocked(); + } + + protected void actionPerformed(GuiButton parGuiButton) { + if(parGuiButton.id == 0) { + if(dirty) { + mc.gameSettings.saveOptions(); + dirty = false; + } + mc.displayGuiScreen(parent); + }else if(parGuiButton.id == 1) { + mc.displayGuiScreen(new GuiScreenSelectCodec(this, mc.gameSettings.screenRecordCodec)); + }else if(parGuiButton.id == 2) { + if(!ScreenRecordingController.isRecording()) { + try { + ScreenRecordingController.startRecording(new ScreenRecordParameters(mc.gameSettings.screenRecordCodec, + mc.gameSettings.screenRecordResolution, mc.gameSettings.screenRecordVideoBitrate, + mc.gameSettings.screenRecordAudioBitrate, mc.gameSettings.screenRecordFPS)); + }catch(Throwable t) { + logger.error("Failed to begin screen recording!"); + logger.error(t); + mc.displayGuiScreen(new GuiScreenGenericErrorMessage("options.screenRecording.failed", t.toString(), parent)); + } + }else { + ScreenRecordingController.endRecording(); + } + } + } + + public void drawScreen(int i, int j, float var3) { + drawDefaultBackground(); + drawCenteredString(fontRendererObj, I18n.format("options.screenRecording.title"), this.width / 2, 15, 16777215); + if(mc.gameSettings.screenRecordCodec == null) { + mc.gameSettings.screenRecordCodec = ScreenRecordingController.getDefaultCodec(); + } + + String codecString = mc.gameSettings.screenRecordCodec.name; + int codecStringWidth = fontRendererObj.getStringWidth(codecString); + drawString(fontRendererObj, codecString, this.width / 2 + 60 - codecStringWidth, this.height / 6 + 4, 0xFFFFFF); + + boolean isRecording = ScreenRecordingController.isRecording(); + codecButton.enabled = !isRecording; + videoResolutionSlider.enabled = !isRecording; + videoFrameRateSlider.enabled = !isRecording; + audioBitrateSlider.enabled = !isRecording; + videoBitrateSlider.enabled = !isRecording; + microphoneVolumeSlider.enabled = !ScreenRecordingController.isMicVolumeLocked(); + recordButton.displayString = I18n.format(isRecording ? "options.screenRecording.stop" : "options.screenRecording.start"); + String statusString = I18n.format("options.screenRecording.status", + (isRecording ? EnumChatFormatting.GREEN : EnumChatFormatting.RED) + I18n.format(isRecording ? "options.screenRecording.status.1" : "options.screenRecording.status.0")); + int statusStringWidth = fontRendererObj.getStringWidth(statusString); + drawString(fontRendererObj, statusString, this.width / 2 + 10 - statusStringWidth, this.height / 6 + 34, 0xFFFFFF); + + super.drawScreen(i, j, var3); + } + + protected void handleCodecCallback(EnumScreenRecordingCodec codec) { + EnumScreenRecordingCodec oldCodec = mc.gameSettings.screenRecordCodec; + if(ScreenRecordingController.codecs.contains(codec)) { + mc.gameSettings.screenRecordCodec = codec; + }else { + mc.gameSettings.screenRecordCodec = ScreenRecordingController.getDefaultCodec(); + } + if(oldCodec != mc.gameSettings.screenRecordCodec) { + dirty = true; + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenSelectCodec.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenSelectCodec.java new file mode 100644 index 00000000..3b6cfaa8 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiScreenSelectCodec.java @@ -0,0 +1,92 @@ +package net.lax1dude.eaglercraft.v1_8.recording; + +import java.io.IOException; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenSelectCodec extends GuiScreen { + + protected final GuiScreenRecordingSettings parent; + protected List codecs; + protected int selectedCodec; + protected GuiSlotSelectCodec slots; + protected GuiButton showAllButton; + protected boolean showAll; + protected EnumScreenRecordingCodec codec; + + public GuiScreenSelectCodec(GuiScreenRecordingSettings parent, EnumScreenRecordingCodec codec) { + this.parent = parent; + this.codec = codec; + } + + public void initGui() { + showAll = codec.advanced; + codecs = showAll ? ScreenRecordingController.advancedCodecsOrdered : ScreenRecordingController.simpleCodecsOrdered; + selectedCodec = codecs.indexOf(codec); + buttonList.clear(); + buttonList.add(showAllButton = new GuiButton(0, this.width / 2 - 154, this.height - 38, 150, 20, + I18n.format("options.recordingCodec.showAdvancedCodecs", I18n.format(showAll ? "gui.yes" : "gui.no")))); + buttonList.add(new GuiButton(1, this.width / 2 + 4, this.height - 38, 150, 20, I18n.format("gui.done"))); + slots = new GuiSlotSelectCodec(this, 32, height - 45); + } + + protected void actionPerformed(GuiButton parGuiButton) { + if(parGuiButton.id == 0) { + changeStateShowAll(!showAll); + showAllButton.displayString = I18n.format("options.recordingCodec.showAdvancedCodecs", I18n.format(showAll ? "gui.yes" : "gui.no")); + }else if(parGuiButton.id == 1) { + if(selectedCodec >= 0 && selectedCodec < codecs.size()) { + parent.handleCodecCallback(codecs.get(selectedCodec)); + } + mc.displayGuiScreen(parent); + } + } + + protected void changeStateShowAll(boolean newShowAll) { + if(newShowAll == showAll) return; + EnumScreenRecordingCodec oldCodec = codecs.get(selectedCodec >= 0 && selectedCodec < codecs.size() ? selectedCodec : 0); + codecs = newShowAll ? ScreenRecordingController.advancedCodecsOrdered : ScreenRecordingController.simpleCodecsOrdered; + showAll = newShowAll; + selectedCodec = codecs.indexOf(oldCodec); + } + + public void drawScreen(int i, int j, float var3) { + slots.drawScreen(i, j, var3); + this.drawCenteredString(this.fontRendererObj, I18n.format("options.recordingCodec.title"), this.width / 2, 16, 16777215); + super.drawScreen(i, j, var3); + } + + public void handleMouseInput() throws IOException { + super.handleMouseInput(); + slots.handleMouseInput(); + } + + public void handleTouchInput() throws IOException { + super.handleTouchInput(); + slots.handleTouchInput(); + } + + static Minecraft getMC(GuiScreenSelectCodec screen) { + return screen.mc; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiSlotSelectCodec.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiSlotSelectCodec.java new file mode 100644 index 00000000..4521968d --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/GuiSlotSelectCodec.java @@ -0,0 +1,58 @@ +package net.lax1dude.eaglercraft.v1_8.recording; + +import net.minecraft.client.gui.GuiSlot; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiSlotSelectCodec extends GuiSlot { + + protected final GuiScreenSelectCodec screen; + + public GuiSlotSelectCodec(GuiScreenSelectCodec screen, int topIn, int bottomIn) { + super(GuiScreenSelectCodec.getMC(screen), screen.width, screen.height, topIn, bottomIn, 18); + this.screen = screen; + } + + @Override + protected int getSize() { + return screen.codecs.size(); + } + + @Override + protected void elementClicked(int var1, boolean var2, int var3, int var4) { + if(var1 < screen.codecs.size()) { + screen.selectedCodec = var1; + } + } + + @Override + protected boolean isSelected(int var1) { + return screen.selectedCodec == var1; + } + + @Override + protected void drawBackground() { + screen.drawDefaultBackground(); + } + + @Override + protected void drawSlot(int id, int xx, int yy, int width, int height, int ii) { + if(id < screen.codecs.size()) { + this.screen.drawString(mc.fontRendererObj, screen.codecs.get(id).name, xx + 4, yy + 3, 0xFFFFFF); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/ScreenRecordingController.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/ScreenRecordingController.java new file mode 100644 index 00000000..869b7dea --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/recording/ScreenRecordingController.java @@ -0,0 +1,99 @@ +package net.lax1dude.eaglercraft.v1_8.recording; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.lax1dude.eaglercraft.v1_8.internal.PlatformScreenRecord; +import net.lax1dude.eaglercraft.v1_8.internal.ScreenRecordParameters; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ScreenRecordingController { + + public static final int DEFAULT_FPS = 30; + public static final int DEFAULT_RESOLUTION = 1; + public static final int DEFAULT_AUDIO_BITRATE = 120; + public static final int DEFAULT_VIDEO_BITRATE = 2500; + public static final float DEFAULT_GAME_VOLUME = 1.0f; + public static final float DEFAULT_MIC_VOLUME = 0.0f; + + public static final List simpleCodecsOrdered = new ArrayList<>(); + public static final List advancedCodecsOrdered = new ArrayList<>(); + public static final Set codecs = new HashSet<>(); + private static boolean supported = false; + + public static void initialize() { + simpleCodecsOrdered.clear(); + advancedCodecsOrdered.clear(); + codecs.clear(); + supported = PlatformScreenRecord.isSupported(); + if(supported) { + EnumScreenRecordingCodec[] codecsOrdered = EnumScreenRecordingCodec.preferred_codec_order; + for(int i = 0; i < codecsOrdered.length; ++i) { + EnumScreenRecordingCodec codec = codecsOrdered[i]; + if(PlatformScreenRecord.isCodecSupported(codec)) { + if(!codec.advanced) { + simpleCodecsOrdered.add(codec); + } + advancedCodecsOrdered.add(codec); + codecs.add(codec); + } + } + } + if(codecs.isEmpty()) { + supported = false; + } + } + + public static boolean isSupported() { + return supported; + } + + public static void setGameVolume(float volume) { + PlatformScreenRecord.setGameVolume(volume); + } + + public static void setMicrophoneVolume(float volume) { + PlatformScreenRecord.setMicrophoneVolume(volume); + } + + public static void startRecording(ScreenRecordParameters params) { + PlatformScreenRecord.startRecording(params); + } + + public static void endRecording() { + PlatformScreenRecord.endRecording(); + } + + public static boolean isRecording() { + return PlatformScreenRecord.isRecording(); + } + + public static boolean isMicVolumeLocked() { + return PlatformScreenRecord.isMicVolumeLocked(); + } + + public static boolean isVSyncLocked() { + return PlatformScreenRecord.isVSyncLocked(); + } + + public static EnumScreenRecordingCodec getDefaultCodec() { + return simpleCodecsOrdered.isEmpty() ? (advancedCodecsOrdered.isEmpty() ? null : advancedCodecsOrdered.get(0)) : simpleCodecsOrdered.get(0); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ConnectionHandshake.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ConnectionHandshake.java index c262ee94..d1ca8978 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ConnectionHandshake.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ConnectionHandshake.java @@ -4,14 +4,22 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import net.lax1dude.eaglercraft.v1_8.ArrayUtils; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EaglerInputStream; import net.lax1dude.eaglercraft.v1_8.EaglerOutputStream; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; import net.lax1dude.eaglercraft.v1_8.EaglercraftVersion; +import net.lax1dude.eaglercraft.v1_8.PauseMenuCustomizeState; import net.lax1dude.eaglercraft.v1_8.crypto.SHA256Digest; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformNetworking; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketClient; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketFrame; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.profile.EaglerProfile; @@ -42,20 +50,40 @@ import net.minecraft.util.IChatComponent; */ public class ConnectionHandshake { - private static final long baseTimeout = 15000l; + private static final long baseTimeout = 10000l; private static final int protocolV2 = 2; private static final int protocolV3 = 3; + private static final int protocolV4 = 4; private static final Logger logger = LogManager.getLogger(); public static String pluginVersion = null; public static String pluginBrand = null; + public static int protocolVersion = -1; - public static boolean attemptHandshake(Minecraft mc, GuiConnecting connecting, GuiScreen ret, String password, boolean allowPlaintext) { + public static byte[] getSPHandshakeProtocolData() { try { + EaglerOutputStream bao = new EaglerOutputStream(); + DataOutputStream d = new DataOutputStream(bao); + d.writeShort(3); // supported eagler protocols count + d.writeShort(protocolV2); // client supports v2 + d.writeShort(protocolV3); // client supports v3 + d.writeShort(protocolV4); // client supports v4 + return bao.toByteArray(); + }catch(IOException ex) { + throw new RuntimeException(ex); + } + } + + public static boolean attemptHandshake(Minecraft mc, IWebSocketClient client, GuiConnecting connecting, + GuiScreen ret, String password, boolean allowPlaintext, boolean enableCookies, byte[] cookieData) { + try { + EaglerProfile.clearServerSkinOverride(); + PauseMenuCustomizeState.reset(); pluginVersion = null; pluginBrand = null; + protocolVersion = -1; EaglerOutputStream bao = new EaglerOutputStream(); DataOutputStream d = new DataOutputStream(bao); @@ -63,9 +91,7 @@ public class ConnectionHandshake { d.writeByte(2); // legacy protocol version - d.writeShort(2); // supported eagler protocols count - d.writeShort(protocolV2); // client supports v2 - d.writeShort(protocolV3); // client supports v3 + d.write(getSPHandshakeProtocolData()); // write supported eagler protocol versions d.writeShort(1); // supported game protocols count d.writeShort(47); // client supports 1.8 protocol @@ -84,9 +110,9 @@ public class ConnectionHandshake { d.writeByte(username.length()); d.writeBytes(username); - PlatformNetworking.writePlayPacket(bao.toByteArray()); + client.send(bao.toByteArray()); - byte[] read = awaitNextPacket(baseTimeout); + byte[] read = awaitNextPacket(client, baseTimeout); if(read == null) { logger.error("Read timed out while waiting for server protocol response!"); return false; @@ -115,7 +141,7 @@ public class ConnectionHandshake { games.append("mc").append(di.readShort()); } - logger.info("Incompatible client: v2 & mc47"); + logger.info("Incompatible client: v2/v3/v4 & mc47"); logger.info("Server supports: {}", protocols); logger.info("Server supports: {}", games); @@ -128,11 +154,11 @@ public class ConnectionHandshake { return false; }else if(type == HandshakePacketTypes.PROTOCOL_SERVER_VERSION) { - int serverVers = di.readShort(); + protocolVersion = di.readShort(); - if(serverVers != protocolV2 && serverVers != protocolV3) { - logger.info("Incompatible server version: {}", serverVers); - mc.displayGuiScreen(new GuiDisconnected(ret, "connect.failed", new ChatComponentText(serverVers < protocolV2 ? "Outdated Server" : "Outdated Client"))); + if(protocolVersion != protocolV2 && protocolVersion != protocolV3 && protocolVersion != protocolV4) { + logger.info("Incompatible server version: {}", protocolVersion); + mc.displayGuiScreen(new GuiDisconnected(ret, "connect.failed", new ChatComponentText(protocolVersion < protocolV2 ? "Outdated Server" : "Outdated Client"))); return false; } @@ -143,7 +169,7 @@ public class ConnectionHandshake { return false; } - logger.info("Server protocol: {}", serverVers); + logger.info("Server protocol: {}", protocolVersion); int msgLen = di.read(); byte[] dat = new byte[msgLen]; @@ -260,10 +286,19 @@ public class ConnectionHandshake { }else { d.writeByte(0); } + if(protocolVersion >= protocolV4) { + d.writeBoolean(enableCookies); + if(enableCookies && cookieData != null) { + d.writeByte(cookieData.length); + d.write(cookieData); + }else { + d.writeByte(0); + } + } - PlatformNetworking.writePlayPacket(bao.toByteArray()); + client.send(bao.toByteArray()); - read = awaitNextPacket(baseTimeout); + read = awaitNextPacket(client, baseTimeout); if(read == null) { logger.error("Read timed out while waiting for login negotiation response!"); return false; @@ -280,52 +315,79 @@ public class ConnectionHandshake { Minecraft.getMinecraft().getSession().update(serverUsername, new EaglercraftUUID(di.readLong(), di.readLong())); - bao.reset(); - d.writeByte(HandshakePacketTypes.PROTOCOL_CLIENT_PROFILE_DATA); - String profileDataType = "skin_v1"; - d.writeByte(profileDataType.length()); - d.writeBytes(profileDataType); - byte[] packetSkin = EaglerProfile.getSkinPacket(); + Map profileDataToSend = new HashMap<>(); + + if(protocolVersion >= 4) { + bao.reset(); + d.writeLong(EaglercraftVersion.clientBrandUUID.msb); + d.writeLong(EaglercraftVersion.clientBrandUUID.lsb); + profileDataToSend.put("brand_uuid_v1", bao.toByteArray()); + } + + byte[] packetSkin = EaglerProfile.getSkinPacket(protocolVersion); if(packetSkin.length > 0xFFFF) { throw new IOException("Skin packet is too long: " + packetSkin.length); } - d.writeShort(packetSkin.length); - d.write(packetSkin); - PlatformNetworking.writePlayPacket(bao.toByteArray()); + profileDataToSend.put(protocolVersion >= 4 ? "skin_v2" : "skin_v1", packetSkin); - bao.reset(); - d.writeByte(HandshakePacketTypes.PROTOCOL_CLIENT_PROFILE_DATA); - profileDataType = "cape_v1"; - d.writeByte(profileDataType.length()); - d.writeBytes(profileDataType); byte[] packetCape = EaglerProfile.getCapePacket(); if(packetCape.length > 0xFFFF) { throw new IOException("Cape packet is too long: " + packetCape.length); } - d.writeShort(packetCape.length); - d.write(packetCape); - PlatformNetworking.writePlayPacket(bao.toByteArray()); + profileDataToSend.put("cape_v1", packetCape); byte[] packetSignatureData = UpdateService.getClientSignatureData(); if(packetSignatureData != null) { - bao.reset(); - d.writeByte(HandshakePacketTypes.PROTOCOL_CLIENT_PROFILE_DATA); - profileDataType = "update_cert_v1"; - d.writeByte(profileDataType.length()); - d.writeBytes(profileDataType); - if(packetSignatureData.length > 0xFFFF) { - throw new IOException("Update certificate login packet is too long: " + packetSignatureData.length); + profileDataToSend.put("update_cert_v1", packetSignatureData); + } + + if(protocolVersion >= 4) { + List> toSend = new ArrayList<>(profileDataToSend.entrySet()); + while(!toSend.isEmpty()) { + int sendLen = 2; + bao.reset(); + d.writeByte(HandshakePacketTypes.PROTOCOL_CLIENT_PROFILE_DATA); + d.writeByte(0); // will be replaced + int packetCount = 0; + while(!toSend.isEmpty() && packetCount < 255) { + Entry etr = toSend.get(toSend.size() - 1); + int i = 3 + etr.getKey().length() + etr.getValue().length; + if(sendLen + i < 0xFF00) { + String profileDataType = etr.getKey(); + d.writeByte(profileDataType.length()); + d.writeBytes(profileDataType); + byte[] data = etr.getValue(); + d.writeShort(data.length); + d.write(data); + toSend.remove(toSend.size() - 1); + ++packetCount; + }else { + break; + } + } + byte[] send = bao.toByteArray(); + send[1] = (byte)packetCount; + client.send(send); + } + }else { + for(Entry etr : profileDataToSend.entrySet()) { + bao.reset(); + d.writeByte(HandshakePacketTypes.PROTOCOL_CLIENT_PROFILE_DATA); + String profileDataType = etr.getKey(); + d.writeByte(profileDataType.length()); + d.writeBytes(profileDataType); + byte[] data = etr.getValue(); + d.writeShort(data.length); + d.write(data); + client.send(bao.toByteArray()); } - d.writeShort(packetSignatureData.length); - d.write(packetSignatureData); - PlatformNetworking.writePlayPacket(bao.toByteArray()); } bao.reset(); d.writeByte(HandshakePacketTypes.PROTOCOL_CLIENT_FINISH_LOGIN); - PlatformNetworking.writePlayPacket(bao.toByteArray()); + client.send(bao.toByteArray()); - read = awaitNextPacket(baseTimeout); + read = awaitNextPacket(client, baseTimeout); if(read == null) { logger.error("Read timed out while waiting for login confirmation response!"); return false; @@ -336,13 +398,13 @@ public class ConnectionHandshake { if(type == HandshakePacketTypes.PROTOCOL_SERVER_FINISH_LOGIN) { return true; }else if(type == HandshakePacketTypes.PROTOCOL_SERVER_ERROR) { - showError(mc, connecting, ret, di, serverVers == protocolV2); + showError(mc, client, connecting, ret, di, protocolVersion == protocolV2); return false; }else { return false; } }else if(type == HandshakePacketTypes.PROTOCOL_SERVER_DENY_LOGIN) { - if(serverVers == protocolV2) { + if(protocolVersion == protocolV2) { msgLen = di.read(); }else { msgLen = di.readUnsignedShort(); @@ -353,13 +415,13 @@ public class ConnectionHandshake { mc.displayGuiScreen(new GuiDisconnected(ret, "connect.failed", IChatComponent.Serializer.jsonToComponent(errStr))); return false; }else if(type == HandshakePacketTypes.PROTOCOL_SERVER_ERROR) { - showError(mc, connecting, ret, di, serverVers == protocolV2); + showError(mc, client, connecting, ret, di, protocolVersion == protocolV2); return false; }else { return false; } }else if(type == HandshakePacketTypes.PROTOCOL_SERVER_ERROR) { - showError(mc, connecting, ret, di, true); + showError(mc, client, connecting, ret, di, true); return false; }else { return false; @@ -372,37 +434,51 @@ public class ConnectionHandshake { } - private static byte[] awaitNextPacket(long timeout) { - long millis = System.currentTimeMillis(); - byte[] b; - while((b = PlatformNetworking.readPlayPacket()) == null) { - if(PlatformNetworking.playConnectionState().isClosed()) { + private static byte[] awaitNextPacket(IWebSocketClient client, long timeout) { + long millis = EagRuntime.steadyTimeMillis(); + IWebSocketFrame b; + while((b = client.getNextBinaryFrame()) == null) { + if(client.getState().isClosed()) { return null; } try { Thread.sleep(50l); } catch (InterruptedException e) { } - if(System.currentTimeMillis() - millis > timeout) { - PlatformNetworking.playDisconnect(); + if(EagRuntime.steadyTimeMillis() - millis > timeout) { + client.close(); return null; } } - return b; + return b.getByteArray(); } - private static void showError(Minecraft mc, GuiConnecting connecting, GuiScreen scr, DataInputStream err, boolean v2) throws IOException { + private static void showError(Minecraft mc, IWebSocketClient client, GuiConnecting connecting, GuiScreen scr, DataInputStream err, boolean v2) throws IOException { int errorCode = err.read(); int msgLen = v2 ? err.read() : err.readUnsignedShort(); + + // workaround for bug in EaglerXBungee 1.2.7 and below + if(msgLen == 0) { + if(v2) { + if(err.available() == 256) { + msgLen = 256; + } + }else { + if(err.available() == 65536) { + msgLen = 65536; + } + } + } + byte[] dat = new byte[msgLen]; err.read(dat); String errStr = new String(dat, StandardCharsets.UTF_8); logger.info("Server Error Code {}: {}", errorCode, errStr); if(errorCode == HandshakePacketTypes.SERVER_ERROR_RATELIMIT_BLOCKED) { - RateLimitTracker.registerBlock(PlatformNetworking.getCurrentURI()); + RateLimitTracker.registerBlock(client.getCurrentURI()); mc.displayGuiScreen(GuiDisconnected.createRateLimitKick(scr)); }else if(errorCode == HandshakePacketTypes.SERVER_ERROR_RATELIMIT_LOCKED) { - RateLimitTracker.registerLockOut(PlatformNetworking.getCurrentURI()); + RateLimitTracker.registerLockOut(client.getCurrentURI()); mc.displayGuiScreen(GuiDisconnected.createRateLimitKick(scr)); }else if(errorCode == HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE) { mc.displayGuiScreen(new GuiDisconnected(scr, "connect.failed", IChatComponent.Serializer.jsonToComponent(errStr))); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/EaglercraftNetworkManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/EaglercraftNetworkManager.java index 1fe7e649..86959d94 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/EaglercraftNetworkManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/EaglercraftNetworkManager.java @@ -1,24 +1,19 @@ package net.lax1dude.eaglercraft.v1_8.socket; import java.io.IOException; -import java.util.List; import net.lax1dude.eaglercraft.v1_8.internal.EnumEaglerConnectionState; -import net.lax1dude.eaglercraft.v1_8.internal.PlatformNetworking; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.netty.ByteBuf; import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; import net.minecraft.network.EnumConnectionState; -import net.minecraft.network.EnumPacketDirection; import net.minecraft.network.INetHandler; import net.minecraft.network.Packet; import net.minecraft.network.PacketBuffer; -import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.IChatComponent; /** - * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -32,7 +27,7 @@ import net.minecraft.util.IChatComponent; * POSSIBILITY OF SUCH DAMAGE. * */ -public class EaglercraftNetworkManager { +public abstract class EaglercraftNetworkManager { protected final String address; protected INetHandler nethandler = null; @@ -63,103 +58,23 @@ public class EaglercraftNetworkManager { return pluginVersion; } - public void connect() { - PlatformNetworking.startPlayConnection(address); + public abstract void connect(); + + public abstract EnumEaglerConnectionState getConnectStatus(); + + public String getAddress() { + return address; } - public EnumEaglerConnectionState getConnectStatus() { - return PlatformNetworking.playConnectionState(); - } - - public void closeChannel(IChatComponent reason) { - PlatformNetworking.playDisconnect(); - if(nethandler != null) { - nethandler.onDisconnect(reason); - } - clientDisconnected = true; - } + public abstract void closeChannel(IChatComponent reason); public void setConnectionState(EnumConnectionState state) { packetState = state; } - public void processReceivedPackets() throws IOException { - if(nethandler == null) return; - List pkts = PlatformNetworking.readAllPacket(); + public abstract void processReceivedPackets() throws IOException; - if(pkts == null) { - return; - } - - for(int i = 0, l = pkts.size(); i < l; ++i) { - byte[] next = pkts.get(i); - ++debugPacketCounter; - try { - ByteBuf nettyBuffer = Unpooled.buffer(next, next.length); - nettyBuffer.writerIndex(next.length); - PacketBuffer input = new PacketBuffer(nettyBuffer); - int pktId = input.readVarIntFromBuffer(); - - Packet pkt; - try { - pkt = packetState.getPacket(EnumPacketDirection.CLIENTBOUND, pktId); - }catch(IllegalAccessException | InstantiationException ex) { - throw new IOException("Recieved a packet with type " + pktId + " which is invalid!"); - } - - if(pkt == null) { - throw new IOException("Recieved packet type " + pktId + " which is undefined in state " + packetState); - } - - try { - pkt.readPacketData(input); - }catch(Throwable t) { - throw new IOException("Failed to read packet type '" + pkt.getClass().getSimpleName() + "'", t); - } - - try { - pkt.processPacket(nethandler); - }catch(Throwable t) { - logger.error("Failed to process {}! It'll be skipped for debug purposes.", pkt.getClass().getSimpleName()); - logger.error(t); - } - - }catch(Throwable t) { - logger.error("Failed to process websocket frame {}! It'll be skipped for debug purposes.", debugPacketCounter); - logger.error(t); - } - } - } - - public void sendPacket(Packet pkt) { - if(!isChannelOpen()) { - logger.error("Packet was sent on a closed connection: {}", pkt.getClass().getSimpleName()); - return; - } - - int i; - try { - i = packetState.getPacketId(EnumPacketDirection.SERVERBOUND, pkt); - }catch(Throwable t) { - logger.error("Incorrect packet for state: {}", pkt.getClass().getSimpleName()); - return; - } - - temporaryBuffer.clear(); - temporaryBuffer.writeVarIntToBuffer(i); - try { - pkt.writePacketData(temporaryBuffer); - }catch(IOException ex) { - logger.error("Failed to write packet {}!", pkt.getClass().getSimpleName()); - return; - } - - int len = temporaryBuffer.writerIndex(); - byte[] bytes = new byte[len]; - temporaryBuffer.getBytes(0, bytes); - - PlatformNetworking.writePlayPacket(bytes); - } + public abstract void sendPacket(Packet pkt); public void setNetHandler(INetHandler nethandler) { this.nethandler = nethandler; @@ -181,18 +96,7 @@ public class EaglercraftNetworkManager { throw new CompressionNotSupportedException(); } - public boolean checkDisconnected() { - if(PlatformNetworking.playConnectionState().isClosed()) { - try { - processReceivedPackets(); // catch kick message - } catch (IOException e) { - } - doClientDisconnect(new ChatComponentTranslation("disconnect.endOfStream")); - return true; - }else { - return false; - } - } + public abstract boolean checkDisconnected(); protected boolean clientDisconnected = false; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/GuiHandshakeApprove.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/GuiHandshakeApprove.java index 1783f85e..19882b92 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/GuiHandshakeApprove.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/GuiHandshakeApprove.java @@ -46,7 +46,7 @@ public class GuiHandshakeApprove extends GuiScreen { public void initGui() { this.buttonList.clear(); titleString = I18n.format("handshakeApprove." + message + ".title"); - bodyLines = new ArrayList(); + bodyLines = new ArrayList<>(); int i = 0; boolean wasNull = true; while(true) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/RateLimitTracker.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/RateLimitTracker.java index 659809b1..97786ca3 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/RateLimitTracker.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/RateLimitTracker.java @@ -4,6 +4,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; + /** * Copyright (c) 2022-2023 lax1dude, ayunami2000. All Rights Reserved. * @@ -23,12 +25,12 @@ public class RateLimitTracker { private static long lastTickUpdate = 0l; - private static final Map blocks = new HashMap(); - private static final Map lockout = new HashMap(); + private static final Map blocks = new HashMap<>(); + private static final Map lockout = new HashMap<>(); public static boolean isLockedOut(String addr) { Long lockoutStatus = lockout.get(addr); - return lockoutStatus != null && System.currentTimeMillis() - lockoutStatus.longValue() < 300000l; + return lockoutStatus != null && EagRuntime.steadyTimeMillis() - lockoutStatus.longValue() < 300000l; } public static boolean isProbablyLockedOut(String addr) { @@ -36,17 +38,17 @@ public class RateLimitTracker { } public static void registerBlock(String addr) { - blocks.put(addr, System.currentTimeMillis()); + blocks.put(addr, EagRuntime.steadyTimeMillis()); } public static void registerLockOut(String addr) { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); blocks.put(addr, millis); lockout.put(addr, millis); } public static void tick() { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - lastTickUpdate > 5000l) { lastTickUpdate = millis; Iterator blocksItr = blocks.values().iterator(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ServerQueryDispatch.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ServerQueryDispatch.java index dceedc1a..bfff0c4b 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ServerQueryDispatch.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ServerQueryDispatch.java @@ -1,6 +1,7 @@ package net.lax1dude.eaglercraft.v1_8.socket; import net.lax1dude.eaglercraft.v1_8.internal.IServerQuery; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketClient; import net.lax1dude.eaglercraft.v1_8.internal.PlatformNetworking; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; @@ -26,7 +27,8 @@ public class ServerQueryDispatch { public static IServerQuery sendServerQuery(String uri, String accept) { logger.info("Sending {} query to: \"{}\"", accept, uri); - return PlatformNetworking.sendServerQuery(uri, accept); + IWebSocketClient sockClient = PlatformNetworking.openWebSocket(uri); + return sockClient != null ? new ServerQueryImpl(sockClient, accept) : null; } } diff --git a/sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMServerQuery.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ServerQueryImpl.java similarity index 64% rename from sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMServerQuery.java rename to sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ServerQueryImpl.java index 550078cb..19c37156 100644 --- a/sources/teavm/java/net/lax1dude/eaglercraft/v1_8/internal/teavm/TeaVMServerQuery.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/ServerQueryImpl.java @@ -1,25 +1,22 @@ -package net.lax1dude.eaglercraft.v1_8.internal.teavm; +package net.lax1dude.eaglercraft.v1_8.socket; import java.util.LinkedList; import java.util.List; import org.json.JSONObject; -import org.teavm.jso.JSBody; -import org.teavm.jso.JSObject; -import org.teavm.jso.dom.events.Event; -import org.teavm.jso.dom.events.EventListener; -import org.teavm.jso.dom.events.MessageEvent; -import org.teavm.jso.typedarrays.ArrayBuffer; -import org.teavm.jso.websocket.WebSocket; +import net.lax1dude.eaglercraft.v1_8.internal.EnumEaglerConnectionState; import net.lax1dude.eaglercraft.v1_8.internal.EnumServerRateLimit; import net.lax1dude.eaglercraft.v1_8.internal.IServerQuery; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketClient; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketFrame; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime; import net.lax1dude.eaglercraft.v1_8.internal.QueryResponse; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; /** - * Copyright (c) 2022 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -33,58 +30,48 @@ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; * POSSIBILITY OF SUCH DAMAGE. * */ -public class TeaVMServerQuery implements IServerQuery { +class ServerQueryImpl implements IServerQuery { public static final Logger logger = LogManager.getLogger("WebSocketQuery"); - private final List queryResponses = new LinkedList(); - private final List queryResponsesBytes = new LinkedList(); + private final List queryResponses = new LinkedList<>(); + private final List queryResponsesBytes = new LinkedList<>(); + protected final IWebSocketClient websocketClient; protected final String uri; protected final String accept; - protected final WebSocket sock; + protected boolean hasSentAccept = false; protected boolean open = true; protected boolean alive = false; protected long pingStart = -1l; protected long pingTimer = -1l; private EnumServerRateLimit rateLimit = EnumServerRateLimit.OK; - public TeaVMServerQuery(String uri, String accept) { - this.uri = uri; + ServerQueryImpl(IWebSocketClient websocketClient, String accept) { + this.websocketClient = websocketClient; + this.uri = websocketClient.getCurrentURI(); this.accept = accept; - this.sock = WebSocket.create(uri); - initHandlers(); } - @JSBody(params = { "obj" }, script = "return typeof obj === \"string\";") - private static native boolean isString(JSObject obj); - - protected void initHandlers() { - sock.setBinaryType("arraybuffer"); - TeaVMUtils.addEventListener(sock, "open", new EventListener() { - @Override - public void handleEvent(Event evt) { - sock.send("Accept: " + accept); - } - }); - TeaVMUtils.addEventListener(sock, "close", new EventListener() { - @Override - public void handleEvent(Event evt) { - open = false; - } - }); - TeaVMUtils.addEventListener(sock, "message", new EventListener() { - @Override - public void handleEvent(MessageEvent evt) { + @Override + public void update() { + if(!hasSentAccept && websocketClient.getState() == EnumEaglerConnectionState.CONNECTED) { + hasSentAccept = true; + websocketClient.send("Accept: " + accept); + } + List lst = websocketClient.getNextFrames(); + if(lst != null) { + for(int i = 0, l = lst.size(); i < l; ++i) { + IWebSocketFrame frame = lst.get(i); alive = true; if(pingTimer == -1) { - pingTimer = System.currentTimeMillis() - pingStart; + pingTimer = PlatformRuntime.steadyTimeMillis() - pingStart; if(pingTimer < 1) { pingTimer = 1; } } - if(isString(evt.getData())) { - String str = evt.getDataAsString(); + if(frame.isString()) { + String str = frame.getString(); if(str.equalsIgnoreCase("BLOCKED")) { logger.error("Reached full IP ratelimit for {}!", uri); rateLimit = EnumServerRateLimit.BLOCKED; @@ -104,45 +91,33 @@ public class TeaVMServerQuery implements IServerQuery { logger.error("Reached query ratelimit lockout for {}!", uri); rateLimit = EnumServerRateLimit.LOCKED_OUT; }else { - QueryResponse response = new QueryResponse(obj, pingTimer); - synchronized(queryResponses) { - queryResponses.add(response); - } + queryResponses.add(new QueryResponse(obj, pingTimer)); } }catch(Throwable t) { logger.error("Exception thrown parsing websocket query response from \"" + uri + "\"!"); logger.error(t); } }else { - synchronized(queryResponsesBytes) { - queryResponsesBytes.add(TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray())); - } + queryResponsesBytes.add(frame.getByteArray()); } } - }); - TeaVMUtils.addEventListener(sock, "error", new EventListener() { - @Override - public void handleEvent(Event evt) { - sock.close(); - open = false; - } - }); + } + if(websocketClient.isClosed()) { + open = false; + } } @Override public void send(String str) { - if(open) { - sock.send(str); + if(websocketClient.getState() == EnumEaglerConnectionState.CONNECTED) { + websocketClient.send(str); } } - @JSBody(params = { "sock", "buffer" }, script = "sock.send(buffer);") - private static native void nativeBinarySend(WebSocket sock, ArrayBuffer buffer); - @Override public void send(byte[] bytes) { - if(open) { - nativeBinarySend(sock, TeaVMUtils.unwrapArrayBuffer(bytes)); + if(websocketClient.getState() == EnumEaglerConnectionState.CONNECTED) { + websocketClient.send(bytes); } } @@ -192,7 +167,7 @@ public class TeaVMServerQuery implements IServerQuery { public void close() { if(open) { open = false; - sock.close(); + websocketClient.close(); } } @@ -200,4 +175,5 @@ public class TeaVMServerQuery implements IServerQuery { public EnumServerRateLimit getRateLimit() { return rateLimit; } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/WebSocketNetworkManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/WebSocketNetworkManager.java new file mode 100644 index 00000000..fb251f6f --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/WebSocketNetworkManager.java @@ -0,0 +1,152 @@ +package net.lax1dude.eaglercraft.v1_8.socket; + +import java.io.IOException; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.internal.EnumEaglerConnectionState; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketClient; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketFrame; +import net.lax1dude.eaglercraft.v1_8.netty.ByteBuf; +import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; +import net.minecraft.network.EnumPacketDirection; +import net.minecraft.network.Packet; +import net.minecraft.network.PacketBuffer; +import net.minecraft.util.ChatComponentTranslation; +import net.minecraft.util.IChatComponent; + +/** + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class WebSocketNetworkManager extends EaglercraftNetworkManager { + + protected final IWebSocketClient webSocketClient; + + public WebSocketNetworkManager(IWebSocketClient webSocketClient) { + super(webSocketClient.getCurrentURI()); + this.webSocketClient = webSocketClient; + } + + public void connect() { + } + + public EnumEaglerConnectionState getConnectStatus() { + return webSocketClient.getState(); + } + + public void closeChannel(IChatComponent reason) { + webSocketClient.close(); + if(nethandler != null) { + nethandler.onDisconnect(reason); + } + clientDisconnected = true; + } + + public void processReceivedPackets() throws IOException { + if(nethandler == null) return; + if(webSocketClient.availableStringFrames() > 0) { + logger.warn("discarding {} string frames recieved on a binary connection", webSocketClient.availableStringFrames()); + webSocketClient.clearStringFrames(); + } + List pkts = webSocketClient.getNextBinaryFrames(); + + if(pkts == null) { + return; + } + + for(int i = 0, l = pkts.size(); i < l; ++i) { + IWebSocketFrame next = pkts.get(i); + ++debugPacketCounter; + try { + byte[] asByteArray = next.getByteArray(); + ByteBuf nettyBuffer = Unpooled.buffer(asByteArray, asByteArray.length); + nettyBuffer.writerIndex(asByteArray.length); + PacketBuffer input = new PacketBuffer(nettyBuffer); + int pktId = input.readVarIntFromBuffer(); + + Packet pkt; + try { + pkt = packetState.getPacket(EnumPacketDirection.CLIENTBOUND, pktId); + }catch(IllegalAccessException | InstantiationException ex) { + throw new IOException("Recieved a packet with type " + pktId + " which is invalid!"); + } + + if(pkt == null) { + throw new IOException("Recieved packet type " + pktId + " which is undefined in state " + packetState); + } + + try { + pkt.readPacketData(input); + }catch(Throwable t) { + throw new IOException("Failed to read packet type '" + pkt.getClass().getSimpleName() + "'", t); + } + + try { + pkt.processPacket(nethandler); + }catch(Throwable t) { + logger.error("Failed to process {}! It'll be skipped for debug purposes.", pkt.getClass().getSimpleName()); + logger.error(t); + } + + }catch(Throwable t) { + logger.error("Failed to process websocket frame {}! It'll be skipped for debug purposes.", debugPacketCounter); + logger.error(t); + } + } + } + + public void sendPacket(Packet pkt) { + if(!isChannelOpen()) { + logger.error("Packet was sent on a closed connection: {}", pkt.getClass().getSimpleName()); + return; + } + + int i; + try { + i = packetState.getPacketId(EnumPacketDirection.SERVERBOUND, pkt); + }catch(Throwable t) { + logger.error("Incorrect packet for state: {}", pkt.getClass().getSimpleName()); + return; + } + + temporaryBuffer.clear(); + temporaryBuffer.writeVarIntToBuffer(i); + try { + pkt.writePacketData(temporaryBuffer); + }catch(IOException ex) { + logger.error("Failed to write packet {}!", pkt.getClass().getSimpleName()); + return; + } + + int len = temporaryBuffer.writerIndex(); + byte[] bytes = new byte[len]; + temporaryBuffer.getBytes(0, bytes); + + webSocketClient.send(bytes); + } + + public boolean checkDisconnected() { + if(webSocketClient.isClosed()) { + try { + processReceivedPackets(); // catch kick message + } catch (IOException e) { + } + doClientDisconnect(new ChatComponentTranslation("disconnect.endOfStream")); + return true; + }else { + return false; + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/ClientV3MessageHandler.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/ClientV3MessageHandler.java new file mode 100644 index 00000000..039c8646 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/ClientV3MessageHandler.java @@ -0,0 +1,130 @@ +package net.lax1dude.eaglercraft.v1_8.socket.protocol.client; + +import java.nio.charset.StandardCharsets; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +import net.lax1dude.eaglercraft.v1_8.profile.SkinModel; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessageHandler; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.*; +import net.lax1dude.eaglercraft.v1_8.update.UpdateService; +import net.lax1dude.eaglercraft.v1_8.voice.VoiceClientController; +import net.minecraft.client.Minecraft; +import net.minecraft.client.network.NetHandlerPlayClient; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ClientV3MessageHandler implements GameMessageHandler { + + private final NetHandlerPlayClient netHandler; + + public ClientV3MessageHandler(NetHandlerPlayClient netHandler) { + this.netHandler = netHandler; + } + + public void handleServer(SPacketEnableFNAWSkinsEAG packet) { + netHandler.currentFNAWSkinAllowedState = packet.enableSkins; + netHandler.currentFNAWSkinForcedState = packet.enableSkins; + Minecraft.getMinecraft().getRenderManager().setEnableFNAWSkins(netHandler.currentFNAWSkinForcedState + || (netHandler.currentFNAWSkinAllowedState && Minecraft.getMinecraft().gameSettings.enableFNAWSkins)); + } + + public void handleServer(SPacketOtherCapeCustomEAG packet) { + netHandler.getCapeCache().cacheCapeCustom(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), + packet.customCape); + } + + public void handleServer(SPacketOtherCapePresetEAG packet) { + netHandler.getCapeCache().cacheCapePreset(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), + packet.presetCape); + } + + public void handleServer(SPacketOtherSkinCustomV3EAG packet) { + EaglercraftUUID responseUUID = new EaglercraftUUID(packet.uuidMost, packet.uuidLeast); + SkinModel modelId; + if(packet.modelID == (byte)0xFF) { + modelId = this.netHandler.getSkinCache().getRequestedSkinType(responseUUID); + }else { + modelId = SkinModel.getModelFromId(packet.modelID & 0x7F); + if((packet.modelID & 0x80) != 0 && modelId.sanitize) { + modelId = SkinModel.STEVE; + } + } + if(modelId.highPoly != null) { + modelId = SkinModel.STEVE; + } + this.netHandler.getSkinCache().cacheSkinCustom(responseUUID, packet.customSkin, modelId); + } + + public void handleServer(SPacketOtherSkinPresetEAG packet) { + this.netHandler.getSkinCache().cacheSkinPreset(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), + packet.presetSkin); + } + + public void handleServer(SPacketUpdateCertEAG packet) { + if (EagRuntime.getConfiguration().allowUpdateSvc()) { + UpdateService.addCertificateToSet(packet.updateCert); + } + } + + public void handleServer(SPacketVoiceSignalAllowedEAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeAllowed(packet.allowed, packet.iceServers); + } + } + + public void handleServer(SPacketVoiceSignalConnectV3EAG packet) { + if (VoiceClientController.isClientSupported()) { + if (packet.isAnnounceType) { + VoiceClientController.handleVoiceSignalPacketTypeConnectAnnounce( + new EaglercraftUUID(packet.uuidMost, packet.uuidLeast)); + } else { + VoiceClientController.handleVoiceSignalPacketTypeConnect( + new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), packet.offer); + } + } + } + + public void handleServer(SPacketVoiceSignalDescEAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeDescription( + new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), + new String(packet.desc, StandardCharsets.UTF_8)); + } + } + + public void handleServer(SPacketVoiceSignalDisconnectPeerEAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeDisconnect( + new EaglercraftUUID(packet.uuidMost, packet.uuidLeast)); + } + } + + public void handleServer(SPacketVoiceSignalGlobalEAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeGlobalNew(packet.users); + } + } + + public void handleServer(SPacketVoiceSignalICEEAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeICECandidate( + new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), + new String(packet.ice, StandardCharsets.UTF_8)); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/ClientV4MessageHandler.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/ClientV4MessageHandler.java new file mode 100644 index 00000000..eb52fbe8 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/ClientV4MessageHandler.java @@ -0,0 +1,222 @@ +package net.lax1dude.eaglercraft.v1_8.socket.protocol.client; + +import java.nio.charset.StandardCharsets; + +import net.lax1dude.eaglercraft.v1_8.ClientUUIDLoadingCache; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +import net.lax1dude.eaglercraft.v1_8.PauseMenuCustomizeState; +import net.lax1dude.eaglercraft.v1_8.cookie.ServerCookieDataStore; +import net.lax1dude.eaglercraft.v1_8.profile.EaglerProfile; +import net.lax1dude.eaglercraft.v1_8.profile.SkinModel; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessageHandler; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.*; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.util.SkinPacketVersionCache; +import net.lax1dude.eaglercraft.v1_8.update.UpdateService; +import net.lax1dude.eaglercraft.v1_8.voice.VoiceClientController; +import net.lax1dude.eaglercraft.v1_8.webview.ServerInfoCache; +import net.lax1dude.eaglercraft.v1_8.webview.WebViewOverlayController; +import net.minecraft.client.Minecraft; +import net.minecraft.client.network.NetHandlerPlayClient; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ClientV4MessageHandler implements GameMessageHandler { + + private final NetHandlerPlayClient netHandler; + + public ClientV4MessageHandler(NetHandlerPlayClient netHandler) { + this.netHandler = netHandler; + } + + public void handleServer(SPacketEnableFNAWSkinsEAG packet) { + netHandler.currentFNAWSkinAllowedState = packet.enableSkins; + netHandler.currentFNAWSkinForcedState = packet.force; + Minecraft.getMinecraft().getRenderManager().setEnableFNAWSkins(netHandler.currentFNAWSkinForcedState + || (netHandler.currentFNAWSkinAllowedState && Minecraft.getMinecraft().gameSettings.enableFNAWSkins)); + } + + public void handleServer(SPacketOtherCapeCustomEAG packet) { + netHandler.getCapeCache().cacheCapeCustom(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), + packet.customCape); + } + + public void handleServer(SPacketOtherCapePresetEAG packet) { + netHandler.getCapeCache().cacheCapePreset(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), + packet.presetCape); + } + + public void handleServer(SPacketOtherSkinCustomV4EAG packet) { + EaglercraftUUID responseUUID = new EaglercraftUUID(packet.uuidMost, packet.uuidLeast); + SkinModel modelId; + if(packet.modelID == (byte)0xFF) { + modelId = this.netHandler.getSkinCache().getRequestedSkinType(responseUUID); + }else { + modelId = SkinModel.getModelFromId(packet.modelID & 0x7F); + if((packet.modelID & 0x80) != 0 && modelId.sanitize) { + modelId = SkinModel.STEVE; + } + } + if(modelId.highPoly != null) { + modelId = SkinModel.STEVE; + } + this.netHandler.getSkinCache().cacheSkinCustom(responseUUID, SkinPacketVersionCache.convertToV3Raw(packet.customSkin), modelId); + } + + public void handleServer(SPacketOtherSkinPresetEAG packet) { + this.netHandler.getSkinCache().cacheSkinPreset(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), packet.presetSkin); + } + + public void handleServer(SPacketUpdateCertEAG packet) { + if (EagRuntime.getConfiguration().allowUpdateSvc()) { + UpdateService.addCertificateToSet(packet.updateCert); + } + } + + public void handleServer(SPacketVoiceSignalAllowedEAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeAllowed(packet.allowed, packet.iceServers); + } + } + + public void handleServer(SPacketVoiceSignalConnectV4EAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeConnect(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), packet.offer); + } + } + + public void handleServer(SPacketVoiceSignalConnectAnnounceV4EAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeConnectAnnounce(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast)); + } + } + + public void handleServer(SPacketVoiceSignalDescEAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeDescription( + new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), + new String(packet.desc, StandardCharsets.UTF_8)); + } + } + + public void handleServer(SPacketVoiceSignalDisconnectPeerEAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeDisconnect(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast)); + } + } + + public void handleServer(SPacketVoiceSignalGlobalEAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeGlobalNew(packet.users); + } + } + + public void handleServer(SPacketVoiceSignalICEEAG packet) { + if (VoiceClientController.isClientSupported()) { + VoiceClientController.handleVoiceSignalPacketTypeICECandidate( + new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), + new String(packet.ice, StandardCharsets.UTF_8)); + } + } + + public void handleServer(SPacketForceClientSkinPresetV4EAG packet) { + EaglerProfile.handleForceSkinPreset(packet.presetSkin); + } + + public void handleServer(SPacketForceClientSkinCustomV4EAG packet) { + EaglerProfile.handleForceSkinCustom(packet.modelID, SkinPacketVersionCache.convertToV3Raw(packet.customSkin)); + } + + public void handleServer(SPacketSetServerCookieV4EAG packet) { + if(!netHandler.isClientInEaglerSingleplayerOrLAN() && Minecraft.getMinecraft().getCurrentServerData().enableCookies) { + ServerCookieDataStore.saveCookie(netHandler.getNetworkManager().getAddress(), packet.expires, packet.data, + packet.revokeQuerySupported, packet.saveCookieToDisk); + } + } + + public void handleServer(SPacketRedirectClientV4EAG packet) { + Minecraft.getMinecraft().handleReconnectPacket(packet.redirectURI); + } + + public void handleServer(SPacketOtherPlayerClientUUIDV4EAG packet) { + ClientUUIDLoadingCache.handleResponse(packet.requestId, new EaglercraftUUID(packet.clientUUIDMost, packet.clientUUIDLeast)); + } + + public void handleServer(SPacketForceClientCapePresetV4EAG packet) { + EaglerProfile.handleForceCapePreset(packet.presetCape); + } + + public void handleServer(SPacketForceClientCapeCustomV4EAG packet) { + EaglerProfile.handleForceCapeCustom(packet.customCape); + } + + public void handleServer(SPacketInvalidatePlayerCacheV4EAG packet) { + if(packet.players != null && packet.players.size() > 0) { + for(SPacketInvalidatePlayerCacheV4EAG.InvalidateRequest req : packet.players) { + EaglercraftUUID uuid = new EaglercraftUUID(req.uuidMost, req.uuidLeast); + if(req.invalidateSkin) { + this.netHandler.getSkinCache().handleInvalidate(uuid); + } + if(req.invalidateCape) { + this.netHandler.getCapeCache().handleInvalidate(uuid); + } + } + } + } + + public void handleServer(SPacketUnforceClientV4EAG packet) { + if(packet.resetSkin) { + EaglerProfile.isServerSkinOverride = false; + } + if(packet.resetCape) { + EaglerProfile.isServerCapeOverride = false; + } + if(packet.resetFNAW) { + netHandler.currentFNAWSkinForcedState = false; + Minecraft.getMinecraft().getRenderManager().setEnableFNAWSkins( + netHandler.currentFNAWSkinAllowedState && Minecraft.getMinecraft().gameSettings.enableFNAWSkins); + } + } + + public void handleServer(SPacketCustomizePauseMenuV4EAG packet) { + PauseMenuCustomizeState.loadPacket(packet); + } + + public void handleServer(SPacketServerInfoDataChunkV4EAG packet) { + ServerInfoCache.handleChunk(packet); + } + + public void handleServer(SPacketWebViewMessageV4EAG packet) { + WebViewOverlayController.handleMessagePacket(packet); + } + + public void handleServer(SPacketNotifIconsRegisterV4EAG packet) { + netHandler.getNotifManager().processPacketAddIcons(packet); + } + + public void handleServer(SPacketNotifIconsReleaseV4EAG packet) { + netHandler.getNotifManager().processPacketRemIcons(packet); + } + + public void handleServer(SPacketNotifBadgeShowV4EAG packet) { + netHandler.getNotifManager().processPacketShowBadge(packet); + } + + public void handleServer(SPacketNotifBadgeHideV4EAG packet) { + netHandler.getNotifManager().processPacketHideBadge(packet); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/GameProtocolMessageController.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/GameProtocolMessageController.java new file mode 100644 index 00000000..dc4176ea --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/GameProtocolMessageController.java @@ -0,0 +1,199 @@ +package net.lax1dude.eaglercraft.v1_8.socket.protocol.client; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePacketOutputBuffer; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageConstants; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageProtocol; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessageHandler; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +import net.lax1dude.eaglercraft.v1_8.sp.server.socket.protocol.ServerV3MessageHandler; +import net.lax1dude.eaglercraft.v1_8.sp.server.socket.protocol.ServerV4MessageHandler; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.PacketBuffer; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GameProtocolMessageController { + + private static final Logger logger = LogManager.getLogger("GameProtocolMessageController"); + + public final GamePluginMessageProtocol protocol; + public final int sendDirection; + public final int receiveDirection; + private final PacketBufferInputWrapper inputStream = new PacketBufferInputWrapper(null); + private final PacketBufferOutputWrapper outputStream = new PacketBufferOutputWrapper(null); + private final GameMessageHandler handler; + private final IPluginMessageSendFunction sendFunction; + private final List sendQueueV4; + private final boolean noDelay; + + public GameProtocolMessageController(GamePluginMessageProtocol protocol, int sendDirection, GameMessageHandler handler, + IPluginMessageSendFunction sendCallback) { + this.protocol = protocol; + this.sendDirection = sendDirection; + this.receiveDirection = GamePluginMessageConstants.oppositeDirection(sendDirection); + this.handler = handler; + this.sendFunction = sendCallback; + this.noDelay = protocol.ver < 4 || EagRuntime.getConfiguration().isEaglerNoDelay(); + this.sendQueueV4 = !noDelay ? new LinkedList<>() : null; + } + + public boolean handlePacket(String channel, PacketBuffer data) throws IOException { + GameMessagePacket pkt; + if(protocol.ver >= 4 && data.readableBytes() > 0 && data.getByte(data.readerIndex()) == (byte) 0xFF + && channel.equals(GamePluginMessageConstants.V4_CHANNEL)) { + data.readByte(); + inputStream.buffer = data; + int count = inputStream.readVarInt(); + for(int i = 0, j, k; i < count; ++i) { + j = data.readVarIntFromBuffer(); + k = data.readerIndex() + j; + if(j > data.readableBytes()) { + throw new IOException("Packet fragment is too long: " + j + " > " + data.readableBytes()); + } + pkt = protocol.readPacket(channel, receiveDirection, inputStream); + if(pkt != null) { + try { + pkt.handlePacket(handler); + }catch(Throwable t) { + logger.error("Failed to handle packet {} in direction {} using handler {}!", pkt.getClass().getSimpleName(), + GamePluginMessageConstants.getDirectionString(receiveDirection), handler); + logger.error(t); + } + }else { + logger.warn("Could not read packet fragment {} of {}, unknown packet", count, i); + } + if(data.readerIndex() != k) { + logger.warn("Packet fragment {} was the wrong length: {} != {}", + (pkt != null ? pkt.getClass().getSimpleName() : "unknown"), j + data.readerIndex() - k, j); + data.readerIndex(k); + } + } + if(data.readableBytes() > 0) { + logger.warn("Leftover data after reading multi-packet! ({} bytes)", data.readableBytes()); + } + inputStream.buffer = null; + return true; + } + inputStream.buffer = data; + pkt = protocol.readPacket(channel, receiveDirection, inputStream); + if(pkt != null && inputStream.available() > 0) { + logger.warn("Leftover data after reading packet {}! ({} bytes)", pkt.getClass().getSimpleName(), inputStream.available()); + } + inputStream.buffer = null; + if(pkt != null) { + try { + pkt.handlePacket(handler); + }catch(Throwable t) { + logger.error("Failed to handle packet {} in direction {} using handler {}!", pkt.getClass().getSimpleName(), + GamePluginMessageConstants.getDirectionString(receiveDirection), handler); + logger.error(t); + } + return true; + }else { + return false; + } + } + + public void sendPacket(GameMessagePacket packet) throws IOException { + int len = packet.length() + 1; + PacketBuffer buf = new PacketBuffer(len != 0 ? Unpooled.buffer(len) : Unpooled.buffer(64)); + outputStream.buffer = buf; + String chan = protocol.writePacket(sendDirection, outputStream, packet); + outputStream.buffer = null; + int j = buf.writerIndex(); + if(len != 0 && j != len && j + 1 != len) { + logger.warn("Packet {} was expected to be {} bytes but was serialized to {} bytes!", + packet.getClass().getSimpleName(), len, j); + } + if(sendQueueV4 != null && chan.equals(GamePluginMessageConstants.V4_CHANNEL)) { + sendQueueV4.add(buf); + }else { + sendFunction.sendPluginMessage(chan, buf); + } + } + + public void flush() { + if(sendQueueV4 != null) { + int queueLen = sendQueueV4.size(); + PacketBuffer pkt; + if(queueLen == 0) { + return; + }else if(queueLen == 1) { + pkt = sendQueueV4.remove(0); + sendFunction.sendPluginMessage(GamePluginMessageConstants.V4_CHANNEL, pkt); + }else { + int i, j, sendCount = 0, totalLen = 0; + PacketBuffer sendBuffer; + while(sendQueueV4.size() > 0) { + do { + i = sendQueueV4.get(sendCount++).readableBytes(); + totalLen += GamePacketOutputBuffer.getVarIntSize(i) + i; + }while(totalLen < 32760 && sendCount < sendQueueV4.size()); + if(totalLen >= 32760) { + --sendCount; + } + if(sendCount <= 1) { + pkt = sendQueueV4.remove(0); + sendFunction.sendPluginMessage(GamePluginMessageConstants.V4_CHANNEL, pkt); + continue; + } + sendBuffer = new PacketBuffer(Unpooled.buffer(1 + totalLen + GamePacketOutputBuffer.getVarIntSize(sendCount))); + sendBuffer.writeByte(0xFF); + sendBuffer.writeVarIntToBuffer(sendCount); + for(j = 0; j < sendCount; ++j) { + pkt = sendQueueV4.remove(0); + sendBuffer.writeVarIntToBuffer(pkt.readableBytes()); + sendBuffer.writeBytes(pkt); + } + sendFunction.sendPluginMessage(GamePluginMessageConstants.V4_CHANNEL, sendBuffer); + } + } + } + } + + public static GameMessageHandler createClientHandler(int protocolVersion, NetHandlerPlayClient netHandler) { + switch(protocolVersion) { + case 2: + case 3: + return new ClientV3MessageHandler(netHandler); + case 4: + return new ClientV4MessageHandler(netHandler); + default: + throw new IllegalArgumentException("Unknown protocol verison: " + protocolVersion); + } + } + + public static GameMessageHandler createServerHandler(int protocolVersion, NetHandlerPlayServer netHandler) { + switch(protocolVersion) { + case 2: + case 3: + return new ServerV3MessageHandler(netHandler); + case 4: + return new ServerV4MessageHandler(netHandler); + default: + throw new IllegalArgumentException("Unknown protocol verison: " + protocolVersion); + } + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/IPluginMessageSendFunction.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/IPluginMessageSendFunction.java new file mode 100644 index 00000000..e4fbbf7b --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/IPluginMessageSendFunction.java @@ -0,0 +1,24 @@ +package net.lax1dude.eaglercraft.v1_8.socket.protocol.client; + +import net.minecraft.network.PacketBuffer; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public interface IPluginMessageSendFunction { + + void sendPluginMessage(String channel, PacketBuffer contents); + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/PacketBufferInputWrapper.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/PacketBufferInputWrapper.java new file mode 100644 index 00000000..1566bb7e --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/PacketBufferInputWrapper.java @@ -0,0 +1,303 @@ +package net.lax1dude.eaglercraft.v1_8.socket.protocol.client; + +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +import net.lax1dude.eaglercraft.v1_8.DecoderException; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePacketInputBuffer; +import net.minecraft.network.PacketBuffer; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class PacketBufferInputWrapper implements GamePacketInputBuffer { + + protected PacketBuffer buffer; + + public PacketBufferInputWrapper(PacketBuffer buffer) { + this.buffer = buffer; + } + + public PacketBuffer getBuffer() { + return buffer; + } + + public void setBuffer(PacketBuffer buffer) { + this.buffer = buffer; + } + + @Override + public void readFully(byte[] b) throws IOException { + try { + buffer.readBytes(b); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public void readFully(byte[] b, int off, int len) throws IOException { + try { + buffer.readBytes(b, off, len); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public int skipBytes(int n) throws IOException { + int r = buffer.readableBytes(); + if(n > r) { + n = r; + } + buffer.readerIndex(buffer.readerIndex() + n); + return n; + } + + @Override + public boolean readBoolean() throws IOException { + try { + return buffer.readBoolean(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public byte readByte() throws IOException { + try { + return buffer.readByte(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public int readUnsignedByte() throws IOException { + try { + return buffer.readUnsignedByte(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public short readShort() throws IOException { + try { + return buffer.readShort(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public int readUnsignedShort() throws IOException { + try { + return buffer.readUnsignedShort(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public char readChar() throws IOException { + try { + return buffer.readChar(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public int readInt() throws IOException { + try { + return buffer.readInt(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public long readLong() throws IOException { + try { + return buffer.readLong(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public float readFloat() throws IOException { + try { + return buffer.readFloat(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public double readDouble() throws IOException { + try { + return buffer.readDouble(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public String readLine() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public String readUTF() throws IOException { + return DataInputStream.readUTF(this); + } + + @Override + public void skipAllBytes(int n) throws IOException { + if(buffer.readableBytes() < n) { + throw new EOFException(); + } + buffer.readerIndex(buffer.readerIndex() + n); + } + + @Override + public int readVarInt() throws IOException { + try { + return buffer.readVarIntFromBuffer(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public long readVarLong() throws IOException { + try { + return buffer.readVarLong(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public String readStringMC(int maxLen) throws IOException { + try { + return buffer.readStringFromBuffer(maxLen); + }catch(DecoderException ex) { + throw new IOException(ex.getMessage()); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public String readStringEaglerASCII8() throws IOException { + int len = readUnsignedByte(); + char[] ret = new char[len]; + for(int i = 0; i < len; ++i) { + ret[i] = (char)readByte(); + } + return new String(ret); + } + + @Override + public String readStringEaglerASCII16() throws IOException { + int len = readUnsignedShort(); + char[] ret = new char[len]; + for(int i = 0; i < len; ++i) { + ret[i] = (char)readByte(); + } + return new String(ret); + } + + @Override + public byte[] readByteArrayMC() throws IOException { + try { + return buffer.readByteArray(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + @Override + public int available() throws IOException { + return buffer.readableBytes(); + } + + @Override + public InputStream stream() { + return new InputStream() { + + @Override + public int read() throws IOException { + if(buffer.readableBytes() > 0) { + return buffer.readUnsignedShort(); + }else { + return -1; + } + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + int avail = buffer.readableBytes(); + if(avail == 0) return -1; + len = avail > len ? avail : len; + buffer.readBytes(b, off, len); + return len; + } + + @Override + public long skip(long n) throws IOException { + return PacketBufferInputWrapper.this.skipBytes((int)n); + } + + @Override + public int available() throws IOException { + return buffer.readableBytes(); + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public synchronized void mark(int readlimit) { + buffer.markReaderIndex(); + } + + @Override + public synchronized void reset() throws IOException { + try { + buffer.resetReaderIndex(); + }catch(IndexOutOfBoundsException ex) { + throw new EOFException(); + } + } + + }; + } + + @Override + public byte[] toByteArray() throws IOException { + byte[] ret = new byte[buffer.readableBytes()]; + buffer.readBytes(ret); + return ret; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/PacketBufferOutputWrapper.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/PacketBufferOutputWrapper.java new file mode 100644 index 00000000..67e17cc0 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/socket/protocol/client/PacketBufferOutputWrapper.java @@ -0,0 +1,316 @@ +package net.lax1dude.eaglercraft.v1_8.socket.protocol.client; + +import java.io.IOException; +import java.io.OutputStream; + +import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePacketOutputBuffer; +import net.minecraft.network.PacketBuffer; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class PacketBufferOutputWrapper implements GamePacketOutputBuffer { + + protected PacketBuffer buffer; + + public PacketBufferOutputWrapper(PacketBuffer buffer) { + this.buffer = buffer; + } + + public PacketBuffer getBuffer() { + return buffer; + } + + public void setBuffer(PacketBuffer buffer) { + this.buffer = buffer; + } + + @Override + public void write(int b) throws IOException { + try { + buffer.writeByte(b); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void write(byte[] b) throws IOException { + try { + buffer.writeBytes(b); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + try { + buffer.writeBytes(b, off, len); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeBoolean(boolean v) throws IOException { + try { + buffer.writeBoolean(v); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeByte(int v) throws IOException { + try { + buffer.writeByte(v); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeShort(int v) throws IOException { + try { + buffer.writeShort(v); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeChar(int v) throws IOException { + try { + buffer.writeChar(v); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeInt(int v) throws IOException { + try { + buffer.writeInt(v); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeLong(long v) throws IOException { + try { + buffer.writeLong(v); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeFloat(float v) throws IOException { + try { + buffer.writeFloat(v); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeDouble(double v) throws IOException { + try { + buffer.writeDouble(v); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeBytes(String s) throws IOException { + try { + int l = s.length(); + for(int i = 0; i < l; ++i) { + buffer.writeByte((int)s.charAt(i)); + } + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeChars(String s) throws IOException { + try { + int l = s.length(); + for(int i = 0; i < l; ++i) { + buffer.writeChar(s.charAt(i)); + } + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public final void writeUTF(String str) throws IOException { + long utfCount = countUTFBytes(str); + if (utfCount > 65535) { + throw new IOException("String is longer than 65535 bytes when encoded as UTF8!"); + } + byte[] arr = new byte[(int) utfCount + 2]; + int offset = 2; + arr[0] = (byte)(((int)utfCount >>> 8) & 0xFF); + arr[1] = (byte)((int)utfCount & 0xFF); + offset = writeUTFBytesToBuffer(str, arr, offset); + try { + buffer.writeBytes(arr, 0, offset); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + private static long countUTFBytes(String str) { + int utfCount = 0; + int length = str.length(); + for (int i = 0; i < length; i++) { + int charValue = str.charAt(i); + if (charValue > 0 && charValue <= 127) { + utfCount++; + } else if (charValue <= 2047) { + utfCount += 2; + } else { + utfCount += 3; + } + } + return utfCount; + } + + private static int writeUTFBytesToBuffer(String str, byte[] buffer, int offset) throws IOException { + int length = str.length(); + for (int i = 0; i < length; i++) { + int charValue = str.charAt(i); + if (charValue > 0 && charValue <= 127) { + buffer[offset++] = (byte) charValue; + } else if (charValue <= 2047) { + buffer[offset++] = (byte) (0xc0 | (0x1f & (charValue >> 6))); + buffer[offset++] = (byte) (0x80 | (0x3f & charValue)); + } else { + buffer[offset++] = (byte) (0xe0 | (0x0f & (charValue >> 12))); + buffer[offset++] = (byte) (0x80 | (0x3f & (charValue >> 6))); + buffer[offset++] = (byte) (0x80 | (0x3f & charValue)); + } + } + return offset; + } + + @Override + public void writeVarInt(int i) throws IOException { + try { + buffer.writeVarIntToBuffer(i); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeVarLong(long i) throws IOException { + try { + buffer.writeVarLong(i); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeStringMC(String str) throws IOException { + try { + buffer.writeString(str); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeStringEaglerASCII8(String str) throws IOException { + int len = str.length(); + if(len > 255) { + throw new IOException("String is longer than 255 chars! (" + len + ")"); + } + try { + buffer.writeByte(len); + for(int i = 0, j; i < len; ++i) { + j = (int)str.charAt(i); + if(j > 255) { + j = (int)'?'; + } + buffer.writeByte(j); + } + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeStringEaglerASCII16(String str) throws IOException { + int len = str.length(); + if(len > 65535) { + throw new IOException("String is longer than 65535 chars! (" + len + ")"); + } + try { + buffer.writeShort(len); + for(int i = 0, j; i < len; ++i) { + j = (int)str.charAt(i); + if(j > 255) { + j = (int)'?'; + } + buffer.writeByte(j); + } + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void writeByteArrayMC(byte[] bytes) throws IOException { + try { + buffer.writeByteArray(bytes); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public OutputStream stream() { + return new OutputStream() { + + @Override + public void write(int b) throws IOException { + try { + buffer.writeByte(b); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + try { + buffer.writeBytes(b, off, len); + }catch(IndexOutOfBoundsException ex) { + throw new IOException("Packet buffer overflowed!"); + } + } + + }; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/SingleplayerServerController.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/SingleplayerServerController.java index 1479e012..4f8c30a1 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/SingleplayerServerController.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/SingleplayerServerController.java @@ -3,9 +3,11 @@ package net.lax1dude.eaglercraft.v1_8.sp; import java.io.IOException; import java.util.ArrayList; 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.lax1dude.eaglercraft.v1_8.internal.PlatformWebRTC; @@ -56,34 +58,59 @@ public class SingleplayerServerController implements ISaveFormat { private static boolean loggingState = true; private static String worldStatusString = ""; private static float worldStatusProgress = 0.0f; - private static final LinkedList exceptions = new LinkedList(); + private static final LinkedList exceptions = new LinkedList<>(); + private static final Set issuesDetected = new HashSet<>(); public static final SingleplayerServerController instance = new SingleplayerServerController(); public static final Logger logger = LogManager.getLogger("SingleplayerServerController"); - public static final List saveListCache = new ArrayList(); - public static final Map saveListMap = new HashMap(); - public static final List saveListNBT = new ArrayList(); + public static final List saveListCache = new ArrayList<>(); + public static final Map saveListMap = new HashMap<>(); + public static final List saveListNBT = new ArrayList<>(); private static boolean isPaused = false; - private static List integratedServerTPS = new ArrayList(); + private static List integratedServerTPS = new ArrayList<>(); private static long integratedServerLastTPSUpdate = 0; public static final ClientIntegratedServerNetworkManager localPlayerNetworkManager = new ClientIntegratedServerNetworkManager(PLAYER_CHANNEL); - private static final List openLANChannels = new ArrayList(); + private static final List openLANChannels = new ArrayList<>(); private static final IPCPacketManager packetManagerInstance = new IPCPacketManager(); private SingleplayerServerController() { } - public static void startIntegratedServerWorker() { + public static void startIntegratedServerWorker(boolean forceSingleThread) { if(statusState == IntegratedServerState.WORLD_WORKER_NOT_RUNNING) { exceptions.clear(); + issuesDetected.clear(); statusState = IntegratedServerState.WORLD_WORKER_BOOTING; loggingState = true; - ClientPlatformSingleplayer.startIntegratedServer(); + boolean singleThreadSupport = ClientPlatformSingleplayer.isSingleThreadModeSupported(); + if(!singleThreadSupport && forceSingleThread) { + throw new UnsupportedOperationException("Single thread mode is not supported!"); + } + if(forceSingleThread || !singleThreadSupport) { + ClientPlatformSingleplayer.startIntegratedServer(forceSingleThread); + }else { + try { + ClientPlatformSingleplayer.startIntegratedServer(forceSingleThread); + }catch(Throwable t) { + logger.error("Failed to start integrated server worker"); + logger.error(t); + logger.error("Attempting to use single thread mode"); + exceptions.clear(); + issuesDetected.clear(); + statusState = IntegratedServerState.WORLD_WORKER_BOOTING; + loggingState = true; + ClientPlatformSingleplayer.startIntegratedServer(true); + } + } } } + public static boolean isIssueDetected(int issue) { + return issuesDetected.contains(issue); + } + public static boolean isIntegratedServerWorkerStarted() { return statusState != IntegratedServerState.WORLD_WORKER_NOT_RUNNING && statusState != IntegratedServerState.WORLD_WORKER_BOOTING; } @@ -196,7 +223,7 @@ public class SingleplayerServerController implements ISaveFormat { } public static long getTPSAge() { - return System.currentTimeMillis() - integratedServerLastTPSUpdate; + return EagRuntime.steadyTimeMillis() - integratedServerLastTPSUpdate; } public static boolean hangupEaglercraftServer() { @@ -269,7 +296,11 @@ public class SingleplayerServerController implements ISaveFormat { boolean logWindowState = PlatformApplication.isShowingDebugConsole(); if(loggingState != logWindowState) { loggingState = logWindowState; - sendIPCPacket(new IPCPacket21EnableLogging(logWindowState)); + sendIPCPacket(new IPCPacket1BEnableLogging(logWindowState)); + } + + if(ClientPlatformSingleplayer.isRunningSingleThreadMode()) { + ClientPlatformSingleplayer.updateSingleThreadMode(); } LANServerController.updateLANServer(); @@ -385,17 +416,22 @@ public class SingleplayerServerController implements ISaveFormat { if(pkt.opCode == IPCPacket14StringList.SERVER_TPS) { integratedServerTPS.clear(); integratedServerTPS.addAll(pkt.stringList); - integratedServerLastTPSUpdate = System.currentTimeMillis(); + integratedServerLastTPSUpdate = EagRuntime.steadyTimeMillis(); }else { logger.warn("Strange string list type {} recieved!", pkt.opCode); } break; } - case IPCPacket20LoggerMessage.ID: { - IPCPacket20LoggerMessage pkt = (IPCPacket20LoggerMessage)ipc; + case IPCPacket1ALoggerMessage.ID: { + IPCPacket1ALoggerMessage pkt = (IPCPacket1ALoggerMessage)ipc; PlatformApplication.addLogMessage(pkt.logMessage, pkt.isError); break; } + case IPCPacket1CIssueDetected.ID: { + IPCPacket1CIssueDetected pkt = (IPCPacket1CIssueDetected)ipc; + issuesDetected.add(pkt.issueID); + break; + } default: throw new RuntimeException("Unexpected IPC packet type recieved on client: " + ipc.id()); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/SkullCommand.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/SkullCommand.java index 5d2bf322..1d2aa1d0 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/SkullCommand.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/SkullCommand.java @@ -3,9 +3,8 @@ package net.lax1dude.eaglercraft.v1_8.sp; import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.internal.FileChooserResult; import net.lax1dude.eaglercraft.v1_8.opengl.ImageData; -import net.lax1dude.eaglercraft.v1_8.profile.SkinPackets; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.CPacketInstallSkinSPEAG; import net.minecraft.client.Minecraft; -import net.minecraft.network.play.client.C17PacketCustomPayload; import net.minecraft.util.ChatComponentTranslation; /** @@ -44,9 +43,9 @@ public class SkullCommand { if(fr == null || mc.thePlayer == null || mc.thePlayer.sendQueue == null) { return; } - ImageData loaded = ImageData.loadImageFile(fr.fileData); + ImageData loaded = ImageData.loadImageFile(fr.fileData, ImageData.getMimeFromType(fr.fileName)); if(loaded == null) { - mc.ingameGUI.getChatGUI().printChatMessage(new ChatComponentTranslation("command.skull.error.invalid.png")); + mc.ingameGUI.getChatGUI().printChatMessage(new ChatComponentTranslation("command.skull.error.invalid.format")); return; } if(loaded.width != 64 || loaded.height > 64) { @@ -62,7 +61,7 @@ public class SkullCommand { rawSkin[j + 2] = (byte)(k >>> 8); rawSkin[j + 3] = (byte)(k & 0xFF); } - mc.thePlayer.sendQueue.addToSendQueue(new C17PacketCustomPayload("EAG|Skins-1.8", SkinPackets.writeCreateCustomSkull(rawSkin))); + mc.thePlayer.sendQueue.sendEaglerMessage(new CPacketInstallSkinSPEAG(rawSkin)); } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiIntegratedServerStartup.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiIntegratedServerStartup.java deleted file mode 100644 index 9e5349d1..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiIntegratedServerStartup.java +++ /dev/null @@ -1,58 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.sp.gui; - -import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.GuiSelectWorld; -import net.minecraft.client.resources.I18n; - -/** - * Copyright (c) 2023-2024 lax1dude. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class GuiIntegratedServerStartup extends GuiScreen { - - private final GuiScreen backScreen; - private static final String[] dotDotDot = new String[] { "", ".", "..", "..." }; - - private int counter = 0; - - public GuiIntegratedServerStartup(GuiScreen backScreen) { - this.backScreen = backScreen; - } - - protected void keyTyped(char parChar1, int parInt1) { - } - - public void initGui() { - this.buttonList.clear(); - } - - public void updateScreen() { - ++counter; - if(counter > 1 && SingleplayerServerController.isIntegratedServerWorkerStarted()) { - mc.displayGuiScreen(new GuiSelectWorld(backScreen)); - }else if(counter == 2) { - SingleplayerServerController.startIntegratedServerWorker(); - } - } - - public void drawScreen(int i, int j, float f) { - this.drawBackground(0); - String txt = I18n.format("singleplayer.integratedStartup"); - int w = this.fontRendererObj.getStringWidth(txt); - this.drawString(this.fontRendererObj, txt + dotDotDot[(int)((System.currentTimeMillis() / 300L) % 4L)], (this.width - w) / 2, this.height / 2 - 50, 16777215); - super.drawScreen(i, j, f); - } - -} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenAddRelay.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenAddRelay.java index 7032d616..8bba2619 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenAddRelay.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenAddRelay.java @@ -2,6 +2,7 @@ package net.lax1dude.eaglercraft.v1_8.sp.gui; import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.Keyboard; +import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayManager; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; @@ -143,4 +144,16 @@ public class GuiScreenAddRelay extends GuiScreen { public boolean blockPTTKey() { return this.serverName.isFocused() || this.serverAddress.isFocused(); } + + @Override + public boolean showCopyPasteButtons() { + return this.serverName.isFocused() || this.serverAddress.isFocused(); + } + + @Override + public void fireInputEvent(EnumInputEvent event, String param) { + this.serverName.fireInputEvent(event, param); + this.serverAddress.fireInputEvent(event, param); + } + } \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenDemoIntegratedServerStartup.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenDemoIntegratedServerStartup.java index 4a351d56..f5418127 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenDemoIntegratedServerStartup.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenDemoIntegratedServerStartup.java @@ -1,9 +1,14 @@ package net.lax1dude.eaglercraft.v1_8.sp.gui; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; import net.lax1dude.eaglercraft.v1_8.sp.WorkerStartupFailedException; import net.lax1dude.eaglercraft.v1_8.sp.ipc.IPCPacket15Crashed; +import net.lax1dude.eaglercraft.v1_8.sp.ipc.IPCPacket1CIssueDetected; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiMainMenu; import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiSelectWorld; import net.minecraft.client.resources.I18n; /** @@ -24,46 +29,76 @@ import net.minecraft.client.resources.I18n; public class GuiScreenDemoIntegratedServerStartup extends GuiScreen { private final GuiScreen contScreen; + private final boolean singleThread; private static final String[] dotDotDot = new String[] { "", ".", "..", "..." }; private int counter = 0; + private GuiButton cancelButton; + public GuiScreenDemoIntegratedServerStartup(GuiScreen contScreen) { this.contScreen = contScreen; + this.singleThread = false; } - protected void keyTyped(char parChar1, int parInt1) { + public GuiScreenDemoIntegratedServerStartup(GuiScreen contScreen, boolean singleThread) { + this.contScreen = contScreen; + this.singleThread = singleThread; } public void initGui() { this.buttonList.clear(); + this.buttonList.add(cancelButton = new GuiButton(0, this.width / 2 - 100, this.height / 3 + 50, I18n.format("singleplayer.busy.killTask"))); + cancelButton.visible = false; } public void updateScreen() { ++counter; if(counter == 2) { try { - SingleplayerServerController.startIntegratedServerWorker(); + SingleplayerServerController.startIntegratedServerWorker(singleThread); }catch(WorkerStartupFailedException ex) { mc.displayGuiScreen(new GuiScreenIntegratedServerFailed(ex.getMessage(), new GuiScreenDemoIntegratedServerFailed())); return; } }else if(counter > 2) { + if(counter > 100 && SingleplayerServerController.canKillWorker() && !singleThread) { + cancelButton.visible = true; + } IPCPacket15Crashed[] crashReport = SingleplayerServerController.worldStatusErrors(); if(crashReport != null) { mc.displayGuiScreen(GuiScreenIntegratedServerBusy.createException(new GuiScreenDemoIntegratedServerFailed(), "singleplayer.failed.notStarted", crashReport)); }else if(SingleplayerServerController.isIntegratedServerWorkerStarted()) { - mc.displayGuiScreen(contScreen); + GuiScreen cont = contScreen; + if(SingleplayerServerController.isRunningSingleThreadMode()) { + cont = new GuiScreenIntegratedServerFailed("singleplayer.failed.singleThreadWarning.1", "singleplayer.failed.singleThreadWarning.2", cont); + } else if (!EagRuntime.getConfiguration().isRamdiskMode() + && SingleplayerServerController.isIssueDetected(IPCPacket1CIssueDetected.ISSUE_RAMDISK_MODE) + && SingleplayerServerController.canKillWorker()) { + cont = new GuiScreenRAMDiskModeDetected(cont); + } + mc.displayGuiScreen(cont); } } } + protected void actionPerformed(GuiButton parGuiButton) { + if(parGuiButton.id == 0) { + SingleplayerServerController.killWorker(); + mc.displayGuiScreen(new GuiScreenDemoIntegratedServerStartup(contScreen, true)); + } + } + public void drawScreen(int i, int j, float f) { this.drawBackground(0); String txt = I18n.format("singleplayer.integratedStartup"); int w = this.fontRendererObj.getStringWidth(txt); - this.drawString(this.fontRendererObj, txt + dotDotDot[(int)((System.currentTimeMillis() / 300L) % 4L)], (this.width - w) / 2, this.height / 2 - 50, 16777215); + this.drawString(this.fontRendererObj, txt + dotDotDot[(int)((EagRuntime.steadyTimeMillis() / 300L) % 4L)], (this.width - w) / 2, this.height / 2 - 50, 16777215); super.drawScreen(i, j, f); } + public boolean canCloseGui() { + return false; + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerBusy.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerBusy.java index 53b82876..effd5153 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerBusy.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerBusy.java @@ -88,7 +88,7 @@ public class GuiScreenIntegratedServerBusy extends GuiScreen { } public void initGui() { - if(startStartTime == 0) this.startStartTime = System.currentTimeMillis(); + if(startStartTime == 0) this.startStartTime = EagRuntime.steadyTimeMillis(); areYouSure = 0; this.buttonList.add(killTask = new GuiButton(0, this.width / 2 - 100, this.height / 3 + 50, I18n.format("singleplayer.busy.killTask"))); killTask.enabled = false; @@ -102,7 +102,7 @@ public class GuiScreenIntegratedServerBusy extends GuiScreen { this.drawDefaultBackground(); int top = this.height / 3; - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); String str = I18n.format(currentStatus); @@ -128,7 +128,7 @@ public class GuiScreenIntegratedServerBusy extends GuiScreen { } public void updateScreen() { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - startStartTime > 6000l && SingleplayerServerController.canKillWorker()) { killTask.enabled = true; } @@ -164,4 +164,8 @@ public class GuiScreenIntegratedServerBusy extends GuiScreen { return false; } + public boolean canCloseGui() { + return false; + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerCrashed.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerCrashed.java index fb97ea4f..aba74974 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerCrashed.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerCrashed.java @@ -2,7 +2,6 @@ package net.lax1dude.eaglercraft.v1_8.sp.gui; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.resources.I18n; /** @@ -33,8 +32,7 @@ public class GuiScreenIntegratedServerCrashed extends GuiScreen { public void initGui() { this.buttonList.clear(); this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height - 50, I18n.format("singleplayer.crashed.continue"))); - ScaledResolution res = new ScaledResolution(mc); - int i = res.getScaleFactor(); + int i = mc.scaledResolution.getScaleFactor(); CrashScreen.showCrashReportOverlay(crashReport, 90 * i, 60 * i, (width - 180) * i, (height - 130) * i); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerFailed.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerFailed.java index 2e6a88b6..aba653c0 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerFailed.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerFailed.java @@ -1,6 +1,9 @@ package net.lax1dude.eaglercraft.v1_8.sp.gui; +import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; +import net.lax1dude.eaglercraft.v1_8.sp.internal.ClientPlatformSingleplayer; import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiMainMenu; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.resources.I18n; @@ -40,6 +43,9 @@ public class GuiScreenIntegratedServerFailed extends GuiScreen { public void initGui() { this.buttonList.clear(); this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 96, I18n.format("singleplayer.crashed.continue"))); + if(!ClientPlatformSingleplayer.isRunningSingleThreadMode() && ClientPlatformSingleplayer.isSingleThreadModeSupported()) { + this.buttonList.add(new GuiButton(1, this.width / 2 - 100, this.height / 6 + 126, I18n.format("singleplayer.crashed.singleThreadCont"))); + } } public void drawScreen(int par1, int par2, float par3) { @@ -52,6 +58,12 @@ public class GuiScreenIntegratedServerFailed extends GuiScreen { protected void actionPerformed(GuiButton par1GuiButton) { if(par1GuiButton.id == 0) { this.mc.displayGuiScreen(cont); + }else if(par1GuiButton.id == 1) { + if(SingleplayerServerController.canKillWorker()) { + SingleplayerServerController.killWorker(); + } + this.mc.displayGuiScreen(new GuiScreenIntegratedServerStartup(new GuiMainMenu(), true)); } } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerStartup.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerStartup.java index 5b8e5c6f..13ddcf05 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerStartup.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenIntegratedServerStartup.java @@ -1,8 +1,11 @@ package net.lax1dude.eaglercraft.v1_8.sp.gui; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; import net.lax1dude.eaglercraft.v1_8.sp.WorkerStartupFailedException; import net.lax1dude.eaglercraft.v1_8.sp.ipc.IPCPacket15Crashed; +import net.lax1dude.eaglercraft.v1_8.sp.ipc.IPCPacket1CIssueDetected; +import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiMainMenu; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiSelectWorld; @@ -26,31 +29,42 @@ import net.minecraft.client.resources.I18n; public class GuiScreenIntegratedServerStartup extends GuiScreen { private final GuiScreen backScreen; + private final boolean singleThread; private static final String[] dotDotDot = new String[] { "", ".", "..", "..." }; private int counter = 0; + private GuiButton cancelButton; + public GuiScreenIntegratedServerStartup(GuiScreen backScreen) { this.backScreen = backScreen; + this.singleThread = false; } - protected void keyTyped(char parChar1, int parInt1) { + public GuiScreenIntegratedServerStartup(GuiScreen backScreen, boolean singleThread) { + this.backScreen = backScreen; + this.singleThread = singleThread; } public void initGui() { this.buttonList.clear(); + this.buttonList.add(cancelButton = new GuiButton(0, this.width / 2 - 100, this.height / 3 + 50, I18n.format("singleplayer.busy.killTask"))); + cancelButton.visible = false; } public void updateScreen() { ++counter; if(counter == 2) { try { - SingleplayerServerController.startIntegratedServerWorker(); + SingleplayerServerController.startIntegratedServerWorker(singleThread); }catch(WorkerStartupFailedException ex) { mc.displayGuiScreen(new GuiScreenIntegratedServerFailed(ex.getMessage(), new GuiMainMenu())); return; } }else if(counter > 2) { + if(counter > 100 && SingleplayerServerController.canKillWorker() && !singleThread) { + cancelButton.visible = true; + } IPCPacket15Crashed[] crashReport = SingleplayerServerController.worldStatusErrors(); if(crashReport != null) { mc.displayGuiScreen(GuiScreenIntegratedServerBusy.createException(new GuiMainMenu(), "singleplayer.failed.notStarted", crashReport)); @@ -58,18 +72,33 @@ public class GuiScreenIntegratedServerStartup extends GuiScreen { GuiScreen cont = new GuiSelectWorld(backScreen); if(SingleplayerServerController.isRunningSingleThreadMode()) { cont = new GuiScreenIntegratedServerFailed("singleplayer.failed.singleThreadWarning.1", "singleplayer.failed.singleThreadWarning.2", cont); + } else if (!EagRuntime.getConfiguration().isRamdiskMode() + && SingleplayerServerController.isIssueDetected(IPCPacket1CIssueDetected.ISSUE_RAMDISK_MODE) + && SingleplayerServerController.canKillWorker()) { + cont = new GuiScreenRAMDiskModeDetected(cont); } mc.displayGuiScreen(cont); } } } + protected void actionPerformed(GuiButton parGuiButton) { + if(parGuiButton.id == 0) { + SingleplayerServerController.killWorker(); + mc.displayGuiScreen(new GuiScreenIntegratedServerStartup(new GuiMainMenu(), true)); + } + } + public void drawScreen(int i, int j, float f) { this.drawBackground(0); String txt = I18n.format("singleplayer.integratedStartup"); int w = this.fontRendererObj.getStringWidth(txt); - this.drawString(this.fontRendererObj, txt + dotDotDot[(int)((System.currentTimeMillis() / 300L) % 4L)], (this.width - w) / 2, this.height / 2 - 50, 16777215); + this.drawString(this.fontRendererObj, txt + dotDotDot[(int)((EagRuntime.steadyTimeMillis() / 300L) % 4L)], (this.width - w) / 2, this.height / 2 - 50, 16777215); super.drawScreen(i, j, f); } + public boolean canCloseGui() { + return false; + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenLANConnect.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenLANConnect.java index c6b0aba1..7853f459 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenLANConnect.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenLANConnect.java @@ -1,6 +1,7 @@ package net.lax1dude.eaglercraft.v1_8.sp.gui; import net.lax1dude.eaglercraft.v1_8.Keyboard; +import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiTextField; @@ -88,4 +89,14 @@ public class GuiScreenLANConnect extends GuiScreen { } } + @Override + public boolean showCopyPasteButtons() { + return codeTextField.isFocused(); + } + + @Override + public void fireInputEvent(EnumInputEvent event, String param) { + codeTextField.fireInputEvent(event, param); + } + } \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenLANConnecting.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenLANConnecting.java index d433bfd9..26e4cba1 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenLANConnecting.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenLANConnecting.java @@ -1,7 +1,9 @@ package net.lax1dude.eaglercraft.v1_8.sp.gui; +import net.lax1dude.eaglercraft.v1_8.EaglercraftVersion; import net.lax1dude.eaglercraft.v1_8.internal.PlatformWebRTC; import net.lax1dude.eaglercraft.v1_8.profile.EaglerProfile; +import net.lax1dude.eaglercraft.v1_8.socket.ConnectionHandshake; import net.lax1dude.eaglercraft.v1_8.sp.lan.LANClientNetworkManager; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayManager; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayServer; @@ -120,9 +122,15 @@ public class GuiScreenLANConnecting extends GuiScreen { this.mc.clearTitles(); networkManager.setConnectionState(EnumConnectionState.LOGIN); networkManager.setNetHandler(new NetHandlerSingleplayerLogin(networkManager, mc, parent)); - networkManager.sendPacket(new C00PacketLoginStart(this.mc.getSession().getProfile(), EaglerProfile.getSkinPacket(), EaglerProfile.getCapePacket())); + networkManager.sendPacket(new C00PacketLoginStart(this.mc.getSession().getProfile(), + EaglerProfile.getSkinPacket(3), EaglerProfile.getCapePacket(), + ConnectionHandshake.getSPHandshakeProtocolData(), EaglercraftVersion.clientBrandUUID)); } } } + public boolean canCloseGui() { + return false; + } + } \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenNameWorldImport.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenNameWorldImport.java index eded7662..ff8c54d6 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenNameWorldImport.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenNameWorldImport.java @@ -3,6 +3,7 @@ package net.lax1dude.eaglercraft.v1_8.sp.gui; import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.Keyboard; import net.lax1dude.eaglercraft.v1_8.internal.FileChooserResult; +import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiCreateWorld; @@ -144,10 +145,21 @@ public class GuiScreenNameWorldImport extends GuiScreen { this.theGuiTextField.drawTextBox(); }else { definetlyTimeToImport = true; - long dots = (System.currentTimeMillis() / 500l) % 4l; + long dots = (EagRuntime.steadyTimeMillis() / 500l) % 4l; String str = I18n.format("singleplayer.import.reading", world.fileName); this.drawString(fontRendererObj, str + (dots > 0 ? "." : "") + (dots > 1 ? "." : "") + (dots > 2 ? "." : ""), (this.width - this.fontRendererObj.getStringWidth(str)) / 2, this.height / 3 + 10, 0xFFFFFF); } super.drawScreen(par1, par2, par3); } + + @Override + public boolean showCopyPasteButtons() { + return theGuiTextField.isFocused(); + } + + @Override + public void fireInputEvent(EnumInputEvent event, String param) { + theGuiTextField.fireInputEvent(event, param); + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenRAMDiskModeDetected.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenRAMDiskModeDetected.java new file mode 100644 index 00000000..732f88d6 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenRAMDiskModeDetected.java @@ -0,0 +1,55 @@ +package net.lax1dude.eaglercraft.v1_8.sp.gui; + +import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenRAMDiskModeDetected extends GuiScreen { + + private GuiScreen cont; + + public GuiScreenRAMDiskModeDetected(GuiScreen cont) { + this.cont = cont; + } + + public void initGui() { + this.buttonList.clear(); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 106, I18n.format("singleplayer.ramdiskdetected.continue"))); + this.buttonList.add(new GuiButton(1, this.width / 2 - 100, this.height / 6 + 136, I18n.format("singleplayer.ramdiskdetected.singleThreadCont"))); + } + + public void drawScreen(int par1, int par2, float par3) { + this.drawDefaultBackground(); + this.drawCenteredString(fontRendererObj, I18n.format("singleplayer.ramdiskdetected.title"), this.width / 2, 70, 11184810); + this.drawCenteredString(fontRendererObj, I18n.format("singleplayer.ramdiskdetected.text0"), this.width / 2, 90, 16777215); + this.drawCenteredString(fontRendererObj, I18n.format("singleplayer.ramdiskdetected.text1"), this.width / 2, 105, 16777215); + super.drawScreen(par1, par2, par3); + } + + protected void actionPerformed(GuiButton par1GuiButton) { + if(par1GuiButton.id == 0) { + this.mc.displayGuiScreen(cont); + }else if(par1GuiButton.id == 1) { + SingleplayerServerController.killWorker(); + mc.displayGuiScreen(new GuiScreenIntegratedServerStartup(new GuiMainMenu(), true)); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenRelay.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenRelay.java index da4a48cc..8f10ec17 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenRelay.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenRelay.java @@ -98,7 +98,7 @@ public class GuiScreenRelay extends GuiScreen implements GuiYesNoCallback { selected = 0; } } else if(btn.id == 4) { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - lastRefresh > 700l) { lastRefresh = millis; slots.relayManager.ping(); @@ -106,14 +106,14 @@ public class GuiScreenRelay extends GuiScreen implements GuiYesNoCallback { lastRefresh += 60l; } else if(btn.id == 5) { slots.relayManager.loadDefaults(); - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - lastRefresh > 700l) { lastRefresh = millis; slots.relayManager.ping(); } lastRefresh += 60l; } else if(btn.id == 6) { - EagRuntime.downloadFileWithName("EaglerSPRelay.zip", EagRuntime.getResourceBytes("relay_download.zip")); + EagRuntime.downloadFileWithName("EaglerSPRelay.zip", EagRuntime.getRequiredResourceBytes("relay_download.zip")); } } @@ -215,4 +215,10 @@ public class GuiScreenRelay extends GuiScreen implements GuiYesNoCallback { this.slots.handleMouseInput(); } + @Override + public void handleTouchInput() throws IOException { + super.handleTouchInput(); + this.slots.handleTouchInput(); + } + } \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenSingleplayerConnecting.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenSingleplayerConnecting.java index fb38a61c..89f8d6f4 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenSingleplayerConnecting.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiScreenSingleplayerConnecting.java @@ -2,7 +2,10 @@ package net.lax1dude.eaglercraft.v1_8.sp.gui; import java.io.IOException; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.EaglercraftVersion; import net.lax1dude.eaglercraft.v1_8.profile.EaglerProfile; +import net.lax1dude.eaglercraft.v1_8.socket.ConnectionHandshake; import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; import net.lax1dude.eaglercraft.v1_8.sp.socket.ClientIntegratedServerNetworkManager; import net.lax1dude.eaglercraft.v1_8.sp.socket.NetHandlerSingleplayerLogin; @@ -47,7 +50,7 @@ public class GuiScreenSingleplayerConnecting extends GuiScreen { } public void initGui() { - if(startStartTime == 0) this.startStartTime = System.currentTimeMillis(); + if(startStartTime == 0) this.startStartTime = EagRuntime.steadyTimeMillis(); this.buttonList.add(killTask = new GuiButton(0, this.width / 2 - 100, this.height / 3 + 50, I18n.format("singleplayer.busy.killTask"))); killTask.enabled = false; } @@ -57,7 +60,7 @@ public class GuiScreenSingleplayerConnecting extends GuiScreen { float f = 2.0f; int top = this.height / 3; - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); long dots = (millis / 500l) % 4l; this.drawString(fontRendererObj, message + (dots > 0 ? "." : "") + (dots > 1 ? "." : "") + (dots > 2 ? "." : ""), (this.width - this.fontRendererObj.getStringWidth(message)) / 2, top + 10, 0xFFFFFF); @@ -88,7 +91,9 @@ public class GuiScreenSingleplayerConnecting extends GuiScreen { this.mc.clearTitles(); this.networkManager.setConnectionState(EnumConnectionState.LOGIN); this.networkManager.setNetHandler(new NetHandlerSingleplayerLogin(this.networkManager, this.mc, this.menu)); - this.networkManager.sendPacket(new C00PacketLoginStart(this.mc.getSession().getProfile(), EaglerProfile.getSkinPacket(), EaglerProfile.getCapePacket())); + this.networkManager.sendPacket(new C00PacketLoginStart(this.mc.getSession().getProfile(), + EaglerProfile.getSkinPacket(3), EaglerProfile.getCapePacket(), + ConnectionHandshake.getSPHandshakeProtocolData(), EaglercraftVersion.clientBrandUUID)); } try { this.networkManager.processReceivedPackets(); @@ -106,7 +111,7 @@ public class GuiScreenSingleplayerConnecting extends GuiScreen { } } - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - startStartTime > 6000l && SingleplayerServerController.canKillWorker()) { killTask.enabled = true; } @@ -124,4 +129,9 @@ public class GuiScreenSingleplayerConnecting extends GuiScreen { public boolean shouldHangupIntegratedServer() { return false; } + + public boolean canCloseGui() { + return false; + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiShareToLan.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiShareToLan.java index cbf896f9..107013f4 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiShareToLan.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiShareToLan.java @@ -1,6 +1,7 @@ package net.lax1dude.eaglercraft.v1_8.sp.gui; import net.lax1dude.eaglercraft.v1_8.internal.PlatformWebRTC; +import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; import net.lax1dude.eaglercraft.v1_8.sp.lan.LANServerController; import net.minecraft.client.LoadingScreenRenderer; @@ -198,4 +199,15 @@ public class GuiShareToLan extends GuiScreen { public boolean blockPTTKey() { return this.codeTextField.isFocused(); } + + @Override + public boolean showCopyPasteButtons() { + return this.codeTextField.isFocused(); + } + + @Override + public void fireInputEvent(EnumInputEvent event, String param) { + this.codeTextField.fireInputEvent(event, param); + } + } \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiSlider2.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiSlider2.java index 12dc29c5..cba3519f 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiSlider2.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/gui/GuiSlider2.java @@ -27,10 +27,11 @@ public class GuiSlider2 extends GuiButton { /** Is this slider control being dragged. */ public boolean dragging = false; - public GuiSlider2(int par1, int par2, int par3, int par4, int par5, float par6, float par7) { - super(par1, par2, par3, par4, par5, (int)(par6 * par7 * 100.0F) + "%"); - this.sliderValue = par6; - this.sliderMax = par7; + public GuiSlider2(int buttonId, int x, int y, int widthIn, int heightIn, float sliderValue, float sliderMax) { + super(buttonId, x, y, widthIn, heightIn, null); + this.sliderValue = sliderValue; + this.sliderMax = sliderMax; + this.displayString = updateDisplayString(); } /** @@ -48,6 +49,7 @@ public class GuiSlider2 extends GuiButton { protected void mouseDragged(Minecraft par1Minecraft, int par2, int par3) { if (this.visible) { if (this.dragging) { + float oldValue = sliderValue; this.sliderValue = (float) (par2 - (this.xPosition + 4)) / (float) (this.width - 8); if (this.sliderValue < 0.0F) { @@ -58,7 +60,11 @@ public class GuiSlider2 extends GuiButton { this.sliderValue = 1.0F; } - this.displayString = (int)(this.sliderValue * this.sliderMax * 100.0F) + "%"; + if(oldValue != sliderValue) { + onChange(); + } + + this.displayString = updateDisplayString(); } if(this.enabled) { @@ -75,6 +81,7 @@ public class GuiSlider2 extends GuiButton { */ public boolean mousePressed(Minecraft par1Minecraft, int par2, int par3) { if (super.mousePressed(par1Minecraft, par2, par3)) { + float oldValue = sliderValue; this.sliderValue = (float) (par2 - (this.xPosition + 4)) / (float) (this.width - 8); if (this.sliderValue < 0.0F) { @@ -85,7 +92,11 @@ public class GuiSlider2 extends GuiButton { this.sliderValue = 1.0F; } - this.displayString = (int)(this.sliderValue * this.sliderMax * 100.0F) + "%"; + if(oldValue != sliderValue) { + onChange(); + } + + this.displayString = updateDisplayString(); this.dragging = true; return true; } else { @@ -100,4 +111,17 @@ public class GuiSlider2 extends GuiButton { public void mouseReleased(int par1, int par2) { this.dragging = false; } + + protected String updateDisplayString() { + return (int)(this.sliderValue * this.sliderMax * 100.0F) + "%"; + } + + protected void onChange() { + + } + + public boolean isSliderTouchEvents() { + return true; + } + } \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket14StringList.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket14StringList.java index 62c56440..90d3df6c 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket14StringList.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket14StringList.java @@ -34,11 +34,11 @@ public class IPCPacket14StringList implements IPCPacketBase { public final List stringList; public IPCPacket14StringList() { - stringList = new ArrayList(); + stringList = new ArrayList<>(); } public IPCPacket14StringList(int opcode, String[] list) { - stringList = new ArrayList(); + stringList = new ArrayList<>(list.length); for(int i = 0; i < list.length; ++i) { String s = list[i].trim(); if(s.length() > 0) { @@ -49,7 +49,7 @@ public class IPCPacket14StringList implements IPCPacketBase { } public IPCPacket14StringList(int opcode, List list) { - stringList = new ArrayList(); + stringList = new ArrayList<>(list.size()); for(int i = 0, l = list.size(); i < l; ++i) { String s = list.get(i).trim(); if(s.length() > 0) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket16NBTList.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket16NBTList.java index bb9d464f..d065a082 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket16NBTList.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket16NBTList.java @@ -40,8 +40,8 @@ public class IPCPacket16NBTList implements IPCPacketBase { public final List nbtTagList; public IPCPacket16NBTList() { - tagList = new LinkedList(); - nbtTagList = new LinkedList(); + tagList = new LinkedList<>(); + nbtTagList = new LinkedList<>(); } public IPCPacket16NBTList(int opcode, NBTTagCompound[] list) { @@ -49,7 +49,7 @@ public class IPCPacket16NBTList implements IPCPacketBase { } public IPCPacket16NBTList(int opcode, List list) { - tagList = new LinkedList(); + tagList = new LinkedList<>(); nbtTagList = list; for(int i = 0, size = list.size(); i < size; ++i) { NBTTagCompound tag = list.get(i); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket17ConfigureLAN.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket17ConfigureLAN.java index 272dbc21..21167e54 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket17ConfigureLAN.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket17ConfigureLAN.java @@ -30,7 +30,7 @@ public class IPCPacket17ConfigureLAN implements IPCPacketBase { public final List iceServers; public IPCPacket17ConfigureLAN() { - iceServers = new ArrayList(); + iceServers = new ArrayList<>(); } public IPCPacket17ConfigureLAN(int gamemode, boolean cheats, List iceServers) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket20LoggerMessage.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket1ALoggerMessage.java similarity index 86% rename from sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket20LoggerMessage.java rename to sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket1ALoggerMessage.java index 9babf0b6..7404478f 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket20LoggerMessage.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket1ALoggerMessage.java @@ -19,22 +19,22 @@ import java.io.IOException; * POSSIBILITY OF SUCH DAMAGE. * */ -public class IPCPacket20LoggerMessage implements IPCPacketBase { +public class IPCPacket1ALoggerMessage implements IPCPacketBase { - public static final int ID = 0x20; + public static final int ID = 0x1A; public String logMessage; public boolean isError; - public IPCPacket20LoggerMessage() { + public IPCPacket1ALoggerMessage() { } - public IPCPacket20LoggerMessage(String logMessage, boolean isError) { + public IPCPacket1ALoggerMessage(String logMessage, boolean isError) { this.logMessage = logMessage; this.isError = isError; } - public IPCPacket20LoggerMessage(String logMessage) { + public IPCPacket1ALoggerMessage(String logMessage) { this.logMessage = logMessage; this.isError = false; } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket21EnableLogging.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket1BEnableLogging.java similarity index 87% rename from sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket21EnableLogging.java rename to sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket1BEnableLogging.java index 7c9423d2..a3e5afd1 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket21EnableLogging.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket1BEnableLogging.java @@ -19,16 +19,16 @@ import java.io.IOException; * POSSIBILITY OF SUCH DAMAGE. * */ -public class IPCPacket21EnableLogging implements IPCPacketBase { +public class IPCPacket1BEnableLogging implements IPCPacketBase { - public static final int ID = 0x21; + public static final int ID = 0x1B; public boolean enable; - public IPCPacket21EnableLogging() { + public IPCPacket1BEnableLogging() { } - public IPCPacket21EnableLogging(boolean enable) { + public IPCPacket1BEnableLogging(boolean enable) { this.enable = enable; } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket1CIssueDetected.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket1CIssueDetected.java new file mode 100644 index 00000000..51a5e962 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacket1CIssueDetected.java @@ -0,0 +1,57 @@ +package net.lax1dude.eaglercraft.v1_8.sp.ipc; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class IPCPacket1CIssueDetected implements IPCPacketBase { + + public static final int ID = 0x1C; + + public static final int ISSUE_RAMDISK_MODE = 0x01; + + public int issueID; + + public IPCPacket1CIssueDetected() { + } + + public IPCPacket1CIssueDetected(int issueID) { + this.issueID = issueID; + } + + @Override + public void deserialize(DataInput bin) throws IOException { + issueID = bin.readUnsignedByte(); + } + + @Override + public void serialize(DataOutput bin) throws IOException { + bin.writeByte(issueID); + } + + @Override + public int id() { + return ID; + } + + @Override + public int size() { + return 1; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacketManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacketManager.java index f501c67d..6363efa2 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacketManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/ipc/IPCPacketManager.java @@ -23,7 +23,7 @@ import java.util.function.Supplier; */ public class IPCPacketManager { - public static final HashMap> mappings = new HashMap(); + public static final HashMap> mappings = new HashMap<>(); public final IPCInputStream IPC_INPUT_STREAM = new IPCInputStream(); public final IPCOutputStream IPC_OUTPUT_STREAM = new IPCOutputStream(); @@ -55,8 +55,9 @@ public class IPCPacketManager { mappings.put(IPCPacket17ConfigureLAN.ID, IPCPacket17ConfigureLAN::new); mappings.put(IPCPacket18ClearPlayers.ID, IPCPacket18ClearPlayers::new); mappings.put(IPCPacket19Autosave.ID, IPCPacket19Autosave::new); - mappings.put(IPCPacket20LoggerMessage.ID, IPCPacket20LoggerMessage::new); - mappings.put(IPCPacket21EnableLogging.ID, IPCPacket21EnableLogging::new); + mappings.put(IPCPacket1ALoggerMessage.ID, IPCPacket1ALoggerMessage::new); + mappings.put(IPCPacket1BEnableLogging.ID, IPCPacket1BEnableLogging::new); + mappings.put(IPCPacket1CIssueDetected.ID, IPCPacket1CIssueDetected::new); mappings.put(IPCPacketFFProcessKeepAlive.ID, IPCPacketFFProcessKeepAlive::new); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANClientNetworkManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANClientNetworkManager.java index d442f9fb..15a1054b 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANClientNetworkManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANClientNetworkManager.java @@ -1,5 +1,6 @@ package net.lax1dude.eaglercraft.v1_8.sp.lan; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EagUtils; import net.lax1dude.eaglercraft.v1_8.EaglerInputStream; import net.lax1dude.eaglercraft.v1_8.EaglerZLIB; @@ -75,15 +76,17 @@ public class LANClientNetworkManager extends EaglercraftNetworkManager { public static LANClientNetworkManager connectToWorld(RelayServerSocket sock, String displayCode, String displayRelay) { PlatformWebRTC.clearLANClientState(); int connectState = PRE; - IPacket pkt; + RelayPacket pkt; mainLoop: while(!sock.isClosed()) { + PlatformWebRTC.runScheduledTasks(); + sock.update(); if((pkt = sock.readPacket()) != null) { - if(pkt instanceof IPacket00Handshake) { + if(pkt instanceof RelayPacket00Handshake) { if(connectState == PRE) { // %%%%%% Process IPacket00Handshake %%%%%% - logger.info("Relay [{}|{}] recieved handshake, client id: {}", displayRelay, displayCode, ((IPacket00Handshake)pkt).connectionCode); + logger.info("Relay [{}|{}] recieved handshake, client id: {}", displayRelay, displayCode, ((RelayPacket00Handshake)pkt).connectionCode); connectState = INIT; }else { @@ -91,17 +94,17 @@ public class LANClientNetworkManager extends EaglercraftNetworkManager { logger.error("Relay [{}|{}] unexpected packet: IPacket00Handshake in state {}", displayRelay, displayCode, initStateNames[connectState]); return null; } - }else if(pkt instanceof IPacket01ICEServers) { + }else if(pkt instanceof RelayPacket01ICEServers) { if(connectState == INIT) { // %%%%%% Process IPacket01ICEServers %%%%%% - IPacket01ICEServers ipkt = (IPacket01ICEServers) pkt; + RelayPacket01ICEServers ipkt = (RelayPacket01ICEServers) pkt; // print servers logger.info("Relay [{}|{}] provided ICE servers:", displayRelay, displayCode); - List servers = new ArrayList(); - for(ICEServerSet.RelayServer srv : ipkt.servers) { + List servers = new ArrayList<>(); + for(RelayPacket01ICEServers.RelayServer srv : ipkt.servers) { logger.info("Relay [{}|{}] {}: {}", displayRelay, displayCode, srv.type.name(), srv.address); servers.add(srv.getICEString()); } @@ -110,20 +113,21 @@ public class LANClientNetworkManager extends EaglercraftNetworkManager { PlatformWebRTC.clientLANSetICEServersAndConnect(servers.toArray(new String[servers.size()])); // await result - long lm = System.currentTimeMillis(); + long lm = EagRuntime.steadyTimeMillis(); do { + PlatformWebRTC.runScheduledTasks(); String c = PlatformWebRTC.clientLANAwaitDescription(); if(c != null) { logger.info("Relay [{}|{}] client sent description", displayRelay, displayCode); // 'this.descriptionHandler' was called, send result: - sock.writePacket(new IPacket04Description("", c)); + sock.writePacket(new RelayPacket04Description("", c)); connectState = SENT_DESCRIPTION; continue mainLoop; } EagUtils.sleep(20l); - }while(System.currentTimeMillis() - lm < 5000l); + }while(EagRuntime.steadyTimeMillis() - lm < 5000l); // no description was sent sock.close(); @@ -135,34 +139,35 @@ public class LANClientNetworkManager extends EaglercraftNetworkManager { logger.error("Relay [{}|{}] unexpected packet: IPacket01ICEServers in state {}", displayRelay, displayCode, initStateNames[connectState]); return null; } - }else if(pkt instanceof IPacket03ICECandidate) { + }else if(pkt instanceof RelayPacket03ICECandidate) { if(connectState == SENT_ICE_CANDIDATE) { // %%%%%% Process IPacket03ICECandidate %%%%%% - IPacket03ICECandidate ipkt = (IPacket03ICECandidate) pkt; + RelayPacket03ICECandidate ipkt = (RelayPacket03ICECandidate) pkt; // process logger.info("Relay [{}|{}] recieved server ICE candidate", displayRelay, displayCode); - PlatformWebRTC.clientLANSetICECandidate(ipkt.candidate); + PlatformWebRTC.clientLANSetICECandidate(ipkt.getCandidateString()); // await result - long lm = System.currentTimeMillis(); + long lm = EagRuntime.steadyTimeMillis(); do { + PlatformWebRTC.runScheduledTasks(); if(PlatformWebRTC.clientLANAwaitChannel()) { logger.info("Relay [{}|{}] client opened data channel", displayRelay, displayCode); // 'this.remoteDataChannelHandler' was called, success - sock.writePacket(new IPacket05ClientSuccess(ipkt.peerId)); + sock.writePacket(new RelayPacket05ClientSuccess(ipkt.peerId)); sock.close(); return new LANClientNetworkManager(displayCode, displayRelay); } EagUtils.sleep(20l); - }while(System.currentTimeMillis() - lm < 5000l); + }while(EagRuntime.steadyTimeMillis() - lm < 5000l); // no channel was opened - sock.writePacket(new IPacket06ClientFailure(ipkt.peerId)); + sock.writePacket(new RelayPacket06ClientFailure(ipkt.peerId)); sock.close(); logger.error("Relay [{}|{}] client open data channel timeout", displayRelay, displayCode); return null; @@ -172,32 +177,33 @@ public class LANClientNetworkManager extends EaglercraftNetworkManager { logger.error("Relay [{}|{}] unexpected packet: IPacket03ICECandidate in state {}", displayRelay, displayCode, initStateNames[connectState]); return null; } - }else if(pkt instanceof IPacket04Description) { + }else if(pkt instanceof RelayPacket04Description) { if(connectState == SENT_DESCRIPTION) { // %%%%%% Process IPacket04Description %%%%%% - IPacket04Description ipkt = (IPacket04Description) pkt; + RelayPacket04Description ipkt = (RelayPacket04Description) pkt; // process logger.info("Relay [{}|{}] recieved server description", displayRelay, displayCode); - PlatformWebRTC.clientLANSetDescription(ipkt.description); + PlatformWebRTC.clientLANSetDescription(ipkt.getDescriptionString()); // await result - long lm = System.currentTimeMillis(); + long lm = EagRuntime.steadyTimeMillis(); do { + PlatformWebRTC.runScheduledTasks(); String c = PlatformWebRTC.clientLANAwaitICECandidate(); if(c != null) { logger.info("Relay [{}|{}] client sent ICE candidate", displayRelay, displayCode); // 'this.iceCandidateHandler' was called, send result: - sock.writePacket(new IPacket03ICECandidate("", c)); + sock.writePacket(new RelayPacket03ICECandidate("", c)); connectState = SENT_ICE_CANDIDATE; continue mainLoop; } EagUtils.sleep(20l); - }while(System.currentTimeMillis() - lm < 5000l); + }while(EagRuntime.steadyTimeMillis() - lm < 5000l); // no ice candidates were sent sock.close(); @@ -209,12 +215,12 @@ public class LANClientNetworkManager extends EaglercraftNetworkManager { logger.error("Relay [{}|{}] unexpected packet: IPacket04Description in state {}", displayRelay, displayCode, initStateNames[connectState]); return null; } - }else if(pkt instanceof IPacketFFErrorCode) { + }else if(pkt instanceof RelayPacketFFErrorCode) { // %%%%%% Process IPacketFFErrorCode %%%%%% - IPacketFFErrorCode ipkt = (IPacketFFErrorCode) pkt; - logger.error("Relay [{}|{}] connection failed: {}({}): {}", displayRelay, displayCode, IPacketFFErrorCode.code2string(ipkt.code), ipkt.code, ipkt.desc); + RelayPacketFFErrorCode ipkt = (RelayPacketFFErrorCode) pkt; + logger.error("Relay [{}|{}] connection failed: {}({}): {}", displayRelay, displayCode, RelayPacketFFErrorCode.code2string(ipkt.code), ipkt.code, ipkt.desc); Throwable t; while((t = sock.getException()) != null) { logger.error(t); @@ -291,7 +297,7 @@ public class LANClientNetworkManager extends EaglercraftNetworkManager { return !clientDisconnected; } - private List fragmentedPacket = new ArrayList(); + private List fragmentedPacket = new ArrayList<>(); @Override public void processReceivedPackets() throws IOException { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANClientPeer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANClientPeer.java index 425f34b1..817bc4cf 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANClientPeer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANClientPeer.java @@ -3,6 +3,7 @@ package net.lax1dude.eaglercraft.v1_8.sp.lan; import java.util.Iterator; import java.util.List; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EagUtils; import net.lax1dude.eaglercraft.v1_8.internal.IPCPacketData; import net.lax1dude.eaglercraft.v1_8.internal.PlatformWebRTC; @@ -10,8 +11,8 @@ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.sp.SingleplayerServerController; import net.lax1dude.eaglercraft.v1_8.sp.internal.ClientPlatformSingleplayer; -import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket03ICECandidate; -import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket04Description; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket03ICECandidate; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket04Description; /** * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. @@ -47,12 +48,12 @@ class LANClientPeer { protected void handleICECandidates(String candidates) { if(state == SENT_DESCRIPTION) { PlatformWebRTC.serverLANPeerICECandidates(clientId, candidates); - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); do { LANPeerEvent evt; if((evt = PlatformWebRTC.serverLANGetEvent(clientId)) != null) { if(evt instanceof LANPeerEvent.LANPeerICECandidateEvent) { - LANServerController.lanRelaySocket.writePacket(new IPacket03ICECandidate(clientId, ((LANPeerEvent.LANPeerICECandidateEvent)evt).candidates)); + LANServerController.lanRelaySocket.writePacket(new RelayPacket03ICECandidate(clientId, ((LANPeerEvent.LANPeerICECandidateEvent)evt).candidates)); state = SENT_ICE_CANDIDATE; return; }else if(evt instanceof LANPeerEvent.LANPeerDisconnectEvent) { @@ -64,7 +65,7 @@ class LANClientPeer { return; } EagUtils.sleep(20l); - }while(System.currentTimeMillis() - millis < 5000l); + }while(EagRuntime.steadyTimeMillis() - millis < 5000l); logger.error("Getting server ICE candidates for '{}' timed out!", clientId); disconnect(); }else { @@ -75,12 +76,12 @@ class LANClientPeer { protected void handleDescription(String description) { if(state == PRE) { PlatformWebRTC.serverLANPeerDescription(clientId, description); - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); do { LANPeerEvent evt; if((evt = PlatformWebRTC.serverLANGetEvent(clientId)) != null) { if(evt instanceof LANPeerEvent.LANPeerDescriptionEvent) { - LANServerController.lanRelaySocket.writePacket(new IPacket04Description(clientId, ((LANPeerEvent.LANPeerDescriptionEvent)evt).description)); + LANServerController.lanRelaySocket.writePacket(new RelayPacket04Description(clientId, ((LANPeerEvent.LANPeerDescriptionEvent)evt).description)); state = SENT_DESCRIPTION; return; }else if(evt instanceof LANPeerEvent.LANPeerDisconnectEvent) { @@ -92,7 +93,7 @@ class LANClientPeer { return; } EagUtils.sleep(20l); - }while(System.currentTimeMillis() - millis < 5000l); + }while(EagRuntime.steadyTimeMillis() - millis < 5000l); logger.error("Getting server description for '{}' timed out!", clientId); disconnect(); }else { @@ -102,7 +103,7 @@ class LANClientPeer { protected void handleSuccess() { if(state == SENT_ICE_CANDIDATE) { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); do { LANPeerEvent evt; while((evt = PlatformWebRTC.serverLANGetEvent(clientId)) != null && evt instanceof LANPeerEvent.LANPeerICECandidateEvent) { @@ -122,7 +123,7 @@ class LANClientPeer { return; } EagUtils.sleep(20l); - }while(System.currentTimeMillis() - millis < 5000l); + }while(EagRuntime.steadyTimeMillis() - millis < 5000l); logger.error("Getting server description for '{}' timed out!", clientId); disconnect(); }else { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANServerController.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANServerController.java index 401b1988..c743baf0 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANServerController.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANServerController.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EagUtils; import net.lax1dude.eaglercraft.v1_8.internal.PlatformWebRTC; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; @@ -34,7 +35,7 @@ public class LANServerController { public static final Logger logger = LogManager.getLogger("LANServerController"); - public static final List currentICEServers = new ArrayList(); + public static final List currentICEServers = new ArrayList<>(); static RelayServerSocket lanRelaySocket = null; @@ -49,25 +50,26 @@ public class LANServerController { return null; }else { progressCallback.accept("Opening: " + sock.getURI()); - IPacket00Handshake hs = (IPacket00Handshake)sock.readPacket(); + RelayPacket00Handshake hs = (RelayPacket00Handshake)sock.readPacket(); lanRelaySocket = sock; String code = hs.connectionCode; logger.info("Relay [{}] connected as 'server', code: {}", sock.getURI(), code); progressCallback.accept("Opened '" + code + "' on " + sock.getURI()); - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); do { + sock.update(); if(sock.isClosed()) { logger.info("Relay [{}] connection lost", sock.getURI()); lanRelaySocket = null; return null; } - IPacket pkt = sock.readPacket(); + RelayPacket pkt = sock.readPacket(); if(pkt != null) { - if(pkt instanceof IPacket01ICEServers) { - IPacket01ICEServers ipkt = (IPacket01ICEServers)pkt; + if(pkt instanceof RelayPacket01ICEServers) { + RelayPacket01ICEServers ipkt = (RelayPacket01ICEServers)pkt; logger.info("Relay [{}] provided ICE servers:", sock.getURI()); currentICEServers.clear(); - for(net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.ICEServerSet.RelayServer srv : ipkt.servers) { + for(net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket01ICEServers.RelayServer srv : ipkt.servers) { logger.info("Relay [{}] {}: {}", sock.getURI(), srv.type.name(), srv.address); currentICEServers.add(srv.getICEString()); } @@ -80,7 +82,7 @@ public class LANServerController { } } EagUtils.sleep(50l); - }while(System.currentTimeMillis() - millis < 1000l); + }while(EagRuntime.steadyTimeMillis() - millis < 1000l); logger.info("Relay [{}] relay provide ICE servers timeout", sock.getURI()); closeLAN(); return null; @@ -131,54 +133,55 @@ public class LANServerController { return lanRelaySocket != null; } - private static final Map clients = new HashMap(); + private static final Map clients = new HashMap<>(); public static void updateLANServer() { if(lanRelaySocket != null) { - IPacket pkt; + lanRelaySocket.update(); + RelayPacket pkt; while((pkt = lanRelaySocket.readPacket()) != null) { - if(pkt instanceof IPacket02NewClient) { - IPacket02NewClient ipkt = (IPacket02NewClient) pkt; + if(pkt instanceof RelayPacket02NewClient) { + RelayPacket02NewClient ipkt = (RelayPacket02NewClient) pkt; if(clients.containsKey(ipkt.clientId)) { logger.error("Relay [{}] relay provided duplicate client '{}'", lanRelaySocket.getURI(), ipkt.clientId); }else { clients.put(ipkt.clientId, new LANClientPeer(ipkt.clientId)); } - }else if(pkt instanceof IPacket03ICECandidate) { - IPacket03ICECandidate ipkt = (IPacket03ICECandidate) pkt; + }else if(pkt instanceof RelayPacket03ICECandidate) { + RelayPacket03ICECandidate ipkt = (RelayPacket03ICECandidate) pkt; LANClientPeer c = clients.get(ipkt.peerId); if(c != null) { - c.handleICECandidates(ipkt.candidate); + c.handleICECandidates(ipkt.getCandidateString()); }else { logger.error("Relay [{}] relay sent IPacket03ICECandidate for unknown client '{}'", lanRelaySocket.getURI(), ipkt.peerId); } - }else if(pkt instanceof IPacket04Description) { - IPacket04Description ipkt = (IPacket04Description) pkt; + }else if(pkt instanceof RelayPacket04Description) { + RelayPacket04Description ipkt = (RelayPacket04Description) pkt; LANClientPeer c = clients.get(ipkt.peerId); if(c != null) { - c.handleDescription(ipkt.description); + c.handleDescription(ipkt.getDescriptionString()); }else { logger.error("Relay [{}] relay sent IPacket04Description for unknown client '{}'", lanRelaySocket.getURI(), ipkt.peerId); } - }else if(pkt instanceof IPacket05ClientSuccess) { - IPacket05ClientSuccess ipkt = (IPacket05ClientSuccess) pkt; + }else if(pkt instanceof RelayPacket05ClientSuccess) { + RelayPacket05ClientSuccess ipkt = (RelayPacket05ClientSuccess) pkt; LANClientPeer c = clients.get(ipkt.clientId); if(c != null) { c.handleSuccess(); }else { logger.error("Relay [{}] relay sent IPacket05ClientSuccess for unknown client '{}'", lanRelaySocket.getURI(), ipkt.clientId); } - }else if(pkt instanceof IPacket06ClientFailure) { - IPacket06ClientFailure ipkt = (IPacket06ClientFailure) pkt; + }else if(pkt instanceof RelayPacket06ClientFailure) { + RelayPacket06ClientFailure ipkt = (RelayPacket06ClientFailure) pkt; LANClientPeer c = clients.get(ipkt.clientId); if(c != null) { c.handleFailure(); }else { logger.error("Relay [{}] relay sent IPacket06ClientFailure for unknown client '{}'", lanRelaySocket.getURI(), ipkt.clientId); } - }else if(pkt instanceof IPacketFFErrorCode) { - IPacketFFErrorCode ipkt = (IPacketFFErrorCode) pkt; - logger.error("Relay [{}] error code thrown: {}({}): {}", lanRelaySocket.getURI(), IPacketFFErrorCode.code2string(ipkt.code), ipkt.code, ipkt.desc); + }else if(pkt instanceof RelayPacketFFErrorCode) { + RelayPacketFFErrorCode ipkt = (RelayPacketFFErrorCode) pkt; + logger.error("Relay [{}] error code thrown: {}({}): {}", lanRelaySocket.getURI(), RelayPacketFFErrorCode.code2string(ipkt.code), ipkt.code, ipkt.desc); Throwable t; while((t = lanRelaySocket.getException()) != null) { logger.error(t); @@ -186,6 +189,7 @@ public class LANServerController { }else { logger.error("Relay [{}] unexpected packet: {}", lanRelaySocket.getURI(), pkt.getClass().getSimpleName()); } + lanRelaySocket.update(); } if(lanRelaySocket.isClosed()) { lanRelaySocket = null; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANServerList.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANServerList.java index a9bd0025..d396f6ee 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANServerList.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/lan/LANServerList.java @@ -9,11 +9,12 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.internal.PlatformWebRTC; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayManager; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayServer; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayWorldsQuery; -import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket07LocalWorlds; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket07LocalWorlds; /** * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. @@ -32,15 +33,15 @@ import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket07LocalWorlds; */ public class LANServerList { - private final List lanServersList = new LinkedList(); - private final Map lanServersQueryList = new LinkedHashMap(); - private final Set deadURIs = new HashSet(); + private final List lanServersList = new LinkedList<>(); + private final Map lanServersQueryList = new LinkedHashMap<>(); + private final Set deadURIs = new HashSet<>(); private long lastRefresh = 0l; private int refreshCounter = 0; public boolean update() { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - lastRefresh > 20000l) { if(++refreshCounter < 10) { refresh(); @@ -54,6 +55,7 @@ public class LANServerList { Entry etr = itr.next(); String uri = etr.getKey(); RelayWorldsQuery q = etr.getValue(); + q.update(); if(!q.isQueryOpen()) { itr.remove(); if(q.isQueryFailed()) { @@ -75,9 +77,9 @@ public class LANServerList { } } if(rl != null) { - Iterator itr3 = q.getWorlds().iterator(); + Iterator itr3 = q.getWorlds().iterator(); yee: while(itr3.hasNext()) { - IPacket07LocalWorlds.LocalWorld l = itr3.next(); + RelayPacket07LocalWorlds.LocalWorld l = itr3.next(); itr2 = lanServersList.iterator(); while(itr2.hasNext()) { LanServer l2 = itr2.next(); @@ -116,7 +118,7 @@ public class LANServerList { } private void refresh() { - lastRefresh = System.currentTimeMillis(); + lastRefresh = EagRuntime.steadyTimeMillis(); if(PlatformWebRTC.supported()) { for(int i = 0, l = RelayManager.relayManager.count(); i < l; ++i) { RelayServer srv = RelayManager.relayManager.get(i); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayLoggerImpl.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayLoggerImpl.java new file mode 100644 index 00000000..c9344cc5 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayLoggerImpl.java @@ -0,0 +1,54 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay; + +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IRelayLogger; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class RelayLoggerImpl implements IRelayLogger { + + private final Logger impl; + + public RelayLoggerImpl(Logger impl) { + this.impl = impl; + } + + @Override + public void debug(String msg, Object... args) { + impl.debug(msg, args); + } + + @Override + public void info(String msg, Object... args) { + impl.debug(msg, args); + } + + @Override + public void warn(String msg, Object... args) { + impl.warn(msg, args); + } + + @Override + public void error(String msg, Object... args) { + impl.error(msg, args); + } + + @Override + public void error(Throwable th) { + impl.error(th); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayManager.java index 6c9f3a86..937d5803 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayManager.java @@ -14,9 +14,9 @@ import net.lax1dude.eaglercraft.v1_8.EaglerOutputStream; import net.lax1dude.eaglercraft.v1_8.ThreadLocalRandom; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket; -import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket00Handshake; -import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacketFFErrorCode; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket00Handshake; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacketFFErrorCode; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -43,7 +43,7 @@ public class RelayManager { public static final RelayManager relayManager = new RelayManager(); public static final int preferredRelayVersion = 1; - private final List relays = new ArrayList(); + private final List relays = new ArrayList<>(); private long lastPingThrough = 0l; public void load(byte[] relayConfig) { @@ -180,7 +180,7 @@ public class RelayManager { } public void ping() { - lastPingThrough = System.currentTimeMillis(); + lastPingThrough = EagRuntime.steadyTimeMillis(); for(int i = 0, l = relays.size(); i < l; ++i) { relays.get(i).ping(); } @@ -274,16 +274,18 @@ public class RelayManager { public RelayServerSocket connectHandshake(RelayServer relay, int type, String code) { RelayServerSocket sock = relay.openSocket(); while(!sock.isClosed()) { + sock.update(); if(sock.isOpen()) { - sock.writePacket(new IPacket00Handshake(type, preferredRelayVersion, code)); + sock.writePacket(new RelayPacket00Handshake(type, preferredRelayVersion, code)); while(!sock.isClosed()) { - IPacket pkt = sock.nextPacket(); + sock.update(); + RelayPacket pkt = sock.nextPacket(); if(pkt != null) { - if(pkt instanceof IPacket00Handshake) { + if(pkt instanceof RelayPacket00Handshake) { return sock; - }else if(pkt instanceof IPacketFFErrorCode) { - IPacketFFErrorCode ipkt = (IPacketFFErrorCode) pkt; - logger.error("Relay [{}] failed: {}({}): {}", relay.address, IPacketFFErrorCode.code2string(ipkt.code), ipkt.code, ipkt.desc); + }else if(pkt instanceof RelayPacketFFErrorCode) { + RelayPacketFFErrorCode ipkt = (RelayPacketFFErrorCode) pkt; + logger.error("Relay [{}] failed: {}({}): {}", relay.address, RelayPacketFFErrorCode.code2string(ipkt.code), ipkt.code, ipkt.desc); Throwable t; while((t = sock.getException()) != null) { logger.error(t); @@ -309,12 +311,12 @@ public class RelayManager { return null; } - private final List brokenServers = new LinkedList(); + private final List brokenServers = new LinkedList<>(); public RelayServerSocket getWorkingRelay(Consumer progressCallback, int type, String code) { brokenServers.clear(); if(!relays.isEmpty()) { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(millis - lastPingThrough < 10000l) { RelayServer relay = getPrimary(); if(relay.getPing() > 0l && relay.getPingCompatible().isCompatible()) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQuery.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQuery.java index 455fef82..05e24a2f 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQuery.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQuery.java @@ -28,6 +28,7 @@ public interface RelayQuery { } } + void update(); boolean isQueryOpen(); boolean isQueryFailed(); RateLimit isQueryRateLimit(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQueryImpl.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQueryImpl.java new file mode 100644 index 00000000..09c28da5 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQueryImpl.java @@ -0,0 +1,225 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.EaglerInputStream; +import net.lax1dude.eaglercraft.v1_8.internal.EnumEaglerConnectionState; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketClient; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketFrame; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformNetworking; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket00Handshake; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket69Pong; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket70SpecialUpdate; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacketFFErrorCode; +import net.lax1dude.eaglercraft.v1_8.update.UpdateService; + +/** + * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class RelayQueryImpl implements RelayQuery { + + private static final Logger logger = LogManager.getLogger("RelayQuery"); + private static final RelayLoggerImpl loggerImpl = new RelayLoggerImpl(LogManager.getLogger("RelayPacket")); + + private final IWebSocketClient sock; + private final String uri; + + private boolean failed; + + private boolean hasSentHandshake = false; + private boolean hasRecievedAnyData = false; + + private int vers = -1; + private String comment = ""; + private String brand = ""; + + private long connectionOpenedAt; + private long connectionPingStart = -1; + private long connectionPingTimer = -1; + + private RateLimit rateLimitStatus = RateLimit.NONE; + + private VersionMismatch versError = VersionMismatch.UNKNOWN; + + public RelayQueryImpl(String uri) { + this.uri = uri; + IWebSocketClient s; + try { + connectionOpenedAt = EagRuntime.steadyTimeMillis(); + s = PlatformNetworking.openWebSocketUnsafe(uri); + }catch(Throwable t) { + connectionOpenedAt = 0l; + sock = null; + failed = true; + return; + } + sock = s; + + } + + @Override + public void update() { + if(sock == null) return; + if(sock.availableStringFrames() > 0) { + logger.warn("[{}] discarding {} string frames recieved on a binary connection", uri, sock.availableStringFrames()); + sock.clearStringFrames(); + } + List frames = sock.getNextBinaryFrames(); + if(frames != null) { + for(int i = 0, l = frames.size(); i < l; ++i) { + hasRecievedAnyData = true; + byte[] arr = frames.get(i).getByteArray(); + if(arr.length == 2 && arr[0] == (byte)0xFC) { + if(arr[1] == (byte)0x00 || arr[1] == (byte)0x01) { + rateLimitStatus = RateLimit.BLOCKED; + RelayServerRateLimitTracker.setLimited(RelayQueryImpl.this.uri); + }else if(arr[1] == (byte)0x02) { + rateLimitStatus = RateLimit.NOW_LOCKED; + RelayServerRateLimitTracker.setLimitedLocked(RelayQueryImpl.this.uri); + }else { + rateLimitStatus = RateLimit.LOCKED; + RelayServerRateLimitTracker.setLocked(RelayQueryImpl.this.uri); + } + failed = true; + sock.close(); + }else { + try { + RelayPacket pkt = RelayPacket.readPacket(new DataInputStream(new EaglerInputStream(arr)), loggerImpl); + if(pkt instanceof RelayPacket69Pong) { + RelayPacket69Pong ipkt = (RelayPacket69Pong)pkt; + versError = VersionMismatch.COMPATIBLE; + if(connectionPingTimer == -1) { + connectionPingTimer = frames.get(i).getTimestamp() - connectionPingStart; + } + vers = ipkt.protcolVersion; + comment = ipkt.comment; + brand = ipkt.brand; + failed = false; + sock.close(); + return; + }else if(pkt instanceof RelayPacket70SpecialUpdate) { + RelayPacket70SpecialUpdate ipkt = (RelayPacket70SpecialUpdate)pkt; + if(ipkt.operation == RelayPacket70SpecialUpdate.OPERATION_UPDATE_CERTIFICATE) { + UpdateService.addCertificateToSet(ipkt.updatePacket); + } + }else if(pkt instanceof RelayPacketFFErrorCode) { + RelayPacketFFErrorCode ipkt = (RelayPacketFFErrorCode)pkt; + if(ipkt.code == RelayPacketFFErrorCode.TYPE_PROTOCOL_VERSION) { + String s1 = ipkt.desc.toLowerCase(); + if(s1.contains("outdated client") || s1.contains("client outdated")) { + versError = VersionMismatch.CLIENT_OUTDATED; + }else if(s1.contains("outdated server") || s1.contains("server outdated") || + s1.contains("outdated relay") || s1.contains("server relay")) { + versError = VersionMismatch.RELAY_OUTDATED; + }else { + versError = VersionMismatch.UNKNOWN; + } + } + logger.error("[{}] Recieved query error code {}: {}", uri, ipkt.code, ipkt.desc); + failed = true; + sock.close(); + return; + }else { + throw new IOException("Unexpected packet '" + pkt.getClass().getSimpleName() + "'"); + } + } catch (IOException e) { + logger.error("Relay query error: {}", e.toString()); + logger.error(e); + failed = true; + sock.close(); + return; + } + } + } + } + if(sock.isOpen() && !hasSentHandshake) { + hasSentHandshake = true; + try { + connectionPingStart = EagRuntime.steadyTimeMillis(); + sock.send(RelayPacket.writePacket(new RelayPacket00Handshake(0x03, RelayManager.preferredRelayVersion, ""), loggerImpl)); + } catch (IOException e) { + logger.error("Failed to write handshake: {}", e.toString()); + logger.error(e); + sock.close(); + failed = true; + } + } + if(sock.isClosed()) { + if(!hasRecievedAnyData) { + failed = true; + rateLimitStatus = RelayServerRateLimitTracker.isLimitedLong(uri); + } + } + if(EagRuntime.steadyTimeMillis() - connectionOpenedAt > 10000l) { + logger.error("Terminating connection that was open for too long: {}", uri); + sock.close(); + failed = true; + } + } + + @Override + public boolean isQueryOpen() { + return sock != null && !sock.isClosed(); + } + + @Override + public boolean isQueryFailed() { + return failed || sock == null || sock.getState() == EnumEaglerConnectionState.FAILED; + } + + @Override + public RateLimit isQueryRateLimit() { + return rateLimitStatus; + } + + @Override + public void close() { + if(sock != null) { + sock.close(); + } + } + + @Override + public int getVersion() { + return vers; + } + + @Override + public String getComment() { + return comment; + } + + @Override + public String getBrand() { + return brand; + } + + @Override + public long getPing() { + return connectionPingTimer < 1 ? 1 : connectionPingTimer; + } + + @Override + public VersionMismatch getCompatible() { + return versError; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQueryRateLimitDummy.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQueryRateLimitDummy.java new file mode 100644 index 00000000..059d0837 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayQueryRateLimitDummy.java @@ -0,0 +1,75 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay; + +/** + * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class RelayQueryRateLimitDummy implements RelayQuery { + + private final RateLimit type; + + public RelayQueryRateLimitDummy(RateLimit type) { + this.type = type; + } + + @Override + public void update() { + + } + + @Override + public boolean isQueryOpen() { + return false; + } + + @Override + public boolean isQueryFailed() { + return true; + } + + @Override + public RateLimit isQueryRateLimit() { + return type; + } + + @Override + public void close() { + } + + @Override + public int getVersion() { + return RelayManager.preferredRelayVersion; + } + + @Override + public String getComment() { + return "this query was rate limited"; + } + + @Override + public String getBrand() { + return "lax1dude"; + } + + @Override + public long getPing() { + return 0l; + } + + @Override + public VersionMismatch getCompatible() { + return VersionMismatch.COMPATIBLE; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServer.java index 9d464325..4ccf839a 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServer.java @@ -1,5 +1,6 @@ package net.lax1dude.eaglercraft.v1_8.sp.relay; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EagUtils; import net.lax1dude.eaglercraft.v1_8.internal.PlatformWebRTC; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayQuery.VersionMismatch; @@ -105,23 +106,26 @@ public class RelayServer { } public void update() { - if(query != null && !query.isQueryOpen()) { - if(query.isQueryFailed()) { - queriedVersion = -1; - queriedComment = null; - queriedVendor = null; - queriedCompatible = VersionMismatch.UNKNOWN; - ping = 0l; - }else { - queriedVersion = query.getVersion(); - queriedComment = query.getComment(); - queriedVendor = query.getBrand(); - ping = query.getPing(); - queriedCompatible = query.getCompatible(); - workingPing = ping; + if(query != null) { + query.update(); + if(!query.isQueryOpen()) { + if(query.isQueryFailed()) { + queriedVersion = -1; + queriedComment = null; + queriedVendor = null; + queriedCompatible = VersionMismatch.UNKNOWN; + ping = 0l; + }else { + queriedVersion = query.getVersion(); + queriedComment = query.getComment(); + queriedVendor = query.getBrand(); + ping = query.getPing(); + queriedCompatible = query.getCompatible(); + workingPing = ping; + } + lastPing = EagRuntime.steadyTimeMillis(); + query = null; } - lastPing = System.currentTimeMillis(); - query = null; } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerRateLimitTracker.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerRateLimitTracker.java new file mode 100644 index 00000000..e7b34d65 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerRateLimitTracker.java @@ -0,0 +1,98 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay; + +import java.util.HashMap; +import java.util.Map; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class RelayServerRateLimitTracker { + + private static final Map relayQueryLimited = new HashMap<>(); + private static final Map relayQueryLocked = new HashMap<>(); + + public static void setLimited(String str) { + synchronized(relayQueryLimited) { + relayQueryLimited.put(str, EagRuntime.steadyTimeMillis()); + } + } + + public static void setLocked(String str) { + synchronized(relayQueryLocked) { + relayQueryLocked.put(str, EagRuntime.steadyTimeMillis()); + } + } + + public static void setLimitedLocked(String str) { + long now = EagRuntime.steadyTimeMillis(); + synchronized(relayQueryLimited) { + relayQueryLimited.put(str, now); + } + synchronized(relayQueryLocked) { + relayQueryLocked.put(str, now); + } + } + + public static RelayQuery.RateLimit isLimited(String str) { + long now = EagRuntime.steadyTimeMillis(); + synchronized(relayQueryLocked) { + Long l = relayQueryLocked.get(str); + if(l != null && now - l.longValue() < 60000l) { + return RelayQuery.RateLimit.LOCKED; + } + } + synchronized(relayQueryLimited) { + Long l = relayQueryLimited.get(str); + if(l != null && now - l.longValue() < 10000l) { + return RelayQuery.RateLimit.BLOCKED; + } + } + return RelayQuery.RateLimit.NONE; + } + + public static RelayQuery.RateLimit isLimitedLong(String str) { + long now = EagRuntime.steadyTimeMillis(); + synchronized(relayQueryLocked) { + Long l = relayQueryLocked.get(str); + if(l != null && now - l.longValue() < 400000l) { + return RelayQuery.RateLimit.LOCKED; + } + } + synchronized(relayQueryLimited) { + Long l = relayQueryLimited.get(str); + if(l != null && now - l.longValue() < 900000l) { + return RelayQuery.RateLimit.BLOCKED; + } + } + return RelayQuery.RateLimit.NONE; + } + + public static RelayQuery.RateLimit isLimitedEver(String str) { + synchronized(relayQueryLocked) { + if(relayQueryLocked.containsKey(str)) { + return RelayQuery.RateLimit.LOCKED; + } + } + synchronized(relayQueryLimited) { + if(relayQueryLimited.containsKey(str)) { + return RelayQuery.RateLimit.BLOCKED; + } + } + return RelayQuery.RateLimit.NONE; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocket.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocket.java index e21cb850..e5fdbf02 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocket.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocket.java @@ -1,6 +1,6 @@ package net.lax1dude.eaglercraft.v1_8.sp.relay; -import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket; /** * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. @@ -19,6 +19,7 @@ import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket; */ public interface RelayServerSocket { + void update(); boolean isOpen(); boolean isClosed(); void close(); @@ -26,10 +27,10 @@ public interface RelayServerSocket { boolean isFailed(); Throwable getException(); - void writePacket(IPacket pkt); + void writePacket(RelayPacket pkt); - IPacket readPacket(); - IPacket nextPacket(); + RelayPacket readPacket(); + RelayPacket nextPacket(); RelayQuery.RateLimit getRatelimitHistory(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocketImpl.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocketImpl.java new file mode 100644 index 00000000..05885f48 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocketImpl.java @@ -0,0 +1,173 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.internal.EnumEaglerConnectionState; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketClient; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketFrame; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformNetworking; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket70SpecialUpdate; +import net.lax1dude.eaglercraft.v1_8.update.UpdateService; + +/** + * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class RelayServerSocketImpl implements RelayServerSocket { + + private static final Logger logger = LogManager.getLogger("RelayServerSocket"); + private static final RelayLoggerImpl loggerImpl = new RelayLoggerImpl(LogManager.getLogger("RelayPacket")); + + private final IWebSocketClient sock; + private final String uri; + + private boolean hasRecievedAnyData = false; + private boolean failed = false; + + private final List exceptions = new LinkedList<>(); + private final List packets = new LinkedList<>(); + + public RelayServerSocketImpl(String uri, int timeout) { + this.uri = uri; + IWebSocketClient s; + try { + s = PlatformNetworking.openWebSocketUnsafe(uri); + }catch(Throwable t) { + exceptions.add(t); + sock = null; + failed = true; + return; + } + sock = s; + } + + @Override + public void update() { + if(sock == null) return; + if(sock.availableStringFrames() > 0) { + logger.warn("[{}] discarding {} string frames recieved on a binary connection", uri, sock.availableStringFrames()); + sock.clearStringFrames(); + } + List frames = sock.getNextBinaryFrames(); + if(frames != null) { + for(int i = 0, l = frames.size(); i < l; ++i) { + hasRecievedAnyData = true; + try { + RelayPacket pkt = RelayPacket.readPacket(new DataInputStream(frames.get(i).getInputStream()), loggerImpl); + if(pkt instanceof RelayPacket70SpecialUpdate) { + RelayPacket70SpecialUpdate ipkt = (RelayPacket70SpecialUpdate)pkt; + if(ipkt.operation == RelayPacket70SpecialUpdate.OPERATION_UPDATE_CERTIFICATE) { + UpdateService.addCertificateToSet(ipkt.updatePacket); + } + }else { + packets.add(pkt); + } + } catch (IOException e) { + exceptions.add(e); + logger.error("[{}] Relay Socket Error: {}", uri, e.toString()); + EagRuntime.debugPrintStackTrace(e); + failed = true; + sock.close(); + return; + } + } + } + if(sock.isClosed()) { + if (!hasRecievedAnyData) { + failed = true; + } + } + } + + @Override + public boolean isOpen() { + return sock != null && sock.isOpen(); + } + + @Override + public boolean isClosed() { + return sock == null || sock.isClosed(); + } + + @Override + public void close() { + if(sock != null) { + sock.close(); + } + } + + @Override + public boolean isFailed() { + return failed || sock == null || sock.getState() == EnumEaglerConnectionState.FAILED; + } + + @Override + public Throwable getException() { + if(!exceptions.isEmpty()) { + return exceptions.remove(0); + }else { + return null; + } + } + + @Override + public void writePacket(RelayPacket pkt) { + if(sock != null) { + try { + sock.send(RelayPacket.writePacket(pkt, loggerImpl)); + } catch (Throwable e) { + logger.error("Relay connection error: {}", e.toString()); + EagRuntime.debugPrintStackTrace(e); + exceptions.add(e); + sock.close(); + } + } + } + + @Override + public RelayPacket readPacket() { + if(!packets.isEmpty()) { + return packets.remove(0); + }else { + return null; + } + } + + @Override + public RelayPacket nextPacket() { + if(!packets.isEmpty()) { + return packets.get(0); + }else { + return null; + } + } + + @Override + public RelayQuery.RateLimit getRatelimitHistory() { + return RelayServerRateLimitTracker.isLimitedEver(uri); + } + + @Override + public String getURI() { + return uri; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocketRateLimitDummy.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocketRateLimitDummy.java new file mode 100644 index 00000000..e00aa10e --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayServerSocketRateLimitDummy.java @@ -0,0 +1,80 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay; + +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket; + +/** + * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class RelayServerSocketRateLimitDummy implements RelayServerSocket { + + private final RelayQuery.RateLimit limit; + + public RelayServerSocketRateLimitDummy(RelayQuery.RateLimit limit) { + this.limit = limit; + } + + @Override + public void update() { + } + + @Override + public boolean isOpen() { + return false; + } + + @Override + public boolean isClosed() { + return true; + } + + @Override + public void close() { + } + + @Override + public boolean isFailed() { + return true; + } + + @Override + public Throwable getException() { + return null; + } + + @Override + public void writePacket(RelayPacket pkt) { + } + + @Override + public RelayPacket readPacket() { + return null; + } + + @Override + public RelayPacket nextPacket() { + return null; + } + + @Override + public RelayQuery.RateLimit getRatelimitHistory() { + return limit; + } + + @Override + public String getURI() { + return ""; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQuery.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQuery.java index 787b8a80..ae461fc8 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQuery.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQuery.java @@ -3,7 +3,7 @@ package net.lax1dude.eaglercraft.v1_8.sp.relay; import java.util.List; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayQuery.VersionMismatch; -import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket07LocalWorlds; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket07LocalWorlds; /** * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. @@ -22,12 +22,13 @@ import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket07LocalWorlds; */ public interface RelayWorldsQuery { + void update(); boolean isQueryOpen(); boolean isQueryFailed(); RelayQuery.RateLimit isQueryRateLimit(); void close(); - List getWorlds(); + List getWorlds(); VersionMismatch getCompatible(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQueryImpl.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQueryImpl.java new file mode 100644 index 00000000..d8a68cb4 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQueryImpl.java @@ -0,0 +1,197 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.EaglerInputStream; +import net.lax1dude.eaglercraft.v1_8.internal.EnumEaglerConnectionState; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketClient; +import net.lax1dude.eaglercraft.v1_8.internal.IWebSocketFrame; +import net.lax1dude.eaglercraft.v1_8.internal.PlatformNetworking; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayQuery.RateLimit; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket00Handshake; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket07LocalWorlds; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket70SpecialUpdate; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacketFFErrorCode; +import net.lax1dude.eaglercraft.v1_8.update.UpdateService; + +/** + * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class RelayWorldsQueryImpl implements RelayWorldsQuery { + + private static final Logger logger = LogManager.getLogger("RelayWorldsQuery"); + private static final RelayLoggerImpl loggerImpl = new RelayLoggerImpl(LogManager.getLogger("RelayPacket")); + + private final IWebSocketClient sock; + private final String uri; + + private boolean failed; + + private long openedAt; + + private boolean hasSentHandshake = false; + private boolean hasRecievedAnyData = false; + private RelayQuery.RateLimit rateLimitStatus = RelayQuery.RateLimit.NONE; + + private RelayQuery.VersionMismatch versError = RelayQuery.VersionMismatch.UNKNOWN; + + private List worlds = null; + + public RelayWorldsQueryImpl(String uri) { + this.uri = uri; + IWebSocketClient s; + try { + openedAt = EagRuntime.steadyTimeMillis(); + s = PlatformNetworking.openWebSocketUnsafe(uri); + }catch(Throwable t) { + sock = null; + failed = true; + return; + } + sock = s; + } + + @Override + public void update() { + if(sock == null) return; + if(sock.availableStringFrames() > 0) { + logger.warn("[{}] discarding {} string frames recieved on a binary connection", uri, sock.availableStringFrames()); + sock.clearStringFrames(); + } + List frames = sock.getNextBinaryFrames(); + if(frames != null) { + for(int i = 0, l = frames.size(); i < l; ++i) { + hasRecievedAnyData = true; + byte[] arr = frames.get(i).getByteArray(); + if(arr.length == 2 && arr[0] == (byte)0xFC) { + if(arr[1] == (byte)0x00 || arr[1] == (byte)0x01) { + rateLimitStatus = RateLimit.BLOCKED; + RelayServerRateLimitTracker.setLimited(RelayWorldsQueryImpl.this.uri); + }else if(arr[1] == (byte)0x02) { + rateLimitStatus = RateLimit.NOW_LOCKED; + RelayServerRateLimitTracker.setLimitedLocked(RelayWorldsQueryImpl.this.uri); + }else { + rateLimitStatus = RateLimit.LOCKED; + RelayServerRateLimitTracker.setLocked(RelayWorldsQueryImpl.this.uri); + } + failed = true; + sock.close(); + }else { + try { + RelayPacket pkt = RelayPacket.readPacket(new DataInputStream(new EaglerInputStream(arr)), loggerImpl); + if(pkt instanceof RelayPacket07LocalWorlds) { + worlds = ((RelayPacket07LocalWorlds)pkt).worldsList; + sock.close(); + failed = false; + return; + }else if(pkt instanceof RelayPacket70SpecialUpdate) { + RelayPacket70SpecialUpdate ipkt = (RelayPacket70SpecialUpdate)pkt; + if(ipkt.operation == RelayPacket70SpecialUpdate.OPERATION_UPDATE_CERTIFICATE) { + UpdateService.addCertificateToSet(ipkt.updatePacket); + } + }else if(pkt instanceof RelayPacketFFErrorCode) { + RelayPacketFFErrorCode ipkt = (RelayPacketFFErrorCode)pkt; + if(ipkt.code == RelayPacketFFErrorCode.TYPE_PROTOCOL_VERSION) { + String s1 = ipkt.desc.toLowerCase(); + if(s1.contains("outdated client") || s1.contains("client outdated")) { + versError = RelayQuery.VersionMismatch.CLIENT_OUTDATED; + }else if(s1.contains("outdated server") || s1.contains("server outdated") || + s1.contains("outdated relay") || s1.contains("server relay")) { + versError = RelayQuery.VersionMismatch.RELAY_OUTDATED; + }else { + versError = RelayQuery.VersionMismatch.UNKNOWN; + } + } + logger.error("[{}] Recieved query error code {}: {}", uri, ipkt.code, ipkt.desc); + failed = true; + sock.close(); + return; + }else { + throw new IOException("Unexpected packet '" + pkt.getClass().getSimpleName() + "'"); + } + } catch (IOException e) { + logger.error("Relay World Query Error: {}", e.toString()); + EagRuntime.debugPrintStackTrace(e); + failed = true; + sock.close(); + return; + } + } + } + } + if(sock.isOpen() && !hasSentHandshake) { + hasSentHandshake = true; + try { + sock.send(RelayPacket.writePacket(new RelayPacket00Handshake(0x04, RelayManager.preferredRelayVersion, ""), loggerImpl)); + } catch (IOException e) { + logger.error("Failed to write handshake: {}", e.toString()); + logger.error(e); + sock.close(); + failed = true; + } + } + if(sock.isClosed()) { + if(!hasRecievedAnyData) { + failed = true; + rateLimitStatus = RelayServerRateLimitTracker.isLimitedLong(uri); + } + }else { + if(EagRuntime.steadyTimeMillis() - openedAt > 10000l) { + logger.error("Terminating connection that was open for too long: {}", uri); + sock.close(); + failed = true; + } + } + } + + @Override + public boolean isQueryOpen() { + return sock != null && !sock.isClosed(); + } + + @Override + public boolean isQueryFailed() { + return failed || sock == null || sock.getState() == EnumEaglerConnectionState.FAILED; + } + + @Override + public RelayQuery.RateLimit isQueryRateLimit() { + return rateLimitStatus; + } + + @Override + public void close() { + if(sock != null) { + sock.close(); + } + } + + @Override + public List getWorlds() { + return worlds; + } + + @Override + public RelayQuery.VersionMismatch getCompatible() { + return versError; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQueryRateLimitDummy.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQueryRateLimitDummy.java new file mode 100644 index 00000000..2915c825 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/RelayWorldsQueryRateLimitDummy.java @@ -0,0 +1,64 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay; + +import java.util.ArrayList; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket07LocalWorlds; + +/** + * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class RelayWorldsQueryRateLimitDummy implements RelayWorldsQuery { + + private final RelayQuery.RateLimit rateLimit; + + public RelayWorldsQueryRateLimitDummy(RelayQuery.RateLimit rateLimit) { + this.rateLimit = rateLimit; + } + + @Override + public void update() { + } + + @Override + public boolean isQueryOpen() { + return false; + } + + @Override + public boolean isQueryFailed() { + return true; + } + + @Override + public RelayQuery.RateLimit isQueryRateLimit() { + return rateLimit; + } + + @Override + public void close() { + } + + @Override + public List getWorlds() { + return new ArrayList<>(0); + } + + @Override + public RelayQuery.VersionMismatch getCompatible() { + return RelayQuery.VersionMismatch.COMPATIBLE; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/ICEServerSet.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/ICEServerSet.java deleted file mode 100644 index 2ac4b9d0..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/ICEServerSet.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -/** - * Copyright (c) 2022 lax1dude. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class ICEServerSet { - - public static enum RelayType { - STUN, TURN; - } - - public static class RelayServer { - - public final RelayType type; - public final String address; - public final String username; - public final String password; - - protected RelayServer(RelayType type, String address, String username, String password) { - this.type = type; - this.address = address; - this.username = username; - this.password = password; - } - - protected RelayServer(RelayType type, String address) { - this.type = type; - this.address = address; - this.username = null; - this.password = null; - } - - public String getICEString() { - if(username == null) { - return address; - }else { - return address + ";" + username + ";" + password; - } - } - - } - -} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket.java deleted file mode 100644 index 33dc3a09..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket.java +++ /dev/null @@ -1,165 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Map; - -import net.lax1dude.eaglercraft.v1_8.EaglerOutputStream; -import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; -import net.lax1dude.eaglercraft.v1_8.log4j.Logger; - -/** - * Copyright (c) 2022 lax1dude. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class IPacket { - - private static final Logger logger = LogManager.getLogger("RelayPacket"); - - private static final Map> definedPacketClasses = new HashMap(); - private static final Map,Integer> definedPacketIds = new HashMap(); - - private static void register(int id, Class clazz) { - definedPacketClasses.put(id, clazz); - definedPacketIds.put(clazz, id); - } - - static { - register(0x00, IPacket00Handshake.class); - register(0x01, IPacket01ICEServers.class); - register(0x02, IPacket02NewClient.class); - register(0x03, IPacket03ICECandidate.class); - register(0x04, IPacket04Description.class); - register(0x05, IPacket05ClientSuccess.class); - register(0x06, IPacket06ClientFailure.class); - register(0x07, IPacket07LocalWorlds.class); - register(0x69, IPacket69Pong.class); - register(0x70, IPacket70SpecialUpdate.class); - register(0xFE, IPacketFEDisconnectClient.class); - register(0xFF, IPacketFFErrorCode.class); - } - - public static IPacket readPacket(DataInputStream input) throws IOException { - int i = input.read(); - try { - Class clazz = definedPacketClasses.get(i); - if(clazz == null) { - throw new IOException("Unknown packet type: " + i); - } - IPacket pkt = clazz.newInstance(); - pkt.read(input); - return pkt; - } catch (InstantiationException | IllegalAccessException e) { - throw new IOException("Unknown packet type: " + i); - } - } - - public static byte[] writePacket(IPacket packet) throws IOException { - Integer i = definedPacketIds.get(packet.getClass()); - if(i != null) { - int len = packet.packetLength(); - EaglerOutputStream bao = len == -1 ? new EaglerOutputStream() : - new EaglerOutputStream(len + 1); - bao.write(i); - packet.write(new DataOutputStream(bao)); - byte[] ret = bao.toByteArray(); - if(len != -1 && ret.length != len + 1) { - logger.error("writePacket buffer for packet {} {} by {} bytes", packet.getClass().getSimpleName(), - (len + 1 < ret.length ? "overflowed" : "underflowed"), - (len + 1 < ret.length ? ret.length - len - 1 : len + 1 - ret.length)); - } - return ret; - }else { - throw new IOException("Unknown packet type: " + packet.getClass().getSimpleName()); - } - } - - public void read(DataInputStream input) throws IOException { - } - - public void write(DataOutputStream output) throws IOException { - } - - public int packetLength() { - return -1; - } - - public static String readASCII(InputStream is, int len) throws IOException { - char[] ret = new char[len]; - for(int i = 0; i < len; ++i) { - int j = is.read(); - if(j < 0) { - return null; - } - ret[i] = (char)j; - } - return new String(ret); - } - - public static void writeASCII(OutputStream is, String txt) throws IOException { - for(int i = 0, l = txt.length(); i < l; ++i) { - is.write((int)txt.charAt(i)); - } - } - - public static String readASCII8(InputStream is) throws IOException { - int i = is.read(); - if(i < 0) { - return null; - }else { - return readASCII(is, i); - } - } - - public static void writeASCII8(OutputStream is, String txt) throws IOException { - if(txt == null) { - is.write(0); - }else { - int l = txt.length(); - is.write(l); - for(int i = 0; i < l; ++i) { - is.write((int)txt.charAt(i)); - } - } - } - - public static String readASCII16(InputStream is) throws IOException { - int hi = is.read(); - int lo = is.read(); - if(hi < 0 || lo < 0) { - return null; - }else { - return readASCII(is, (hi << 8) | lo); - } - } - - public static void writeASCII16(OutputStream is, String txt) throws IOException { - if(txt == null) { - is.write(0); - is.write(0); - }else { - int l = txt.length(); - is.write((l >>> 8) & 0xFF); - is.write(l & 0xFF); - for(int i = 0; i < l; ++i) { - is.write((int)txt.charAt(i)); - } - } - } - -} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket00Handshake.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket00Handshake.java deleted file mode 100644 index 1c224902..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket00Handshake.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** - * Copyright (c) 2022 lax1dude. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class IPacket00Handshake extends IPacket { - - public int connectionType = 0; - public int connectionVersion = 1; - public String connectionCode = null; - - public IPacket00Handshake() { - } - - public IPacket00Handshake(int connectionType, int connectionVersion, - String connectionCode) { - this.connectionType = connectionType; - this.connectionVersion = connectionVersion; - this.connectionCode = connectionCode; - } - - @Override - public void read(DataInputStream input) throws IOException { - connectionType = input.read(); - connectionVersion = input.read(); - connectionCode = IPacket.readASCII8(input); - } - - @Override - public void write(DataOutputStream output) throws IOException { - output.write(connectionType); - output.write(connectionVersion); - IPacket.writeASCII8(output, connectionCode); - } - - @Override - public int packetLength() { - return 1 + 1 + (connectionCode != null ? 1 + connectionCode.length() : 0); - } - -} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket01ICEServers.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket01ICEServers.java deleted file mode 100644 index 7d327612..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket01ICEServers.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -import java.io.DataInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; - -/** - * Copyright (c) 2022 lax1dude. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class IPacket01ICEServers extends IPacket { - - public final Collection servers; - - public IPacket01ICEServers() { - servers = new ArrayList(); - } - - public void read(DataInputStream input) throws IOException { - servers.clear(); - int l = input.readUnsignedShort(); - for(int i = 0; i < l; ++i) { - char type = (char)input.read(); - ICEServerSet.RelayType typeEnum; - if(type == 'S') { - typeEnum = ICEServerSet.RelayType.STUN; - }else if(type == 'T') { - typeEnum = ICEServerSet.RelayType.TURN; - }else { - throw new IOException("Unknown/Unsupported Relay Type: '" + type + "'"); - } - servers.add(new ICEServerSet.RelayServer( - typeEnum, - readASCII16(input), - readASCII8(input), - readASCII8(input) - )); - } - } - -} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket03ICECandidate.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket03ICECandidate.java deleted file mode 100644 index 985f0409..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket03ICECandidate.java +++ /dev/null @@ -1,49 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** - * Copyright (c) 2022 lax1dude. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class IPacket03ICECandidate extends IPacket { - - public String peerId; - public String candidate; - - public IPacket03ICECandidate(String peerId, String desc) { - this.peerId = peerId; - this.candidate = desc; - } - - public IPacket03ICECandidate() { - } - - public void read(DataInputStream input) throws IOException { - peerId = readASCII8(input); - candidate = readASCII16(input); - } - - public void write(DataOutputStream output) throws IOException { - writeASCII8(output, peerId); - writeASCII16(output, candidate); - } - - public int packetLength() { - return 1 + peerId.length() + 2 + candidate.length(); - } - -} \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket04Description.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket04Description.java deleted file mode 100644 index 6bc3f8f1..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket04Description.java +++ /dev/null @@ -1,49 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** - * Copyright (c) 2022 lax1dude. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class IPacket04Description extends IPacket { - - public String peerId; - public String description; - - public IPacket04Description(String peerId, String desc) { - this.peerId = peerId; - this.description = desc; - } - - public IPacket04Description() { - } - - public void read(DataInputStream input) throws IOException { - peerId = readASCII8(input); - description = readASCII16(input); - } - - public void write(DataOutputStream output) throws IOException { - writeASCII8(output, peerId); - writeASCII16(output, description); - } - - public int packetLength() { - return 1 + peerId.length() + 2 + description.length(); - } - -} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacketFEDisconnectClient.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacketFEDisconnectClient.java deleted file mode 100644 index 77a787fd..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacketFEDisconnectClient.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Copyright (c) 2022 lax1dude. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class IPacketFEDisconnectClient extends IPacket { - - public static final int TYPE_FINISHED_SUCCESS = 0x00; - public static final int TYPE_FINISHED_FAILED = 0x01; - public static final int TYPE_TIMEOUT = 0x02; - public static final int TYPE_INVALID_OPERATION = 0x03; - public static final int TYPE_INTERNAL_ERROR = 0x04; - public static final int TYPE_SERVER_DISCONNECT = 0x05; - public static final int TYPE_UNKNOWN = 0xFF; - - public String clientId; - public int code; - public String reason; - - public IPacketFEDisconnectClient() { - } - - public IPacketFEDisconnectClient(String clientId, int code, String reason) { - this.clientId = clientId; - this.code = code; - this.reason = reason; - } - - public void read(DataInputStream input) throws IOException { - clientId = readASCII8(input); - code = input.read(); - reason = readASCII16(input); - } - - public void write(DataOutputStream output) throws IOException { - writeASCII8(output, clientId); - output.write(code); - writeASCII16(output, reason); - } - - public int packetLength() { - return -1; - } - - public static final ByteBuffer ratelimitPacketTooMany = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x00 }); - public static final ByteBuffer ratelimitPacketBlock = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x01 }); - public static final ByteBuffer ratelimitPacketBlockLock = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x02 }); - public static final ByteBuffer ratelimitPacketLocked = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x03 }); - -} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacketFFErrorCode.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacketFFErrorCode.java deleted file mode 100644 index 4475d75b..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacketFFErrorCode.java +++ /dev/null @@ -1,82 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** - * Copyright (c) 2022 lax1dude. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class IPacketFFErrorCode extends IPacket { - - public static final int TYPE_INTERNAL_ERROR = 0x00; - public static final int TYPE_PROTOCOL_VERSION = 0x01; - public static final int TYPE_INVALID_PACKET = 0x02; - public static final int TYPE_ILLEGAL_OPERATION = 0x03; - public static final int TYPE_CODE_LENGTH = 0x04; - public static final int TYPE_INCORRECT_CODE = 0x05; - public static final int TYPE_SERVER_DISCONNECTED = 0x06; - public static final int TYPE_UNKNOWN_CLIENT = 0x07; - - public static final String[] packetTypes = new String[0x08]; - - static { - packetTypes[TYPE_INTERNAL_ERROR] = "TYPE_INTERNAL_ERROR"; - packetTypes[TYPE_PROTOCOL_VERSION] = "TYPE_PROTOCOL_VERSION"; - packetTypes[TYPE_INVALID_PACKET] = "TYPE_INVALID_PACKET"; - packetTypes[TYPE_ILLEGAL_OPERATION] = "TYPE_ILLEGAL_OPERATION"; - packetTypes[TYPE_CODE_LENGTH] = "TYPE_CODE_LENGTH"; - packetTypes[TYPE_INCORRECT_CODE] = "TYPE_INCORRECT_CODE"; - packetTypes[TYPE_SERVER_DISCONNECTED] = "TYPE_SERVER_DISCONNECTED"; - packetTypes[TYPE_UNKNOWN_CLIENT] = "TYPE_UNKNOWN_CLIENT"; - } - - public static String code2string(int i) { - if(i >= 0 || i < packetTypes.length) { - return packetTypes[i]; - }else { - return "UNKNOWN"; - } - } - - public int code; - public String desc; - - public IPacketFFErrorCode() { - } - - public IPacketFFErrorCode(int code, String desc) { - this.code = code; - this.desc = desc; - } - - @Override - public void read(DataInputStream input) throws IOException { - code = input.read(); - desc = readASCII16(input); - } - - @Override - public void write(DataOutputStream input) throws IOException { - input.write(code); - writeASCII16(input, desc); - } - - @Override - public int packetLength() { - return 1 + 2 + desc.length(); - } - -} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerChunkLoader.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerChunkLoader.java index 403280b6..11bd903e 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerChunkLoader.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerChunkLoader.java @@ -71,7 +71,7 @@ public class EaglerChunkLoader extends AnvilChunkLoader { @Override public Chunk loadChunk(World var1, int var2, int var3) throws IOException { - VFile2 file = new VFile2(chunkDirectory, getChunkPath(var2, var3) + ".dat"); + VFile2 file = WorldsDB.newVFile(chunkDirectory, getChunkPath(var2, var3) + ".dat"); if(!file.exists()) { return null; } @@ -93,7 +93,7 @@ public class EaglerChunkLoader extends AnvilChunkLoader { this.writeChunkToNBT(var2, var1, chunkData); NBTTagCompound fileData = new NBTTagCompound(); fileData.setTag("Level", chunkData); - VFile2 file = new VFile2(chunkDirectory, getChunkPath(var2.xPosition, var2.zPosition) + ".dat"); + VFile2 file = WorldsDB.newVFile(chunkDirectory, getChunkPath(var2.xPosition, var2.zPosition) + ".dat"); try(OutputStream os = file.getOutputStream()) { CompressedStreamTools.writeCompressed(fileData, os); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerIntegratedServerWorker.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerIntegratedServerWorker.java index 20c96bee..0aad61cb 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerIntegratedServerWorker.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerIntegratedServerWorker.java @@ -58,7 +58,7 @@ public class EaglerIntegratedServerWorker { public static final EaglerSaveFormat saveFormat = new EaglerSaveFormat(EaglerSaveFormat.worldsFolder); - private static final Map openChannels = new HashMap(); + private static final Map openChannels = new HashMap<>(); private static final IPCPacketManager packetManagerInstance = new IPCPacketManager(); @@ -197,7 +197,7 @@ public class EaglerIntegratedServerWorker { } String[] worldsTxt = EaglerSaveFormat.worldsList.getAllLines(); if(worldsTxt != null) { - List newWorlds = new ArrayList(); + List newWorlds = new ArrayList<>(); for(int i = 0; i < worldsTxt.length; ++i) { String str = worldsTxt[i]; if(!str.equalsIgnoreCase(pkt.worldName)) { @@ -301,18 +301,18 @@ public class EaglerIntegratedServerWorker { }else { String[] worlds = EaglerSaveFormat.worldsList.getAllLines(); if(worlds == null) { - sendIPCPacket(new IPCPacket16NBTList(IPCPacket16NBTList.WORLD_LIST, new LinkedList())); + sendIPCPacket(new IPCPacket16NBTList(IPCPacket16NBTList.WORLD_LIST, new LinkedList<>())); break; } - LinkedHashSet updatedList = new LinkedHashSet(); - LinkedList sendListNBT = new LinkedList(); + LinkedHashSet updatedList = new LinkedHashSet<>(); + LinkedList sendListNBT = new LinkedList<>(); boolean rewrite = false; for(int i = 0; i < worlds.length; ++i) { String w = worlds[i].trim(); if(w.length() > 0) { - VFile2 vf = new VFile2(EaglerSaveFormat.worldsFolder, w, "level.dat"); + VFile2 vf = WorldsDB.newVFile(EaglerSaveFormat.worldsFolder, w, "level.dat"); if(!vf.exists()) { - vf = new VFile2(EaglerSaveFormat.worldsFolder, w, "level.dat_old"); + vf = WorldsDB.newVFile(EaglerSaveFormat.worldsFolder, w, "level.dat_old"); } if(vf.exists()) { try(InputStream dat = vf.getInputStream()) { @@ -391,8 +391,8 @@ public class EaglerIntegratedServerWorker { } break; } - case IPCPacket21EnableLogging.ID: { - enableLoggingRedirector(((IPCPacket21EnableLogging)ipc).enable); + case IPCPacket1BEnableLogging.ID: { + enableLoggingRedirector(((IPCPacket1BEnableLogging)ipc).enable); break; } default: @@ -418,7 +418,7 @@ public class EaglerIntegratedServerWorker { } public static void sendLogMessagePacket(String txt, boolean err) { - sendIPCPacket(new IPCPacket20LoggerMessage(txt, err)); + sendIPCPacket(new IPCPacket1ALoggerMessage(txt, err)); } public static void sendIPCPacket(IPCPacketBase ipc) { @@ -454,12 +454,12 @@ public class EaglerIntegratedServerWorker { currentProcess = null; } - private static void mainLoop() { + private static void mainLoop(boolean singleThreadMode) { processAsyncMessageQueue(); if(currentProcess != null) { if(currentProcess.isServerRunning()) { - currentProcess.mainLoop(); + currentProcess.mainLoop(singleThreadMode); } if(!currentProcess.isServerRunning()) { currentProcess.stopServer(); @@ -467,7 +467,9 @@ public class EaglerIntegratedServerWorker { sendIPCPacket(new IPCPacketFFProcessKeepAlive(IPCPacket01StopServer.ID)); } }else { - EagUtils.sleep(50l); + if(!singleThreadMode) { + EagUtils.sleep(50l); + } } } @@ -476,12 +478,16 @@ public class EaglerIntegratedServerWorker { currentProcess = null; logger.info("Starting EaglercraftX integrated server worker..."); + if(ServerPlatformSingleplayer.getWorldsDatabase().isRamdisk()) { + sendIPCPacket(new IPCPacket1CIssueDetected(IPCPacket1CIssueDetected.ISSUE_RAMDISK_MODE)); + } + // signal thread startup successful sendIPCPacket(new IPCPacketFFProcessKeepAlive(0xFF)); while(true) { - mainLoop(); - EagUtils.sleep(0l); + mainLoop(false); + ServerPlatformSingleplayer.immediateContinue(); } }catch(Throwable tt) { if(tt instanceof ReportedException) { @@ -506,4 +512,17 @@ public class EaglerIntegratedServerWorker { sendIPCPacket(new IPCPacketFFProcessKeepAlive(IPCPacketFFProcessKeepAlive.EXITED)); } } + + public static void singleThreadMain() { + logger.info("Starting EaglercraftX integrated server worker..."); + if(ServerPlatformSingleplayer.getWorldsDatabase().isRamdisk()) { + sendIPCPacket(new IPCPacket1CIssueDetected(IPCPacket1CIssueDetected.ISSUE_RAMDISK_MODE)); + } + sendIPCPacket(new IPCPacketFFProcessKeepAlive(0xFF)); + } + + public static void singleThreadUpdate() { + mainLoop(true); + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerMinecraftServer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerMinecraftServer.java index 6e76ca96..7f995361 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerMinecraftServer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerMinecraftServer.java @@ -1,11 +1,12 @@ package net.lax1dude.eaglercraft.v1_8.sp.server; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import com.google.common.collect.Lists; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.internal.vfs2.VFile2; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.minecraft.entity.player.EntityPlayer; @@ -39,7 +40,7 @@ public class EaglerMinecraftServer extends MinecraftServer { public static final Logger logger = EaglerIntegratedServerWorker.logger; - public static final VFile2 savesDir = new VFile2("worlds"); + public static final VFile2 savesDir = WorldsDB.newVFile("worlds"); protected EnumDifficulty difficulty; protected GameType gamemode; @@ -59,13 +60,13 @@ public class EaglerMinecraftServer extends MinecraftServer { public static int counterTileUpdate = 0; public static int counterLightUpdate = 0; - private final List scheduledTasks = new LinkedList(); + private final List scheduledTasks = new LinkedList<>(); public EaglerMinecraftServer(String world, String owner, int viewDistance, WorldSettings currentWorldSettings, boolean demo) { super(world); Bootstrap.register(); this.saveHandler = new EaglerSaveHandler(savesDir, world); - this.skinService = new IntegratedSkinService(new VFile2(saveHandler.getWorldDirectory(), "eagler/skulls")); + this.skinService = new IntegratedSkinService(WorldsDB.newVFile(saveHandler.getWorldDirectory(), "eagler/skulls")); this.capeService = new IntegratedCapeService(); this.voiceService = null; this.setServerOwner(owner); @@ -131,7 +132,7 @@ public class EaglerMinecraftServer extends MinecraftServer { EaglerIntegratedServerWorker.saveFormat.deleteWorldDirectory(getFolderName()); } - public void mainLoop() { + public void mainLoop(boolean singleThreadMode) { long k = getCurrentTimeMillis(); this.sendTPSToClient(k); if(paused && this.playersOnline.size() <= 1) { @@ -140,7 +141,7 @@ public class EaglerMinecraftServer extends MinecraftServer { } long j = k - this.currentTime; - if (j > 2000L && this.currentTime - this.timeOfLastWarning >= 15000L) { + if (j > (singleThreadMode ? 500L : 2000L) && this.currentTime - this.timeOfLastWarning >= (singleThreadMode ? 5000L : 15000L)) { logger.warn( "Can\'t keep up! Did the system time change, or is the server overloaded? Running {}ms behind, skipping {} tick(s)", new Object[] { Long.valueOf(j), Long.valueOf(j / 50L) }); @@ -177,13 +178,13 @@ public class EaglerMinecraftServer extends MinecraftServer { if(millis - lastTPSUpdate > 1000l) { lastTPSUpdate = millis; if(serverRunning && this.worldServers != null) { - List lst = new ArrayList<>(Arrays.asList( + List lst = Lists.newArrayList( "TPS: " + counterTicksPerSecond + "/20", "Chunks: " + countChunksLoaded(this.worldServers) + "/" + countChunksTotal(this.worldServers), "Entities: " + countEntities(this.worldServers) + "+" + countTileEntities(this.worldServers), "R: " + counterChunkRead + ", G: " + counterChunkGenerate + ", W: " + counterChunkWrite, "TU: " + counterTileUpdate + ", LU: " + counterLightUpdate - )); + ); int players = countPlayerEntities(this.worldServers); if(players > 1) { lst.add("Players: " + players); @@ -252,7 +253,7 @@ public class EaglerMinecraftServer extends MinecraftServer { public void setPaused(boolean p) { paused = p; if(!p) { - currentTime = System.currentTimeMillis(); + currentTime = EagRuntime.steadyTimeMillis(); } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerSaveFormat.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerSaveFormat.java index 0f2d4b20..4fb29bdb 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerSaveFormat.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerSaveFormat.java @@ -30,13 +30,13 @@ import net.minecraft.world.storage.WorldInfo; */ public class EaglerSaveFormat extends SaveFormatOld { - public static final VFile2 worldsList = new VFile2("worlds_list.txt"); - public static final VFile2 worldsFolder = new VFile2("worlds"); - public EaglerSaveFormat(VFile2 parFile) { super(parFile); } + public static final VFile2 worldsList = WorldsDB.newVFile("worlds_list.txt"); + public static final VFile2 worldsFolder = WorldsDB.newVFile("worlds"); + public String getName() { return "eagler"; } @@ -46,7 +46,7 @@ public class EaglerSaveFormat extends SaveFormatOld { } public List getSaveList() { - ArrayList arraylist = Lists.newArrayList(); + ArrayList arraylist = Lists.newArrayList(); if(worldsList.exists()) { String[] lines = worldsList.getAllLines(); for (int i = 0; i < lines.length; ++i) { @@ -70,7 +70,7 @@ public class EaglerSaveFormat extends SaveFormatOld { } public void clearPlayers(String worldFolder) { - VFile2 file1 = new VFile2(this.savesDirectory, worldFolder, "player"); + VFile2 file1 = WorldsDB.newVFile(this.savesDirectory, worldFolder, "player"); deleteFiles(file1.listFiles(true), null); } @@ -80,12 +80,12 @@ public class EaglerSaveFormat extends SaveFormatOld { public boolean duplicateWorld(String worldFolder, String displayName) { String newFolderName = displayName.replaceAll("[\\./\"]", "_"); - VFile2 newFolder = new VFile2(savesDirectory, newFolderName); - while((new VFile2(newFolder, "level.dat")).exists() || (new VFile2(newFolder, "level.dat_old")).exists()) { + VFile2 newFolder = WorldsDB.newVFile(savesDirectory, newFolderName); + while((WorldsDB.newVFile(newFolder, "level.dat")).exists() || (WorldsDB.newVFile(newFolder, "level.dat_old")).exists()) { newFolderName += "_"; - newFolder = new VFile2(savesDirectory, newFolderName); + newFolder = WorldsDB.newVFile(savesDirectory, newFolderName); } - VFile2 oldFolder = new VFile2(this.savesDirectory, worldFolder); + VFile2 oldFolder = WorldsDB.newVFile(this.savesDirectory, worldFolder); String oldPath = oldFolder.getPath(); int totalSize = 0; int lastUpdate = 0; @@ -94,7 +94,7 @@ public class EaglerSaveFormat extends SaveFormatOld { for(int i = 0, l = vfl.size(); i < l; ++i) { VFile2 vf = vfl.get(i); String fileNameRelative = vf.getPath().substring(oldPath.length() + 1); - totalSize += VFile2.copyFile(vf, new VFile2(finalNewFolder, fileNameRelative)); + totalSize += VFile2.copyFile(vf, WorldsDB.newVFile(finalNewFolder, fileNameRelative)); if (totalSize - lastUpdate > 10000) { lastUpdate = totalSize; EaglerIntegratedServerWorker.sendProgress("singleplayer.busy.duplicating", totalSize); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerSaveHandler.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerSaveHandler.java index 0e7fe7ae..dae1b7ec 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerSaveHandler.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/EaglerSaveHandler.java @@ -29,7 +29,7 @@ public class EaglerSaveHandler extends SaveHandler { } public IChunkLoader getChunkLoader(WorldProvider provider) { - return new EaglerChunkLoader(new VFile2(this.getWorldDirectory(), "level" + provider.getDimensionId())); + return new EaglerChunkLoader(WorldsDB.newVFile(this.getWorldDirectory(), "level" + provider.getDimensionId())); } public void saveWorldInfoWithPlayer(WorldInfo worldInformation, NBTTagCompound tagCompound) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/WorldsDB.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/WorldsDB.java new file mode 100644 index 00000000..401c3fd9 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/WorldsDB.java @@ -0,0 +1,32 @@ +package net.lax1dude.eaglercraft.v1_8.sp.server; + +import java.util.function.Supplier; + +import net.lax1dude.eaglercraft.v1_8.internal.IEaglerFilesystem; +import net.lax1dude.eaglercraft.v1_8.internal.vfs2.VFile2; +import net.lax1dude.eaglercraft.v1_8.sp.server.internal.ServerPlatformSingleplayer; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class WorldsDB { + + private static final Supplier fsGetter = ServerPlatformSingleplayer::getWorldsDatabase; + + public static VFile2 newVFile(Object... path) { + return VFile2.create(fsGetter, path); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/EPKCompiler.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/EPKCompiler.java index 86e198c9..e20eb498 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/EPKCompiler.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/EPKCompiler.java @@ -7,8 +7,8 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.zip.CRC32; -import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EaglerOutputStream; +import net.lax1dude.eaglercraft.v1_8.EaglerZLIB; /** * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. @@ -28,11 +28,16 @@ import net.lax1dude.eaglercraft.v1_8.EaglerOutputStream; public class EPKCompiler { private final EaglerOutputStream os; + private final OutputStream dos; private final CRC32 checkSum = new CRC32(); private int lengthIntegerOffset = 0; private int totalFileCount = 0; public EPKCompiler(String name, String owner, String type) { + this(name, owner, type, false, true, null); + } + + public EPKCompiler(String name, String owner, String type, boolean gzip, boolean world, String commentStr) { os = new EaglerOutputStream(0x200000); try { @@ -44,10 +49,11 @@ public class EPKCompiler { os.write(filename.length); os.write(filename); - byte[] comment = ("\n\n # Eagler EPK v2.0 (c) " + EagRuntime.fixDateFormat(new SimpleDateFormat("yyyy")).format(d) + " " + - owner + "\n # export: on " + EagRuntime.fixDateFormat(new SimpleDateFormat("MM/dd/yyyy")).format(d) + " at " + - EagRuntime.fixDateFormat(new SimpleDateFormat("hh:mm:ss aa")).format(d) + "\n\n # world name: " + name + "\n\n") - .getBytes(StandardCharsets.UTF_8); + byte[] comment = (world ? ("\n\n # Eagler EPK v2.0 (c) " + + (new SimpleDateFormat("yyyy")).format(d) + " " + owner + + "\n # export: on " + (new SimpleDateFormat("MM/dd/yyyy")).format(d) + + " at " + (new SimpleDateFormat("hh:mm:ss aa")).format(d) + + "\n\n # world name: " + name + "\n\n") : commentStr).getBytes(StandardCharsets.UTF_8); os.write((comment.length >>> 8) & 255); os.write(comment.length & 255); @@ -58,40 +64,50 @@ public class EPKCompiler { lengthIntegerOffset = os.size(); os.write(new byte[]{(byte)255,(byte)255,(byte)255,(byte)255}); // this will be replaced with the file count - os.write('0'); // compression type: none + if(gzip) { + os.write('G'); // compression type: gzip + dos = EaglerZLIB.newGZIPOutputStream(os); + }else { + os.write('0'); // compression type: none + dos = os; + } - os.write(new byte[]{(byte)72,(byte)69,(byte)65,(byte)68}); // HEAD - os.write(new byte[]{(byte)9,(byte)102,(byte)105,(byte)108,(byte)101,(byte)45,(byte)116,(byte)121, + dos.write(new byte[]{(byte)72,(byte)69,(byte)65,(byte)68}); // HEAD + dos.write(new byte[]{(byte)9,(byte)102,(byte)105,(byte)108,(byte)101,(byte)45,(byte)116,(byte)121, (byte)112,(byte)101}); // 9 + file-type byte[] typeBytes = type.getBytes(StandardCharsets.UTF_8); - writeInt(typeBytes.length, os); - os.write(typeBytes); // write type - os.write('>'); + writeInt(typeBytes.length, dos); + dos.write(typeBytes); // write type + dos.write('>'); ++totalFileCount; - os.write(new byte[]{(byte)72,(byte)69,(byte)65,(byte)68}); // HEAD - os.write(new byte[]{(byte)10,(byte)119,(byte)111,(byte)114,(byte)108,(byte)100,(byte)45,(byte)110, - (byte)97,(byte)109,(byte)101}); // 10 + world-name + if(world) { + dos.write(new byte[]{(byte)72,(byte)69,(byte)65,(byte)68}); // HEAD + dos.write(new byte[]{(byte)10,(byte)119,(byte)111,(byte)114,(byte)108,(byte)100,(byte)45,(byte)110, + (byte)97,(byte)109,(byte)101}); // 10 + world-name + + byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); + writeInt(nameBytes.length, dos); + dos.write(nameBytes); // write name + dos.write('>'); + + ++totalFileCount; + } - byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); - writeInt(nameBytes.length, os); - os.write(nameBytes); // write name - os.write('>'); - - ++totalFileCount; - - os.write(new byte[]{(byte)72,(byte)69,(byte)65,(byte)68}); // HEAD - os.write(new byte[]{(byte)11,(byte)119,(byte)111,(byte)114,(byte)108,(byte)100,(byte)45,(byte)111, - (byte)119,(byte)110,(byte)101,(byte)114}); // 11 + world-owner - - byte[] ownerBytes = owner.getBytes(StandardCharsets.UTF_8); - writeInt(ownerBytes.length, os); - os.write(ownerBytes); // write owner - os.write('>'); - - ++totalFileCount; + if(world && owner != null) { + dos.write(new byte[]{(byte)72,(byte)69,(byte)65,(byte)68}); // HEAD + dos.write(new byte[]{(byte)11,(byte)119,(byte)111,(byte)114,(byte)108,(byte)100,(byte)45,(byte)111, + (byte)119,(byte)110,(byte)101,(byte)114}); // 11 + world-owner + + byte[] ownerBytes = owner.getBytes(StandardCharsets.UTF_8); + writeInt(ownerBytes.length, dos); + dos.write(ownerBytes); // write owner + dos.write('>'); + + ++totalFileCount; + } }catch(IOException ex) { throw new RuntimeException("This happened somehow", ex); @@ -105,19 +121,19 @@ public class EPKCompiler { checkSum.update(dat, 0, dat.length); long sum = checkSum.getValue(); - os.write(new byte[]{(byte)70,(byte)73,(byte)76,(byte)69}); // FILE + dos.write(new byte[]{(byte)70,(byte)73,(byte)76,(byte)69}); // FILE byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); - os.write(nameBytes.length); - os.write(nameBytes); + dos.write(nameBytes.length); + dos.write(nameBytes); - writeInt(dat.length + 5, os); - writeInt((int)sum, os); + writeInt(dat.length + 5, dos); + writeInt((int)sum, dos); - os.write(dat); + dos.write(dat); - os.write(':'); - os.write('>'); + dos.write(':'); + dos.write('>'); ++totalFileCount; @@ -128,8 +144,9 @@ public class EPKCompiler { public byte[] complete() { try { + dos.write(new byte[]{(byte)69,(byte)78,(byte)68,(byte)36}); // END$ + dos.close(); - os.write(new byte[]{(byte)69,(byte)78,(byte)68,(byte)36}); // END$ os.write(new byte[]{(byte)58,(byte)58,(byte)58,(byte)89,(byte)69,(byte)69,(byte)58,(byte)62}); // :::YEE:> byte[] ret = os.toByteArray(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/WorldConverterEPK.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/WorldConverterEPK.java index bf6ae1a8..524de859 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/WorldConverterEPK.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/WorldConverterEPK.java @@ -10,6 +10,7 @@ import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerIntegratedServerWorker; import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerSaveFormat; +import net.lax1dude.eaglercraft.v1_8.sp.server.WorldsDB; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.storage.WorldInfo; @@ -37,7 +38,7 @@ public class WorldConverterEPK { logger.info("Importing world \"{}\" from EPK", newName); String folderName = newName.replaceAll("[\\./\"]", "_"); VFile2 worldDir = EaglerIntegratedServerWorker.saveFormat.getSaveLoader(folderName, false).getWorldDirectory(); - while((new VFile2(worldDir, "level.dat")).exists() || (new VFile2(worldDir, "level.dat_old")).exists()) { + while(WorldsDB.newVFile(worldDir, "level.dat").exists() || WorldsDB.newVFile(worldDir, "level.dat_old").exists()) { folderName += "_"; worldDir = EaglerIntegratedServerWorker.saveFormat.getSaveLoader(folderName, false).getWorldDirectory(); } @@ -74,7 +75,7 @@ public class WorldConverterEPK { CompressedStreamTools.writeCompressed(worldDatNBT, tmp); b = tmp.toByteArray(); } - VFile2 ff = new VFile2(worldDir, f.name); + VFile2 ff = WorldsDB.newVFile(worldDir, f.name); ff.setAllBytes(b); prog += b.length; ++cnt; diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/WorldConverterMCA.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/WorldConverterMCA.java index ce8e5900..7e5bfa5e 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/WorldConverterMCA.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/export/WorldConverterMCA.java @@ -19,6 +19,7 @@ import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerChunkLoader; import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerIntegratedServerWorker; import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerSaveFormat; +import net.lax1dude.eaglercraft.v1_8.sp.server.WorldsDB; import net.minecraft.world.chunk.storage.RegionFile; import net.minecraft.world.storage.WorldInfo; import net.minecraft.nbt.CompressedStreamTools; @@ -47,7 +48,7 @@ public class WorldConverterMCA { logger.info("Importing world \"{}\" from MCA", newName); String folderName = newName.replaceAll("[\\./\"]", "_"); VFile2 worldDir = EaglerIntegratedServerWorker.saveFormat.getSaveLoader(folderName, false).getWorldDirectory(); - while((new VFile2(worldDir, "level.dat")).exists() || (new VFile2(worldDir, "level.dat_old")).exists()) { + while(WorldsDB.newVFile(worldDir, "level.dat").exists() || WorldsDB.newVFile(worldDir, "level.dat_old").exists()) { folderName += "_"; worldDir = EaglerIntegratedServerWorker.saveFormat.getSaveLoader(folderName, false).getWorldDirectory(); } @@ -105,11 +106,11 @@ public class WorldConverterMCA { EaglerOutputStream bo = new EaglerOutputStream(); CompressedStreamTools.writeCompressed(worldDatNBT, bo); b = bo.toByteArray(); - VFile2 ff = new VFile2(worldDir, fileName); + VFile2 ff = WorldsDB.newVFile(worldDir, fileName); ff.setAllBytes(b); prog += b.length; } else if ((fileName.endsWith(".mcr") || fileName.endsWith(".mca")) && (fileName.startsWith("region/") || fileName.startsWith("DIM1/region/") || fileName.startsWith("DIM-1/region/"))) { - VFile2 chunkFolder = new VFile2(worldDir, fileName.startsWith("DIM1") ? "level1" : (fileName.startsWith("DIM-1") ? "level-1" : "level0")); + VFile2 chunkFolder = WorldsDB.newVFile(worldDir, fileName.startsWith("DIM1") ? "level1" : (fileName.startsWith("DIM-1") ? "level-1" : "level0")); RegionFile mca = new RegionFile(new RandomAccessMemoryFile(b, b.length)); int loadChunksCount = 0; for(int j = 0; j < 32; ++j) { @@ -130,7 +131,7 @@ public class WorldConverterMCA { } int chunkX = chunkLevel.getInteger("xPos"); int chunkZ = chunkLevel.getInteger("zPos"); - VFile2 chunkOut = new VFile2(chunkFolder, EaglerChunkLoader.getChunkPath(chunkX, chunkZ) + ".dat"); + VFile2 chunkOut = WorldsDB.newVFile(chunkFolder, EaglerChunkLoader.getChunkPath(chunkX, chunkZ) + ".dat"); if(chunkOut.exists()) { logger.error("{}: Chunk already exists: {}", fileName, chunkOut.getPath()); continue; @@ -152,7 +153,7 @@ public class WorldConverterMCA { } else if (fileName.startsWith("playerdata/") || fileName.startsWith("stats/")) { //TODO: LAN player inventories } else if (fileName.startsWith("data/") || fileName.startsWith("players/") || fileName.startsWith("eagler/skulls/")) { - VFile2 ff = new VFile2(worldDir, fileName); + VFile2 ff = WorldsDB.newVFile(worldDir, fileName); ff.setAllBytes(b); prog += b.length; } else if (!fileName.equals("level.dat_mcr") && !fileName.equals("session.lock")) { @@ -184,7 +185,7 @@ public class WorldConverterMCA { zos.setComment("contains backup of world '" + folderName + "'"); worldFolder = EaglerIntegratedServerWorker.saveFormat.getSaveLoader(folderName, false).getWorldDirectory(); logger.info("Exporting world directory \"{}\" as MCA", worldFolder.getPath()); - VFile2 vf = new VFile2(worldFolder, "level.dat"); + VFile2 vf = WorldsDB.newVFile(worldFolder, "level.dat"); byte[] b; int lastProgUpdate = 0; int prog = 0; @@ -196,7 +197,7 @@ public class WorldConverterMCA { prog += b.length; safe = true; } - vf = new VFile2(worldFolder, "level.dat_old"); + vf = WorldsDB.newVFile(worldFolder, "level.dat_old"); if(vf.exists()) { zos.putNextEntry(new ZipEntry(folderName + "/level.dat_old")); b = vf.getAllBytes(); @@ -212,11 +213,11 @@ public class WorldConverterMCA { String[] dstFolderNames = new String[] { "/region/", "/DIM-1/region/", "/DIM1/region/" }; List fileList; for(int i = 0; i < 3; ++i) { - vf = new VFile2(worldFolder, srcFolderNames[i]); + vf = WorldsDB.newVFile(worldFolder, srcFolderNames[i]); fileList = vf.listFiles(true); String regionFolder = folderName + dstFolderNames[i]; logger.info("Converting chunks in \"{}\" as MCA to \"{}\"...", vf.getPath(), regionFolder); - Map regionFiles = new HashMap(); + Map regionFiles = new HashMap<>(); for(int k = 0, l = fileList.size(); k < l; ++k) { VFile2 chunkFile = fileList.get(k); NBTTagCompound chunkNBT; @@ -266,7 +267,7 @@ public class WorldConverterMCA { } } logger.info("Copying extra world data..."); - fileList = (new VFile2(worldFolder, "data")).listFiles(false); + fileList = WorldsDB.newVFile(worldFolder, "data").listFiles(false); for(int k = 0, l = fileList.size(); k < l; ++k) { VFile2 dataFile = fileList.get(k); zos.putNextEntry(new ZipEntry(folderName + "/data/" + dataFile.getName())); @@ -278,7 +279,7 @@ public class WorldConverterMCA { EaglerIntegratedServerWorker.sendProgress("singleplayer.busy.exporting.2", prog); } } - fileList = (new VFile2(worldFolder, "players")).listFiles(false); + fileList = WorldsDB.newVFile(worldFolder, "players").listFiles(false); for(int k = 0, l = fileList.size(); k < l; ++k) { VFile2 dataFile = fileList.get(k); zos.putNextEntry(new ZipEntry(folderName + "/players/" + dataFile.getName())); @@ -290,7 +291,7 @@ public class WorldConverterMCA { EaglerIntegratedServerWorker.sendProgress("singleplayer.busy.exporting.2", prog); } } - fileList = (new VFile2(worldFolder, "eagler/skulls")).listFiles(false); + fileList = WorldsDB.newVFile(worldFolder, "eagler/skulls").listFiles(false); for(int k = 0, l = fileList.size(); k < l; ++k) { VFile2 dataFile = fileList.get(k); zos.putNextEntry(new ZipEntry(folderName + "/eagler/skulls/" + dataFile.getName())); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/CustomSkullData.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/CustomSkullData.java index 5a90a9e2..b37a5562 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/CustomSkullData.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/CustomSkullData.java @@ -1,5 +1,12 @@ package net.lax1dude.eaglercraft.v1_8.sp.server.skins; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageProtocol; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketOtherSkinCustomV3EAG; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.util.SkinPacketVersionCache; + /** * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * @@ -19,21 +26,25 @@ public class CustomSkullData { public String skinURL; public long lastHit; - public byte[] skinData; + public SkinPacketVersionCache skinData; public CustomSkullData(String skinURL, byte[] skinData) { this.skinURL = skinURL; - this.lastHit = System.currentTimeMillis(); - this.skinData = skinData; + this.lastHit = EagRuntime.steadyTimeMillis(); + if(skinData.length != 16384) { + byte[] fixed = new byte[16384]; + System.arraycopy(skinData, 0, fixed, 0, skinData.length > fixed.length ? fixed.length : skinData.length); + skinData = fixed; + } + this.skinData = SkinPacketVersionCache.createCustomV3(0l, 0l, 0, skinData); } public byte[] getFullSkin() { - if(skinData.length == 16384) { - return skinData; - } - byte[] ret = new byte[16384]; - System.arraycopy(skinData, 0, ret, 0, skinData.length > ret.length ? ret.length : skinData.length); - return ret; + return ((SPacketOtherSkinCustomV3EAG)skinData.getV3()).customSkin; + } + + public GameMessagePacket getSkinPacket(EaglercraftUUID uuid, GamePluginMessageProtocol protocol) { + return SkinPacketVersionCache.rewriteUUID(skinData.get(protocol), uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedCapePackets.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedCapePackets.java index 402cde5d..6df2b5c3 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedCapePackets.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedCapePackets.java @@ -3,7 +3,9 @@ package net.lax1dude.eaglercraft.v1_8.sp.server.skins; import java.io.IOException; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; -import net.minecraft.entity.player.EntityPlayerMP; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketOtherCapeCustomEAG; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketOtherCapePresetEAG; /** * Copyright (c) 2024 lax1dude. All Rights Reserved. @@ -24,56 +26,27 @@ public class IntegratedCapePackets { public static final int PACKET_MY_CAPE_PRESET = 0x01; public static final int PACKET_MY_CAPE_CUSTOM = 0x02; - public static final int PACKET_GET_OTHER_CAPE = 0x03; - public static final int PACKET_OTHER_CAPE_PRESET = 0x04; - public static final int PACKET_OTHER_CAPE_CUSTOM = 0x05; - - public static void processPacket(byte[] data, EntityPlayerMP sender, IntegratedCapeService capeService) throws IOException { - if(data.length == 0) { - throw new IOException("Zero-length packet recieved"); - } - int packetId = (int)data[0] & 0xFF; - try { - switch(packetId) { - case PACKET_GET_OTHER_CAPE: - processGetOtherCape(data, sender, capeService); - break; - default: - throw new IOException("Unknown packet type " + packetId); - } - }catch(IOException ex) { - throw ex; - }catch(Throwable t) { - throw new IOException("Unhandled exception handling packet type " + packetId, t); - } - } - - private static void processGetOtherCape(byte[] data, EntityPlayerMP sender, IntegratedCapeService capeService) throws IOException { - if(data.length != 17) { - throw new IOException("Invalid length " + data.length + " for skin request packet"); - } - EaglercraftUUID searchUUID = IntegratedSkinPackets.bytesToUUID(data, 1); - capeService.processGetOtherCape(searchUUID, sender); - } public static void registerEaglerPlayer(EaglercraftUUID clientUUID, byte[] bs, IntegratedCapeService capeService) throws IOException { if(bs.length == 0) { throw new IOException("Zero-length packet recieved"); } - byte[] generatedPacket; + GameMessagePacket generatedPacket; int packetType = (int)bs[0] & 0xFF; switch(packetType) { case PACKET_MY_CAPE_PRESET: if(bs.length != 5) { throw new IOException("Invalid length " + bs.length + " for preset cape packet"); } - generatedPacket = IntegratedCapePackets.makePresetResponse(clientUUID, (bs[1] << 24) | (bs[2] << 16) | (bs[3] << 8) | (bs[4] & 0xFF)); + generatedPacket = new SPacketOtherCapePresetEAG(clientUUID.msb, clientUUID.lsb, (bs[1] << 24) | (bs[2] << 16) | (bs[3] << 8) | (bs[4] & 0xFF)); break; case PACKET_MY_CAPE_CUSTOM: if(bs.length != 1174) { throw new IOException("Invalid length " + bs.length + " for custom cape packet"); } - generatedPacket = IntegratedCapePackets.makeCustomResponse(clientUUID, bs, 1, 1173); + byte[] capePixels = new byte[bs.length - 1]; + System.arraycopy(bs, 1, capePixels, 0, capePixels.length); + generatedPacket = new SPacketOtherCapeCustomEAG(clientUUID.msb, clientUUID.lsb, capePixels); break; default: throw new IOException("Unknown skin packet type: " + packetType); @@ -82,29 +55,7 @@ public class IntegratedCapePackets { } public static void registerEaglerPlayerFallback(EaglercraftUUID clientUUID, IntegratedCapeService capeService) { - capeService.registerEaglercraftPlayer(clientUUID, IntegratedCapePackets.makePresetResponse(clientUUID, 0)); + capeService.registerEaglercraftPlayer(clientUUID, new SPacketOtherCapePresetEAG(clientUUID.msb, clientUUID.lsb, 0)); } - public static byte[] makePresetResponse(EaglercraftUUID uuid, int presetId) { - byte[] ret = new byte[1 + 16 + 4]; - ret[0] = (byte)PACKET_OTHER_CAPE_PRESET; - IntegratedSkinPackets.UUIDToBytes(uuid, ret, 1); - ret[17] = (byte)(presetId >>> 24); - ret[18] = (byte)(presetId >>> 16); - ret[19] = (byte)(presetId >>> 8); - ret[20] = (byte)(presetId & 0xFF); - return ret; - } - - public static byte[] makeCustomResponse(EaglercraftUUID uuid, byte[] pixels) { - return makeCustomResponse(uuid, pixels, 0, pixels.length); - } - - public static byte[] makeCustomResponse(EaglercraftUUID uuid, byte[] pixels, int offset, int length) { - byte[] ret = new byte[1 + 16 + length]; - ret[0] = (byte)PACKET_OTHER_CAPE_CUSTOM; - IntegratedSkinPackets.UUIDToBytes(uuid, ret, 1); - System.arraycopy(pixels, offset, ret, 17, length); - return ret; - } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedCapeService.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedCapeService.java index b3401a83..7e17e673 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedCapeService.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedCapeService.java @@ -7,10 +7,9 @@ import java.util.Map; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketOtherCapePresetEAG; import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.server.S3FPacketCustomPayload; /** * Copyright (c) 2024 lax1dude. All Rights Reserved. @@ -33,19 +32,7 @@ public class IntegratedCapeService { public static final int masterRateLimitPerPlayer = 250; - public static final String CHANNEL = "EAG|Capes-1.8"; - - private final Map capesCache = new HashMap(); - - public void processPacket(byte[] packetData, EntityPlayerMP sender) { - try { - IntegratedCapePackets.processPacket(packetData, sender, this); - } catch (IOException e) { - logger.error("Invalid skin request packet recieved from player {}!", sender.getName()); - logger.error(e); - sender.playerNetServerHandler.kickPlayerFromServer("Invalid skin request packet recieved!"); - } - } + private final Map capesCache = new HashMap<>(); public void processLoginPacket(byte[] packetData, EntityPlayerMP sender) { try { @@ -57,16 +44,16 @@ public class IntegratedCapeService { } } - public void registerEaglercraftPlayer(EaglercraftUUID playerUUID, byte[] capePacket) { + public void registerEaglercraftPlayer(EaglercraftUUID playerUUID, GameMessagePacket capePacket) { capesCache.put(playerUUID, capePacket); } public void processGetOtherCape(EaglercraftUUID searchUUID, EntityPlayerMP sender) { - byte[] maybeCape = capesCache.get(searchUUID); + GameMessagePacket maybeCape = capesCache.get(searchUUID); if(maybeCape == null) { - maybeCape = IntegratedCapePackets.makePresetResponse(searchUUID, 0); + maybeCape = new SPacketOtherCapePresetEAG(searchUUID.msb, searchUUID.lsb, 0); } - sender.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, new PacketBuffer(Unpooled.buffer(maybeCape, maybeCape.length).writerIndex(maybeCape.length)))); + sender.playerNetServerHandler.sendEaglerMessage(maybeCape); } public void unregisterPlayer(EaglercraftUUID playerUUID) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedSkinPackets.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedSkinPackets.java index 4c93d562..67da9f9d 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedSkinPackets.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedSkinPackets.java @@ -3,7 +3,9 @@ package net.lax1dude.eaglercraft.v1_8.sp.server.skins; import java.io.IOException; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; -import net.minecraft.entity.player.EntityPlayerMP; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.*; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.util.SkinPacketVersionCache; /** * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. @@ -24,76 +26,14 @@ public class IntegratedSkinPackets { public static final int PACKET_MY_SKIN_PRESET = 0x01; public static final int PACKET_MY_SKIN_CUSTOM = 0x02; - public static final int PACKET_GET_OTHER_SKIN = 0x03; - public static final int PACKET_OTHER_SKIN_PRESET = 0x04; - public static final int PACKET_OTHER_SKIN_CUSTOM = 0x05; - public static final int PACKET_GET_SKIN_BY_URL = 0x06; - public static final int PACKET_INSTALL_NEW_SKIN = 0x07; - public static void processPacket(byte[] data, EntityPlayerMP sender, IntegratedSkinService skinService) throws IOException { - if(data.length == 0) { - throw new IOException("Zero-length packet recieved"); - } - int packetId = (int)data[0] & 0xFF; - try { - switch(packetId) { - case PACKET_GET_OTHER_SKIN: - processGetOtherSkin(data, sender, skinService); - break; - case PACKET_GET_SKIN_BY_URL: - processGetOtherSkinByURL(data, sender, skinService); - break; - case PACKET_INSTALL_NEW_SKIN: - processInstallNewSkin(data, sender, skinService); - break; - default: - throw new IOException("Unknown packet type " + packetId); - } - }catch(IOException ex) { - throw ex; - }catch(Throwable t) { - throw new IOException("Unhandled exception handling packet type " + packetId, t); - } - } - - private static void processGetOtherSkin(byte[] data, EntityPlayerMP sender, IntegratedSkinService skinService) throws IOException { - if(data.length != 17) { - throw new IOException("Invalid length " + data.length + " for skin request packet"); - } - EaglercraftUUID searchUUID = bytesToUUID(data, 1); - skinService.processPacketGetOtherSkin(searchUUID, sender); - } - - private static void processGetOtherSkinByURL(byte[] data, EntityPlayerMP sender, IntegratedSkinService skinService) throws IOException { - if(data.length < 20) { - throw new IOException("Invalid length " + data.length + " for skin request packet"); - } - EaglercraftUUID searchUUID = bytesToUUID(data, 1); - int urlLength = (data[17] << 8) | data[18]; - if(data.length < 19 + urlLength) { - throw new IOException("Invalid length " + data.length + " for skin request packet with " + urlLength + " length URL"); - } - skinService.processPacketGetOtherSkin(searchUUID, bytesToAscii(data, 19, urlLength), sender); - } - - private static void processInstallNewSkin(byte[] data, EntityPlayerMP sender, IntegratedSkinService skinService) throws IOException { - if(data.length < 3) { - throw new IOException("Invalid length " + data.length + " for skin data packet"); - } - int dataLength = (data[1] << 8) | data[2]; - byte[] dataBmp = new byte[dataLength]; - if(data.length != dataLength + 3) { - throw new IOException("Invalid data length " + dataLength + " for " + data.length + " byte skin data packet"); - } - System.arraycopy(data, 3, dataBmp, 0, dataLength); - skinService.processPacketInstallNewSkin(dataBmp, sender); - } - - public static void registerEaglerPlayer(EaglercraftUUID clientUUID, byte[] bs, IntegratedSkinService skinService) throws IOException { + public static void registerEaglerPlayer(EaglercraftUUID clientUUID, byte[] bs, IntegratedSkinService skinService, + int protocolVers) throws IOException { if(bs.length == 0) { throw new IOException("Zero-length packet recieved"); } - byte[] generatedPacket; + GameMessagePacket generatedPacketV3 = null; + GameMessagePacket generatedPacketV4 = null; int skinModel = -1; int packetType = (int)bs[0] & 0xFF; switch(packetType) { @@ -101,118 +41,63 @@ public class IntegratedSkinPackets { if(bs.length != 5) { throw new IOException("Invalid length " + bs.length + " for preset skin packet"); } - generatedPacket = makePresetResponse(clientUUID, (bs[1] << 24) | (bs[2] << 16) | (bs[3] << 8) | (bs[4] & 0xFF)); + generatedPacketV3 = generatedPacketV4 = new SPacketOtherSkinPresetEAG(clientUUID.msb, clientUUID.lsb, + (bs[1] << 24) | (bs[2] << 16) | (bs[3] << 8) | (bs[4] & 0xFF)); break; case PACKET_MY_SKIN_CUSTOM: - byte[] pixels = new byte[16384]; - if(bs.length != 2 + pixels.length) { - throw new IOException("Invalid length " + bs.length + " for custom skin packet"); + if(protocolVers <= 3) { + byte[] pixels = new byte[16384]; + if(bs.length != 2 + pixels.length) { + throw new IOException("Invalid length " + bs.length + " for custom skin packet"); + } + setAlphaForChestV3(pixels); + System.arraycopy(bs, 2, pixels, 0, pixels.length); + generatedPacketV3 = new SPacketOtherSkinCustomV3EAG(clientUUID.msb, clientUUID.lsb, (skinModel = (int)bs[1] & 0xFF), pixels); + }else { + byte[] pixels = new byte[12288]; + if(bs.length != 2 + pixels.length) { + throw new IOException("Invalid length " + bs.length + " for custom skin packet"); + } + setAlphaForChestV4(pixels); + System.arraycopy(bs, 2, pixels, 0, pixels.length); + generatedPacketV4 = new SPacketOtherSkinCustomV4EAG(clientUUID.msb, clientUUID.lsb, (skinModel = (int)bs[1] & 0xFF), pixels); } - setAlphaForChest(pixels, (byte)255); - System.arraycopy(bs, 2, pixels, 0, pixels.length); - generatedPacket = makeCustomResponse(clientUUID, (skinModel = (int)bs[1] & 0xFF), pixels); break; default: throw new IOException("Unknown skin packet type: " + packetType); } - skinService.processPacketPlayerSkin(clientUUID, generatedPacket, skinModel); + skinService.processPacketPlayerSkin(clientUUID, new SkinPacketVersionCache(generatedPacketV3, generatedPacketV4), skinModel); } public static void registerEaglerPlayerFallback(EaglercraftUUID clientUUID, IntegratedSkinService skinService) throws IOException { int skinModel = (clientUUID.hashCode() & 1) != 0 ? 1 : 0; - byte[] generatedPacket = makePresetResponse(clientUUID, skinModel); - skinService.processPacketPlayerSkin(clientUUID, generatedPacket, skinModel); + skinService.processPacketPlayerSkin(clientUUID, SkinPacketVersionCache.createPreset(clientUUID.msb, clientUUID.lsb, skinModel), skinModel); } - public static void setAlphaForChest(byte[] skin64x64, byte alpha) { + public static void setAlphaForChestV3(byte[] skin64x64) { if(skin64x64.length != 16384) { throw new IllegalArgumentException("Skin is not 64x64!"); } for(int y = 20; y < 32; ++y) { for(int x = 16; x < 40; ++x) { - skin64x64[(y << 8) | (x << 2)] = alpha; + skin64x64[(y << 8) | (x << 2)] = (byte)0xFF; } } } - public static byte[] makePresetResponse(EaglercraftUUID uuid) { - return makePresetResponse(uuid, (uuid.hashCode() & 1) != 0 ? 1 : 0); - } - - public static byte[] makePresetResponse(EaglercraftUUID uuid, int presetId) { - byte[] ret = new byte[1 + 16 + 4]; - ret[0] = (byte)PACKET_OTHER_SKIN_PRESET; - UUIDToBytes(uuid, ret, 1); - ret[17] = (byte)(presetId >>> 24); - ret[18] = (byte)(presetId >>> 16); - ret[19] = (byte)(presetId >>> 8); - ret[20] = (byte)(presetId & 0xFF); - return ret; - } - - public static byte[] makeCustomResponse(EaglercraftUUID uuid, int model, byte[] pixels) { - byte[] ret = new byte[1 + 16 + 1 + pixels.length]; - ret[0] = (byte)PACKET_OTHER_SKIN_CUSTOM; - UUIDToBytes(uuid, ret, 1); - ret[17] = (byte)model; - System.arraycopy(pixels, 0, ret, 18, pixels.length); - return ret; - } - - public static EaglercraftUUID bytesToUUID(byte[] bytes, int off) { - long msb = (((long) bytes[off] & 0xFFl) << 56l) | (((long) bytes[off + 1] & 0xFFl) << 48l) - | (((long) bytes[off + 2] & 0xFFl) << 40l) | (((long) bytes[off + 3] & 0xFFl) << 32l) - | (((long) bytes[off + 4] & 0xFFl) << 24l) | (((long) bytes[off + 5] & 0xFFl) << 16l) - | (((long) bytes[off + 6] & 0xFFl) << 8l) | ((long) bytes[off + 7] & 0xFFl); - long lsb = (((long) bytes[off + 8] & 0xFFl) << 56l) | (((long) bytes[off + 9] & 0xFFl) << 48l) - | (((long) bytes[off + 10] & 0xFFl) << 40l) | (((long) bytes[off + 11] & 0xFFl) << 32l) - | (((long) bytes[off + 12] & 0xFFl) << 24l) | (((long) bytes[off + 13] & 0xFFl) << 16l) - | (((long) bytes[off + 14] & 0xFFl) << 8l) | ((long) bytes[off + 15] & 0xFFl); - return new EaglercraftUUID(msb, lsb); - } - - private static final String hex = "0123456789abcdef"; - - public static String bytesToString(byte[] bytes, int off, int len) { - char[] ret = new char[len << 1]; - for(int i = 0; i < len; ++i) { - ret[i * 2] = hex.charAt((bytes[off + i] >> 4) & 0xF); - ret[i * 2 + 1] = hex.charAt(bytes[off + i] & 0xF); + public static void setAlphaForChestV4(byte[] skin64x64) { + if(skin64x64.length != 12288) { + throw new IllegalArgumentException("Skin is not 64x64!"); } - return new String(ret); - } - - public static String bytesToAscii(byte[] bytes, int off, int len) { - char[] ret = new char[len]; - for(int i = 0; i < len; ++i) { - ret[i] = (char)((int)bytes[off + i] & 0xFF); + for(int y = 20; y < 32; ++y) { + for(int x = 16; x < 40; ++x) { + skin64x64[((y << 6) | x) * 3] |= 0x80; + } } - return new String(ret); - } - - public static String bytesToAscii(byte[] bytes) { - return bytesToAscii(bytes, 0, bytes.length); } - public static void UUIDToBytes(EaglercraftUUID uuid, byte[] bytes, int off) { - long msb = uuid.getMostSignificantBits(); - long lsb = uuid.getLeastSignificantBits(); - bytes[off] = (byte)(msb >>> 56l); - bytes[off + 1] = (byte)(msb >>> 48l); - bytes[off + 2] = (byte)(msb >>> 40l); - bytes[off + 3] = (byte)(msb >>> 32l); - bytes[off + 4] = (byte)(msb >>> 24l); - bytes[off + 5] = (byte)(msb >>> 16l); - bytes[off + 6] = (byte)(msb >>> 8l); - bytes[off + 7] = (byte)(msb & 0xFFl); - bytes[off + 8] = (byte)(lsb >>> 56l); - bytes[off + 9] = (byte)(lsb >>> 48l); - bytes[off + 10] = (byte)(lsb >>> 40l); - bytes[off + 11] = (byte)(lsb >>> 32l); - bytes[off + 12] = (byte)(lsb >>> 24l); - bytes[off + 13] = (byte)(lsb >>> 16l); - bytes[off + 14] = (byte)(lsb >>> 8l); - bytes[off + 15] = (byte)(lsb & 0xFFl); + public static SPacketOtherSkinPresetEAG makePresetResponse(EaglercraftUUID uuid) { + return new SPacketOtherSkinPresetEAG(uuid.msb, uuid.lsb, (uuid.hashCode() & 1) != 0 ? 1 : 0); } public static byte[] asciiString(String string) { @@ -231,21 +116,4 @@ public class IntegratedSkinPackets { return "slim".equalsIgnoreCase(modelName) ? 1 : 0; } - public static byte[] rewriteUUID(EaglercraftUUID newUUID, byte[] pkt) { - byte[] ret = new byte[pkt.length]; - System.arraycopy(pkt, 0, ret, 0, pkt.length); - UUIDToBytes(newUUID, ret, 1); - return ret; - } - - public static byte[] rewriteUUIDModel(EaglercraftUUID newUUID, byte[] pkt, int model) { - byte[] ret = new byte[pkt.length]; - System.arraycopy(pkt, 0, ret, 0, pkt.length); - UUIDToBytes(newUUID, ret, 1); - if(ret[0] == (byte)PACKET_OTHER_SKIN_CUSTOM) { - ret[17] = (byte)model; - } - return ret; - } - } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedSkinService.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedSkinService.java index e2a89173..0aa1ce1c 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedSkinService.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/skins/IntegratedSkinService.java @@ -7,17 +7,19 @@ import java.util.Iterator; import java.util.Map; import net.lax1dude.eaglercraft.v1_8.Base64; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; import net.lax1dude.eaglercraft.v1_8.crypto.SHA1Digest; import net.lax1dude.eaglercraft.v1_8.internal.vfs2.VFile2; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketOtherSkinPresetEAG; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.util.SkinPacketVersionCache; +import net.lax1dude.eaglercraft.v1_8.sp.server.WorldsDB; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.init.Items; import net.minecraft.item.ItemStack; -import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.server.S3FPacketCustomPayload; import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.EnumChatFormatting; import net.minecraft.nbt.NBTTagCompound; @@ -43,8 +45,6 @@ public class IntegratedSkinService { public static final Logger logger = LogManager.getLogger("IntegratedSkinService"); - public static final String CHANNEL = "EAG|Skins-1.8"; - public static final byte[] skullNotFoundTexture = new byte[4096]; static { @@ -62,8 +62,8 @@ public class IntegratedSkinService { public final VFile2 skullsDirectory; - public final Map playerSkins = new HashMap(); - public final Map customSkulls = new HashMap(); + public final Map playerSkins = new HashMap<>(); + public final Map customSkulls = new HashMap<>(); private long lastFlush = 0l; @@ -71,19 +71,9 @@ public class IntegratedSkinService { this.skullsDirectory = skullsDirectory; } - public void processPacket(byte[] packetData, EntityPlayerMP sender) { + public void processLoginPacket(byte[] packetData, EntityPlayerMP sender, int protocolVers) { try { - IntegratedSkinPackets.processPacket(packetData, sender, this); - } catch (IOException e) { - logger.error("Invalid skin request packet recieved from player {}!", sender.getName()); - logger.error(e); - sender.playerNetServerHandler.kickPlayerFromServer("Invalid skin request packet recieved!"); - } - } - - public void processLoginPacket(byte[] packetData, EntityPlayerMP sender) { - try { - IntegratedSkinPackets.registerEaglerPlayer(sender.getUniqueID(), packetData, this); + IntegratedSkinPackets.registerEaglerPlayer(sender.getUniqueID(), packetData, this, protocolVers); } catch (IOException e) { logger.error("Invalid skin data packet recieved from player {}!", sender.getName()); logger.error(e); @@ -92,36 +82,39 @@ public class IntegratedSkinService { } public void processPacketGetOtherSkin(EaglercraftUUID searchUUID, EntityPlayerMP sender) { - byte[] playerSkin = playerSkins.get(searchUUID); - if(playerSkin == null) { - playerSkin = IntegratedSkinPackets.makePresetResponse(searchUUID); + SkinPacketVersionCache playerSkin = playerSkins.get(searchUUID); + GameMessagePacket toSend = null; + if(playerSkin != null) { + toSend = playerSkin.get(sender.playerNetServerHandler.getEaglerMessageProtocol()); + }else { + toSend = IntegratedSkinPackets.makePresetResponse(searchUUID); } - sender.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, new PacketBuffer(Unpooled.buffer(playerSkin, playerSkin.length).writerIndex(playerSkin.length)))); + sender.playerNetServerHandler.sendEaglerMessage(toSend); } public void processPacketGetOtherSkin(EaglercraftUUID searchUUID, String urlStr, EntityPlayerMP sender) { urlStr = urlStr.toLowerCase(); - byte[] playerSkin; + GameMessagePacket playerSkin; if(!urlStr.startsWith("eagler://")) { - playerSkin = IntegratedSkinPackets.makePresetResponse(searchUUID, 0); + playerSkin = new SPacketOtherSkinPresetEAG(searchUUID.msb, searchUUID.lsb, 0); }else { urlStr = urlStr.substring(9); if(urlStr.contains(VFile2.pathSeperator)) { - playerSkin = IntegratedSkinPackets.makePresetResponse(searchUUID, 0); + playerSkin = new SPacketOtherSkinPresetEAG(searchUUID.msb, searchUUID.lsb, 0); }else { CustomSkullData sk = customSkulls.get(urlStr); if(sk == null) { customSkulls.put(urlStr, sk = loadCustomSkull(urlStr)); }else { - sk.lastHit = System.currentTimeMillis(); + sk.lastHit = EagRuntime.steadyTimeMillis(); } - playerSkin = IntegratedSkinPackets.makeCustomResponse(searchUUID, 0, sk.getFullSkin()); + playerSkin = sk.getSkinPacket(searchUUID, sender.playerNetServerHandler.getEaglerMessageProtocol()); } } - sender.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, new PacketBuffer(Unpooled.buffer(playerSkin, playerSkin.length).writerIndex(playerSkin.length)))); + sender.playerNetServerHandler.sendEaglerMessage(playerSkin); } - public void processPacketPlayerSkin(EaglercraftUUID clientUUID, byte[] generatedPacket, int skinModel) { + public void processPacketPlayerSkin(EaglercraftUUID clientUUID, SkinPacketVersionCache generatedPacket, int skinModel) { playerSkins.put(clientUUID, generatedPacket); } @@ -188,12 +181,12 @@ public class IntegratedSkinService { } String str = "skin-" + new String(hashText) + ".bmp"; customSkulls.put(str, new CustomSkullData(str, skullData)); - (new VFile2(skullsDirectory, str)).setAllBytes(skullData); + WorldsDB.newVFile(skullsDirectory, str).setAllBytes(skullData); return str; } private CustomSkullData loadCustomSkull(String urlStr) { - byte[] data = (new VFile2(skullsDirectory, urlStr)).getAllBytes(); + byte[] data = WorldsDB.newVFile(skullsDirectory, urlStr).getAllBytes(); if(data == null) { return new CustomSkullData(urlStr, skullNotFoundTexture); }else { @@ -202,7 +195,7 @@ public class IntegratedSkinService { } public void flushCache() { - long cur = System.currentTimeMillis(); + long cur = EagRuntime.steadyTimeMillis(); if(cur - lastFlush > 300000l) { lastFlush = cur; Iterator customSkullsItr = customSkulls.values().iterator(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/IntegratedServerPlayerNetworkManager.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/IntegratedServerPlayerNetworkManager.java index 89b949c5..419c69b6 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/IntegratedServerPlayerNetworkManager.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/IntegratedServerPlayerNetworkManager.java @@ -56,7 +56,7 @@ public class IntegratedServerPlayerNetworkManager { private boolean firstPacket = true; - private List fragmentedPacket = new ArrayList(); + private List fragmentedPacket = new ArrayList<>(); public static final int fragmentSize = 0xFF00; public static final int compressionThreshold = 1024; @@ -124,8 +124,7 @@ public class IntegratedServerPlayerNetworkManager { kickDAO.write(0x00); kickDAO.write(msg.length()); for(int j = 0, l = msg.length(); j < l; ++j) { - kickDAO.write(0); - kickDAO.write(msg.codePointAt(j)); + kickDAO.writeChar(msg.charAt(j)); } }catch(IOException ex) { throw new RuntimeException(ex); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/protocol/ServerV3MessageHandler.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/protocol/ServerV3MessageHandler.java new file mode 100644 index 00000000..cc6cf034 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/protocol/ServerV3MessageHandler.java @@ -0,0 +1,90 @@ +package net.lax1dude.eaglercraft.v1_8.sp.server.socket.protocol; + +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessageHandler; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.*; +import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerMinecraftServer; +import net.lax1dude.eaglercraft.v1_8.sp.server.voice.IntegratedVoiceService; +import net.minecraft.network.NetHandlerPlayServer; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ServerV3MessageHandler implements GameMessageHandler { + + private final NetHandlerPlayServer netHandler; + private final EaglerMinecraftServer server; + + public ServerV3MessageHandler(NetHandlerPlayServer netHandler) { + this.netHandler = netHandler; + this.server = (EaglerMinecraftServer)netHandler.serverController; + } + + public void handleClient(CPacketGetOtherCapeEAG packet) { + server.getCapeService().processGetOtherCape(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), netHandler.playerEntity); + } + + public void handleClient(CPacketGetOtherSkinEAG packet) { + server.getSkinService().processPacketGetOtherSkin(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), netHandler.playerEntity); + } + + public void handleClient(CPacketGetSkinByURLEAG packet) { + server.getSkinService().processPacketGetOtherSkin(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), packet.url, netHandler.playerEntity); + } + + public void handleClient(CPacketInstallSkinSPEAG packet) { + server.getSkinService().processPacketInstallNewSkin(packet.customSkin, netHandler.playerEntity); + } + + public void handleClient(CPacketVoiceSignalConnectEAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + voiceSvc.handleVoiceSignalPacketTypeConnect(netHandler.playerEntity); + } + } + + public void handleClient(CPacketVoiceSignalDescEAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + voiceSvc.handleVoiceSignalPacketTypeDesc(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), packet.desc, netHandler.playerEntity); + } + } + + public void handleClient(CPacketVoiceSignalDisconnectV3EAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + if(packet.isPeerType) { + voiceSvc.handleVoiceSignalPacketTypeDisconnectPeer(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), netHandler.playerEntity); + }else { + voiceSvc.handleVoiceSignalPacketTypeDisconnect(netHandler.playerEntity); + } + } + } + + public void handleClient(CPacketVoiceSignalICEEAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + voiceSvc.handleVoiceSignalPacketTypeICE(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), packet.ice, netHandler.playerEntity); + } + } + + public void handleClient(CPacketVoiceSignalRequestEAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + voiceSvc.handleVoiceSignalPacketTypeRequest(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), netHandler.playerEntity); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/protocol/ServerV4MessageHandler.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/protocol/ServerV4MessageHandler.java new file mode 100644 index 00000000..a8f993f8 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/socket/protocol/ServerV4MessageHandler.java @@ -0,0 +1,104 @@ +package net.lax1dude.eaglercraft.v1_8.sp.server.socket.protocol; + +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessageHandler; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.*; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketOtherPlayerClientUUIDV4EAG; +import net.lax1dude.eaglercraft.v1_8.sp.server.EaglerMinecraftServer; +import net.lax1dude.eaglercraft.v1_8.sp.server.voice.IntegratedVoiceService; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.NetHandlerPlayServer; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ServerV4MessageHandler implements GameMessageHandler { + + private final NetHandlerPlayServer netHandler; + private final EaglerMinecraftServer server; + + public ServerV4MessageHandler(NetHandlerPlayServer netHandler) { + this.netHandler = netHandler; + this.server = (EaglerMinecraftServer)netHandler.serverController; + } + + public void handleClient(CPacketGetOtherCapeEAG packet) { + server.getCapeService().processGetOtherCape(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), netHandler.playerEntity); + } + + public void handleClient(CPacketGetOtherSkinEAG packet) { + server.getSkinService().processPacketGetOtherSkin(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), netHandler.playerEntity); + } + + public void handleClient(CPacketGetSkinByURLEAG packet) { + server.getSkinService().processPacketGetOtherSkin(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), packet.url, netHandler.playerEntity); + } + + public void handleClient(CPacketInstallSkinSPEAG packet) { + server.getSkinService().processPacketInstallNewSkin(packet.customSkin, netHandler.playerEntity); + } + + public void handleClient(CPacketVoiceSignalConnectEAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + voiceSvc.handleVoiceSignalPacketTypeConnect(netHandler.playerEntity); + } + } + + public void handleClient(CPacketVoiceSignalDescEAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + voiceSvc.handleVoiceSignalPacketTypeDesc(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), packet.desc, netHandler.playerEntity); + } + } + + public void handleClient(CPacketVoiceSignalDisconnectV4EAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + voiceSvc.handleVoiceSignalPacketTypeDisconnect(netHandler.playerEntity); + } + } + + public void handleClient(CPacketVoiceSignalDisconnectPeerV4EAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + voiceSvc.handleVoiceSignalPacketTypeDisconnectPeer(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), netHandler.playerEntity); + } + } + + public void handleClient(CPacketVoiceSignalICEEAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + voiceSvc.handleVoiceSignalPacketTypeICE(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), packet.ice, netHandler.playerEntity); + } + } + + public void handleClient(CPacketVoiceSignalRequestEAG packet) { + IntegratedVoiceService voiceSvc = server.getVoiceService(); + if(voiceSvc != null) { + voiceSvc.handleVoiceSignalPacketTypeRequest(new EaglercraftUUID(packet.uuidMost, packet.uuidLeast), netHandler.playerEntity); + } + } + + public void handleClient(CPacketGetOtherClientUUIDV4EAG packet) { + EntityPlayerMP player = server.getConfigurationManager().getPlayerByUUID(new EaglercraftUUID(packet.playerUUIDMost, packet.playerUUIDLeast)); + if(player != null && player.clientBrandUUID != null) { + netHandler.sendEaglerMessage(new SPacketOtherPlayerClientUUIDV4EAG(packet.requestId, player.clientBrandUUID.msb, player.clientBrandUUID.lsb)); + }else { + netHandler.sendEaglerMessage(new SPacketOtherPlayerClientUUIDV4EAG(packet.requestId, 0l, 0l)); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/voice/IntegratedVoiceService.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/voice/IntegratedVoiceService.java index bb20af2b..f6a802ac 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/voice/IntegratedVoiceService.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/voice/IntegratedVoiceService.java @@ -1,6 +1,7 @@ package net.lax1dude.eaglercraft.v1_8.sp.server.voice; -import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -10,11 +11,10 @@ import java.util.Set; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; -import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.*; import net.lax1dude.eaglercraft.v1_8.voice.ExpiringSet; import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.server.S3FPacketCustomPayload; /** * Copyright (c) 2024 lax1dude. All Rights Reserved. @@ -35,20 +35,18 @@ public class IntegratedVoiceService { public static final Logger logger = LogManager.getLogger("IntegratedVoiceService"); - public static final String CHANNEL = "EAG|Voice-1.8"; - - private byte[] iceServersPacket; + private GameMessagePacket iceServersPacket; private final Map voicePlayers = new HashMap<>(); private final Map> voiceRequests = new HashMap<>(); private final Set voicePairs = new HashSet<>(); public IntegratedVoiceService(String[] iceServers) { - iceServersPacket = IntegratedVoiceSignalPackets.makeVoiceSignalPacketAllowed(true, iceServers); + iceServersPacket = new SPacketVoiceSignalAllowedEAG(true, iceServers); } public void changeICEServers(String[] iceServers) { - iceServersPacket = IntegratedVoiceSignalPackets.makeVoiceSignalPacketAllowed(true, iceServers); + iceServersPacket = new SPacketVoiceSignalAllowedEAG(true, iceServers); } private static class VoicePair { @@ -85,25 +83,14 @@ public class IntegratedVoiceService { } public void handlePlayerLoggedIn(EntityPlayerMP player) { - player.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, new PacketBuffer( - Unpooled.buffer(iceServersPacket, iceServersPacket.length).writerIndex(iceServersPacket.length)))); + player.playerNetServerHandler.sendEaglerMessage(iceServersPacket); } public void handlePlayerLoggedOut(EntityPlayerMP player) { removeUser(player.getUniqueID()); } - public void processPacket(PacketBuffer packetData, EntityPlayerMP sender) { - try { - IntegratedVoiceSignalPackets.processPacket(packetData, sender, this); - } catch (IOException e) { - logger.error("Invalid voice signal packet recieved from player {}!", sender.getName()); - logger.error(e); - sender.playerNetServerHandler.kickPlayerFromServer("Invalid voice signal packet recieved!"); - } - } - - void handleVoiceSignalPacketTypeRequest(EaglercraftUUID player, EntityPlayerMP sender) { + public void handleVoiceSignalPacketTypeRequest(EaglercraftUUID player, EntityPlayerMP sender) { EaglercraftUUID senderUUID = sender.getUniqueID(); if (senderUUID.equals(player)) return; // prevent duplicates @@ -134,14 +121,24 @@ public class IntegratedVoiceService { voiceRequests.remove(senderUUID); // send each other add data voicePairs.add(newPair); - targetPlayerCon.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, - IntegratedVoiceSignalPackets.makeVoiceSignalPacketConnect(senderUUID, false))); - sender.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, - IntegratedVoiceSignalPackets.makeVoiceSignalPacketConnect(player, true))); + if(targetPlayerCon.playerNetServerHandler.getEaglerMessageProtocol().ver <= 3) { + targetPlayerCon.playerNetServerHandler + .sendEaglerMessage(new SPacketVoiceSignalConnectV3EAG(senderUUID.msb, senderUUID.lsb, false, false)); + }else { + targetPlayerCon.playerNetServerHandler + .sendEaglerMessage(new SPacketVoiceSignalConnectV4EAG(senderUUID.msb, senderUUID.lsb, false)); + } + if(sender.playerNetServerHandler.getEaglerMessageProtocol().ver <= 3) { + sender.playerNetServerHandler + .sendEaglerMessage(new SPacketVoiceSignalConnectV3EAG(player.msb, player.lsb, false, true)); + }else { + sender.playerNetServerHandler + .sendEaglerMessage(new SPacketVoiceSignalConnectV4EAG(player.msb, player.lsb, true)); + } } } - void handleVoiceSignalPacketTypeConnect(EntityPlayerMP sender) { + public void handleVoiceSignalPacketTypeConnect(EntityPlayerMP sender) { if (voicePlayers.containsKey(sender.getUniqueID())) { return; } @@ -150,63 +147,60 @@ public class IntegratedVoiceService { if (hasNoOtherPlayers) { return; } - byte[] packetToBroadcast = IntegratedVoiceSignalPackets.makeVoiceSignalPacketGlobal(voicePlayers.values()); + Collection userDatas = new ArrayList<>(voicePlayers.size()); + for(EntityPlayerMP player : voicePlayers.values()) { + EaglercraftUUID uuid = player.getUniqueID(); + userDatas.add(new SPacketVoiceSignalGlobalEAG.UserData(uuid.msb, uuid.lsb, player.getName())); + } + SPacketVoiceSignalGlobalEAG packetToBroadcast = new SPacketVoiceSignalGlobalEAG(userDatas); for (EntityPlayerMP userCon : voicePlayers.values()) { - userCon.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, new PacketBuffer(Unpooled - .buffer(packetToBroadcast, packetToBroadcast.length).writerIndex(packetToBroadcast.length)))); + userCon.playerNetServerHandler.sendEaglerMessage(packetToBroadcast); } } - void handleVoiceSignalPacketTypeICE(EaglercraftUUID player, String str, EntityPlayerMP sender) { - VoicePair pair = new VoicePair(player, sender.getUniqueID()); + public void handleVoiceSignalPacketTypeICE(EaglercraftUUID player, byte[] str, EntityPlayerMP sender) { + EaglercraftUUID uuid = sender.getUniqueID(); + VoicePair pair = new VoicePair(player, uuid); EntityPlayerMP pass = voicePairs.contains(pair) ? voicePlayers.get(player) : null; if (pass != null) { - pass.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, - IntegratedVoiceSignalPackets.makeVoiceSignalPacketICE(sender.getUniqueID(), str))); + pass.playerNetServerHandler.sendEaglerMessage(new SPacketVoiceSignalICEEAG(uuid.msb, uuid.lsb, str)); } } - void handleVoiceSignalPacketTypeDesc(EaglercraftUUID player, String str, EntityPlayerMP sender) { - VoicePair pair = new VoicePair(player, sender.getUniqueID()); + public void handleVoiceSignalPacketTypeDesc(EaglercraftUUID player, byte[] str, EntityPlayerMP sender) { + EaglercraftUUID uuid = sender.getUniqueID(); + VoicePair pair = new VoicePair(player, uuid); EntityPlayerMP pass = voicePairs.contains(pair) ? voicePlayers.get(player) : null; if (pass != null) { - pass.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, - IntegratedVoiceSignalPackets.makeVoiceSignalPacketDesc(sender.getUniqueID(), str))); + pass.playerNetServerHandler.sendEaglerMessage(new SPacketVoiceSignalDescEAG(uuid.msb, uuid.lsb, str)); } } - void handleVoiceSignalPacketTypeDisconnect(EaglercraftUUID player, EntityPlayerMP sender) { - if (player != null) { - if (!voicePlayers.containsKey(player)) { - return; + public void handleVoiceSignalPacketTypeDisconnect(EntityPlayerMP sender) { + removeUser(sender.getUniqueID()); + } + + public void handleVoiceSignalPacketTypeDisconnectPeer(EaglercraftUUID player, EntityPlayerMP sender) { + if (!voicePlayers.containsKey(player)) { + return; + } + Iterator pairsItr = voicePairs.iterator(); + while (pairsItr.hasNext()) { + VoicePair voicePair = pairsItr.next(); + EaglercraftUUID target = null; + if (voicePair.uuid1.equals(player)) { + target = voicePair.uuid2; + } else if (voicePair.uuid2.equals(player)) { + target = voicePair.uuid1; } - byte[] userDisconnectPacket = null; - Iterator pairsItr = voicePairs.iterator(); - while (pairsItr.hasNext()) { - VoicePair voicePair = pairsItr.next(); - EaglercraftUUID target = null; - if (voicePair.uuid1.equals(player)) { - target = voicePair.uuid2; - } else if (voicePair.uuid2.equals(player)) { - target = voicePair.uuid1; - } - if (target != null) { - pairsItr.remove(); - EntityPlayerMP conn = voicePlayers.get(target); - if (conn != null) { - if (userDisconnectPacket == null) { - userDisconnectPacket = IntegratedVoiceSignalPackets.makeVoiceSignalPacketDisconnect(player); - } - conn.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, - new PacketBuffer(Unpooled.buffer(userDisconnectPacket, userDisconnectPacket.length) - .writerIndex(userDisconnectPacket.length)))); - } - sender.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, - IntegratedVoiceSignalPackets.makeVoiceSignalPacketDisconnectPB(target))); + if (target != null) { + pairsItr.remove(); + EntityPlayerMP conn = voicePlayers.get(target); + if (conn != null) { + conn.playerNetServerHandler.sendEaglerMessage(new SPacketVoiceSignalDisconnectPeerEAG(player.msb, player.lsb)); } + sender.playerNetServerHandler.sendEaglerMessage(new SPacketVoiceSignalDisconnectPeerEAG(target.msb, target.lsb)); } - } else { - removeUser(sender.getUniqueID()); } } @@ -216,16 +210,16 @@ public class IntegratedVoiceService { } voiceRequests.remove(user); if (voicePlayers.size() > 0) { - byte[] voicePlayersPkt = IntegratedVoiceSignalPackets.makeVoiceSignalPacketGlobal(voicePlayers.values()); + Collection userDatas = new ArrayList<>(voicePlayers.size()); + for(EntityPlayerMP player : voicePlayers.values()) { + EaglercraftUUID uuid = player.getUniqueID(); + userDatas.add(new SPacketVoiceSignalGlobalEAG.UserData(uuid.msb, uuid.lsb, player.getName())); + } + SPacketVoiceSignalGlobalEAG packetToBroadcast = new SPacketVoiceSignalGlobalEAG(userDatas); for (EntityPlayerMP userCon : voicePlayers.values()) { - if (!user.equals(userCon.getUniqueID())) { - userCon.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, - new PacketBuffer(Unpooled.buffer(voicePlayersPkt, voicePlayersPkt.length) - .writerIndex(voicePlayersPkt.length)))); - } + userCon.playerNetServerHandler.sendEaglerMessage(packetToBroadcast); } } - byte[] userDisconnectPacket = null; Iterator pairsItr = voicePairs.iterator(); while (pairsItr.hasNext()) { VoicePair voicePair = pairsItr.next(); @@ -240,12 +234,7 @@ public class IntegratedVoiceService { if (voicePlayers.size() > 0) { EntityPlayerMP conn = voicePlayers.get(target); if (conn != null) { - if (userDisconnectPacket == null) { - userDisconnectPacket = IntegratedVoiceSignalPackets.makeVoiceSignalPacketDisconnect(user); - } - conn.playerNetServerHandler.sendPacket(new S3FPacketCustomPayload(CHANNEL, - new PacketBuffer(Unpooled.buffer(userDisconnectPacket, userDisconnectPacket.length) - .writerIndex(userDisconnectPacket.length)))); + conn.playerNetServerHandler.sendEaglerMessage(new SPacketVoiceSignalDisconnectPeerEAG(user.msb, user.lsb)); } } } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/voice/IntegratedVoiceSignalPackets.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/voice/IntegratedVoiceSignalPackets.java deleted file mode 100644 index 017916b5..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/server/voice/IntegratedVoiceSignalPackets.java +++ /dev/null @@ -1,198 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.sp.server.voice; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Collection; - -import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; -import net.lax1dude.eaglercraft.v1_8.netty.ByteBuf; -import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.network.PacketBuffer; - -/** - * Copyright (c) 2024 lax1dude. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class IntegratedVoiceSignalPackets { - - static final int VOICE_SIGNAL_ALLOWED = 0; - static final int VOICE_SIGNAL_REQUEST = 0; - static final int VOICE_SIGNAL_CONNECT = 1; - static final int VOICE_SIGNAL_DISCONNECT = 2; - static final int VOICE_SIGNAL_ICE = 3; - static final int VOICE_SIGNAL_DESC = 4; - static final int VOICE_SIGNAL_GLOBAL = 5; - - public static void processPacket(PacketBuffer buffer, EntityPlayerMP sender, IntegratedVoiceService voiceService) throws IOException { - int packetId = -1; - if(buffer.readableBytes() == 0) { - throw new IOException("Zero-length packet recieved"); - } - try { - packetId = buffer.readUnsignedByte(); - switch(packetId) { - case VOICE_SIGNAL_REQUEST: { - voiceService.handleVoiceSignalPacketTypeRequest(buffer.readUuid(), sender); - break; - } - case VOICE_SIGNAL_CONNECT: { - voiceService.handleVoiceSignalPacketTypeConnect(sender); - break; - } - case VOICE_SIGNAL_ICE: { - voiceService.handleVoiceSignalPacketTypeICE(buffer.readUuid(), buffer.readStringFromBuffer(32767), sender); - break; - } - case VOICE_SIGNAL_DESC: { - voiceService.handleVoiceSignalPacketTypeDesc(buffer.readUuid(), buffer.readStringFromBuffer(32767), sender); - break; - } - case VOICE_SIGNAL_DISCONNECT: { - voiceService.handleVoiceSignalPacketTypeDisconnect(buffer.readableBytes() > 0 ? buffer.readUuid() : null, sender); - break; - } - default: { - throw new IOException("Unknown packet type " + packetId); - } - } - if(buffer.readableBytes() > 0) { - throw new IOException("Voice packet is too long!"); - } - }catch(IOException ex) { - throw ex; - }catch(Throwable t) { - throw new IOException("Unhandled exception handling voice packet type " + packetId, t); - } - } - - static byte[] makeVoiceSignalPacketAllowed(boolean allowed, String[] iceServers) { - if (iceServers == null) { - byte[] ret = new byte[2]; - ByteBuf wrappedBuffer = Unpooled.buffer(ret, ret.length); - wrappedBuffer.writeByte(VOICE_SIGNAL_ALLOWED); - wrappedBuffer.writeBoolean(allowed); - return ret; - } - byte[][] iceServersBytes = new byte[iceServers.length][]; - int totalLen = 2 + PacketBuffer.getVarIntSize(iceServers.length); - for(int i = 0; i < iceServers.length; ++i) { - byte[] b = iceServersBytes[i] = iceServers[i].getBytes(StandardCharsets.UTF_8); - totalLen += PacketBuffer.getVarIntSize(b.length) + b.length; - } - byte[] ret = new byte[totalLen]; - PacketBuffer wrappedBuffer = new PacketBuffer(Unpooled.buffer(ret, ret.length)); - wrappedBuffer.writeByte(VOICE_SIGNAL_ALLOWED); - wrappedBuffer.writeBoolean(allowed); - wrappedBuffer.writeVarIntToBuffer(iceServersBytes.length); - for(int i = 0; i < iceServersBytes.length; ++i) { - byte[] b = iceServersBytes[i]; - wrappedBuffer.writeVarIntToBuffer(b.length); - wrappedBuffer.writeBytes(b); - } - return ret; - } - - static byte[] makeVoiceSignalPacketGlobal(Collection users) { - int cnt = users.size(); - byte[][] displayNames = new byte[cnt][]; - int i = 0; - for(EntityPlayerMP user : users) { - String name = user.getName(); - if(name.length() > 16) name = name.substring(0, 16); - displayNames[i++] = name.getBytes(StandardCharsets.UTF_8); - } - int totalLength = 1 + PacketBuffer.getVarIntSize(cnt) + (cnt << 4); - for(i = 0; i < cnt; ++i) { - totalLength += PacketBuffer.getVarIntSize(displayNames[i].length) + displayNames[i].length; - } - byte[] ret = new byte[totalLength]; - PacketBuffer wrappedBuffer = new PacketBuffer(Unpooled.buffer(ret, ret.length)); - wrappedBuffer.writeByte(VOICE_SIGNAL_GLOBAL); - wrappedBuffer.writeVarIntToBuffer(cnt); - for(EntityPlayerMP user : users) { - wrappedBuffer.writeUuid(user.getUniqueID()); - } - for(i = 0; i < cnt; ++i) { - wrappedBuffer.writeVarIntToBuffer(displayNames[i].length); - wrappedBuffer.writeBytes(displayNames[i]); - } - return ret; - } - - static PacketBuffer makeVoiceSignalPacketConnect(EaglercraftUUID player, boolean offer) { - byte[] ret = new byte[18]; - PacketBuffer wrappedBuffer = new PacketBuffer(Unpooled.buffer(ret, ret.length)); - wrappedBuffer.writeByte(VOICE_SIGNAL_CONNECT); - wrappedBuffer.writeUuid(player); - wrappedBuffer.writeBoolean(offer); - return wrappedBuffer; - } - - static byte[] makeVoiceSignalPacketConnectAnnounce(EaglercraftUUID player) { - byte[] ret = new byte[17]; - PacketBuffer wrappedBuffer = new PacketBuffer(Unpooled.buffer(ret, ret.length)); - wrappedBuffer.writeByte(VOICE_SIGNAL_CONNECT); - wrappedBuffer.writeUuid(player); - return ret; - } - - static byte[] makeVoiceSignalPacketDisconnect(EaglercraftUUID player) { - if(player == null) { - return new byte[] { (byte)VOICE_SIGNAL_DISCONNECT }; - } - byte[] ret = new byte[17]; - PacketBuffer wrappedBuffer = new PacketBuffer(Unpooled.buffer(ret, ret.length)); - wrappedBuffer.writeByte(VOICE_SIGNAL_DISCONNECT); - wrappedBuffer.writeUuid(player); - return ret; - } - - static PacketBuffer makeVoiceSignalPacketDisconnectPB(EaglercraftUUID player) { - if(player == null) { - byte[] ret = new byte[1]; - PacketBuffer wrappedBuffer = new PacketBuffer(Unpooled.buffer(ret, ret.length)); - wrappedBuffer.writeByte(VOICE_SIGNAL_DISCONNECT); - return wrappedBuffer; - } - byte[] ret = new byte[17]; - PacketBuffer wrappedBuffer = new PacketBuffer(Unpooled.buffer(ret, ret.length)); - wrappedBuffer.writeByte(VOICE_SIGNAL_DISCONNECT); - wrappedBuffer.writeUuid(player); - return wrappedBuffer; - } - - static PacketBuffer makeVoiceSignalPacketICE(EaglercraftUUID player, String str) { - byte[] strBytes = str.getBytes(StandardCharsets.UTF_8); - byte[] ret = new byte[17 + PacketBuffer.getVarIntSize(strBytes.length) + strBytes.length]; - PacketBuffer wrappedBuffer = new PacketBuffer(Unpooled.buffer(ret, ret.length)); - wrappedBuffer.writeByte(VOICE_SIGNAL_ICE); - wrappedBuffer.writeUuid(player); - wrappedBuffer.writeVarIntToBuffer(strBytes.length); - wrappedBuffer.writeBytes(strBytes); - return wrappedBuffer; - } - - static PacketBuffer makeVoiceSignalPacketDesc(EaglercraftUUID player, String str) { - byte[] strBytes = str.getBytes(StandardCharsets.UTF_8); - byte[] ret = new byte[17 + PacketBuffer.getVarIntSize(strBytes.length) + strBytes.length]; - PacketBuffer wrappedBuffer = new PacketBuffer(Unpooled.buffer(ret, ret.length)); - wrappedBuffer.writeByte(VOICE_SIGNAL_DESC); - wrappedBuffer.writeUuid(player); - wrappedBuffer.writeVarIntToBuffer(strBytes.length); - wrappedBuffer.writeBytes(strBytes); - return wrappedBuffer; - } - -} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/socket/NetHandlerSingleplayerLogin.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/socket/NetHandlerSingleplayerLogin.java index 8bcc2f4f..5736b45c 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/socket/NetHandlerSingleplayerLogin.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/socket/NetHandlerSingleplayerLogin.java @@ -1,7 +1,12 @@ package net.lax1dude.eaglercraft.v1_8.sp.socket; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; import net.lax1dude.eaglercraft.v1_8.socket.EaglercraftNetworkManager; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageConstants; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.GamePluginMessageProtocol; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.client.GameProtocolMessageController; import net.lax1dude.eaglercraft.v1_8.update.UpdateService; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiDisconnected; @@ -15,6 +20,7 @@ import net.minecraft.network.login.server.S01PacketEncryptionRequest; import net.minecraft.network.login.server.S02PacketLoginSuccess; import net.minecraft.network.login.server.S03PacketEnableCompression; import net.minecraft.network.play.client.C17PacketCustomPayload; +import net.minecraft.util.ChatComponentText; import net.minecraft.util.IChatComponent; /** @@ -38,6 +44,8 @@ public class NetHandlerSingleplayerLogin implements INetHandlerLoginClient { private final GuiScreen previousGuiScreen; private final EaglercraftNetworkManager networkManager; + private static final Logger logger = LogManager.getLogger("NetHandlerSingleplayerLogin"); + public NetHandlerSingleplayerLogin(EaglercraftNetworkManager parNetworkManager, Minecraft mcIn, GuiScreen parGuiScreen) { this.networkManager = parNetworkManager; this.mc = mcIn; @@ -57,7 +65,19 @@ public class NetHandlerSingleplayerLogin implements INetHandlerLoginClient { @Override public void handleLoginSuccess(S02PacketLoginSuccess var1) { this.networkManager.setConnectionState(EnumConnectionState.PLAY); - this.networkManager.setNetHandler(new NetHandlerPlayClient(this.mc, this.previousGuiScreen, this.networkManager, var1.getProfile())); + int p = var1.getSelectedProtocol(); + GamePluginMessageProtocol mp = GamePluginMessageProtocol.getByVersion(p); + if(mp == null) { + this.networkManager.closeChannel(new ChatComponentText("Unknown protocol selected: " + p)); + return; + } + logger.info("Server is using protocol: {}", p); + NetHandlerPlayClient netHandler = new NetHandlerPlayClient(this.mc, this.previousGuiScreen, this.networkManager, var1.getProfile()); + netHandler.setEaglerMessageController( + new GameProtocolMessageController(mp, GamePluginMessageConstants.CLIENT_TO_SERVER, + GameProtocolMessageController.createClientHandler(p, netHandler), + (ch, msg) -> netHandler.addToSendQueue(new C17PacketCustomPayload(ch, msg)))); + this.networkManager.setNetHandler(netHandler); byte[] b = UpdateService.getClientSignatureData(); if(b != null) { this.networkManager.sendPacket(new C17PacketCustomPayload("EAG|MyUpdCert-1.8", new PacketBuffer(Unpooled.buffer(b, b.length).writerIndex(b.length)))); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchControl.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchControl.java new file mode 100644 index 00000000..f86a9c45 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchControl.java @@ -0,0 +1,579 @@ +package net.lax1dude.eaglercraft.v1_8.touch_gui; + +import net.lax1dude.eaglercraft.v1_8.Touch; +import net.lax1dude.eaglercraft.v1_8.minecraft.EnumInputEvent; +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.client.gui.GuiChat; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.settings.GameSettings; + +/** + * Copyright (c) 2024 lax1dude, ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public enum EnumTouchControl { + + DPAD_UP(EnumTouchControlPos.BOTTOM_LEFT, 60, 109, 44, null, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 56, 0, 22, 22, 2); + }), + + + DPAD_LEFT(EnumTouchControlPos.BOTTOM_LEFT, 11, 60, 44, null, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 56, 22, 22, 22, 2); + }), + + + DPAD_RIGHT(EnumTouchControlPos.BOTTOM_LEFT, 109, 60, 44, null, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 56, 66, 22, 22, 2); + }), + + + DPAD_DOWN(EnumTouchControlPos.BOTTOM_LEFT, 60, 11, 44, null, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 56, 44, 22, 22, 2); + }), + + + DPAD_UP_LEFT(EnumTouchControlPos.BOTTOM_LEFT, 16, 112, 36, null, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 18, 0, 18, 18, 2); + }), + + + DPAD_UP_RIGHT(EnumTouchControlPos.BOTTOM_LEFT, 112, 112, 36, null, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 18, 18, 18, 18, 2); + }), + + + JUMP(EnumTouchControlPos.BOTTOM_RIGHT, 64, 64, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + if(TouchControls.isSneakToggled) { + TouchControls.resetSneakInvalidate(); + } + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 18, 90, 18, 18, 2); + }), + + + SNEAK(EnumTouchControlPos.BOTTOM_LEFT, 64, 64, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + enumIn.invalid = true; + TouchControls.isSneakToggled = !TouchControls.isSneakToggled; + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 18, TouchControls.isSneakToggled ? 126 : 108, 18, 18, 2); + }), + + + BACK(EnumTouchControlPos.TOP, -18, 0, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + if(Touch.isDeviceKeyboardOpenMAYBE()) { + Touch.closeDeviceKeyboard(); + }else { + Minecraft mc = Minecraft.getMinecraft(); + if(mc.thePlayer != null) { + mc.setIngameFocus(); + }else if(mc.currentScreen != null && !(mc.currentScreen instanceof GuiMainMenu)) { + mc.displayGuiScreen(null); + } + } + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 0, 36, 18, 18, 2); + }), + + + BACK_DISABLED(EnumTouchControlPos.TOP, -18, 0, 36, null, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 0, 54, 18, 18, 2); + }), + + + KEYBOARD(EnumTouchControlPos.TOP, 18, 0, 36, null, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 0, 72, 18, 18, 2); + }), + + + PAUSE(EnumTouchControlPos.TOP, -18, 0, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + Minecraft mc = Minecraft.getMinecraft(); + mc.displayInGameMenu(); + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 0, 0, 18, 18, 2); + }), + + + CHAT(EnumTouchControlPos.TOP, 18, 0, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + Minecraft mc = Minecraft.getMinecraft(); + mc.displayGuiScreen(new GuiChat()); + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 0, 18, 18, 18, 2); + }), + + + F3(EnumTouchControlPos.TOP, 144, 0, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + Minecraft mc = Minecraft.getMinecraft(); + GameSettings gameSettings = mc.gameSettings; + gameSettings.showDebugInfo = !gameSettings.showDebugInfo; + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 218, 220, 18, 18, 2); + }), + + + F5(EnumTouchControlPos.TOP, 90, 0, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + Minecraft mc = Minecraft.getMinecraft(); + mc.togglePerspective(); + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 218, 184, 18, 18, 2); + }), + + + PASTE(EnumTouchControlPos.TOP, 144, 0, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + GuiScreen screen = Minecraft.getMinecraft().currentScreen; + if(screen != null) { + screen.fireInputEvent(EnumInputEvent.CLIPBOARD_PASTE, null); + } + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 218, 148, 18, 18, 2); + }), + + + COPY(EnumTouchControlPos.TOP, 90, 0, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + GuiScreen screen = Minecraft.getMinecraft().currentScreen; + if(screen != null) { + screen.fireInputEvent(EnumInputEvent.CLIPBOARD_COPY, null); + } + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 218, 166, 18, 18, 2); + }), + + + PICK(EnumTouchControlPos.BOTTOM_RIGHT, 62, 125, 40, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + Minecraft.getMinecraft().middleClickMouse(); + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 36, 20, 20, 20, 2); + }), + + + FLY(EnumTouchControlPos.BOTTOM_LEFT, 16, 16, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + TouchControls.resetSneak(); + EntityPlayerSP player = Minecraft.getMinecraft().thePlayer; + player.jump(); + player.capabilities.isFlying = true; + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 18, 72, 18, 18, 2); + }), + + + FLY_UP(EnumTouchControlPos.BOTTOM_RIGHT, 12, 120, 36, null, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 18, 36, 18, 18, 2); + }), + + + FLY_DOWN(EnumTouchControlPos.BOTTOM_RIGHT, 12, 75, 36, null, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 18, 54, 18, 18, 2); + }), + + + FLY_END(EnumTouchControlPos.BOTTOM_RIGHT, 64, 64, 36, (enumIn, x, y) -> { + if(!TouchControls.isPressed(enumIn)) { + Minecraft.getMinecraft().thePlayer.capabilities.isFlying = false; + } + }, (enumIn, x, y, pressed, mc, res) -> { + mc.getTextureManager().bindTexture(TouchOverlayRenderer.spriteSheet); + int[] pos = enumIn.getLocation(res, TouchOverlayRenderer._fuck); + TouchOverlayRenderer.drawTexturedModalRect(pos[0], pos[1], 18, 72, 18, 18, 2); + }); + + public static interface TouchAction { + void call(EnumTouchControl enumIn, int x, int y); + } + + public static interface TouchRender { + void call(EnumTouchControl enumIn, int x, int y, boolean pressed, Minecraft mc, ScaledResolution res); + } + + protected final EnumTouchControlPos pos; + protected final int offX; + protected final int offY; + protected final int size; + protected final TouchAction action; + protected final TouchRender render; + + protected boolean visible = true; + protected boolean invalid = true; + + public static final EnumTouchControl[] _VALUES = values(); + + EnumTouchControl(EnumTouchControlPos pos, int offX, int offY, int size, TouchAction action, TouchRender render) { + this.pos = pos; + this.offX = offX; + this.offY = offY; + this.size = size; + this.action = action; + this.render = render; + } + + public int[] getLocation(ScaledResolution scaledResolution, int[] loc) { + if(loc == null) { + loc = new int[2]; + } + int sz = size; + switch (pos) { + case TOP_LEFT: + loc[0] = offX; + loc[1] = offY; + break; + case TOP: + loc[0] = offX + (scaledResolution.getScaledWidth() - sz) / 2; + loc[1] = offY; + break; + case TOP_RIGHT: + loc[0] = -offX + (scaledResolution.getScaledWidth() - sz); + loc[1] = offY; + break; + case LEFT: + loc[0] = offX; + loc[1] = offY + (scaledResolution.getScaledHeight() - sz) / 2; + break; + case RIGHT: + loc[0] = -offX + (scaledResolution.getScaledWidth() - sz); + loc[1] = offY + (scaledResolution.getScaledHeight() - sz) / 2; + break; + case BOTTOM_LEFT: + loc[0] = offX; + loc[1] = -offY + (scaledResolution.getScaledHeight() - sz); + break; + case BOTTOM: + loc[0] = offX + (scaledResolution.getScaledWidth() - sz) / 2; + loc[1] = -offY + (scaledResolution.getScaledHeight() - sz); + break; + case BOTTOM_RIGHT: + loc[0] = -offX + (scaledResolution.getScaledWidth() - sz); + loc[1] = -offY + (scaledResolution.getScaledHeight() - sz); + break; + } + return loc; + } + + public void setVisible(TouchOverlayRenderer renderer, boolean vis) { + if(visible != vis) { + visible = vis; + invalid = true; + if(vis) { + renderer.invalidate(); + }else { + renderer.invalidateDeep(); + } + } + } + + public int getSize() { + return size; + } + + public TouchAction getAction() { + return action; + } + + public TouchRender getRender() { + return render; + } + + protected static EnumTouchLayoutState currentLayout = null; + + public static void setLayoutState(TouchOverlayRenderer renderer, EnumTouchLayoutState layout) { + if(layout == currentLayout) return; + switch(layout) { + case IN_GUI: + DPAD_UP.setVisible(renderer, false); + DPAD_LEFT.setVisible(renderer, false); + DPAD_RIGHT.setVisible(renderer, false); + DPAD_DOWN.setVisible(renderer, false); + DPAD_UP_LEFT.setVisible(renderer, false); + DPAD_UP_RIGHT.setVisible(renderer, false); + JUMP.setVisible(renderer, false); + SNEAK.setVisible(renderer, false); + BACK.setVisible(renderer, true); + BACK_DISABLED.setVisible(renderer, false); + KEYBOARD.setVisible(renderer, true); + PAUSE.setVisible(renderer, false); + CHAT.setVisible(renderer, false); + F3.setVisible(renderer, false); + F5.setVisible(renderer, false); + PASTE.setVisible(renderer, false); + COPY.setVisible(renderer, false); + PICK.setVisible(renderer, false); + FLY.setVisible(renderer, false); + FLY_UP.setVisible(renderer, false); + FLY_DOWN.setVisible(renderer, false); + FLY_END.setVisible(renderer, false); + break; + case IN_GUI_TYPING: + DPAD_UP.setVisible(renderer, false); + DPAD_LEFT.setVisible(renderer, false); + DPAD_RIGHT.setVisible(renderer, false); + DPAD_DOWN.setVisible(renderer, false); + DPAD_UP_LEFT.setVisible(renderer, false); + DPAD_UP_RIGHT.setVisible(renderer, false); + JUMP.setVisible(renderer, false); + SNEAK.setVisible(renderer, false); + BACK.setVisible(renderer, true); + BACK_DISABLED.setVisible(renderer, false); + KEYBOARD.setVisible(renderer, true); + PAUSE.setVisible(renderer, false); + CHAT.setVisible(renderer, false); + F3.setVisible(renderer, false); + F5.setVisible(renderer, false); + PASTE.setVisible(renderer, true); + COPY.setVisible(renderer, true); + PICK.setVisible(renderer, false); + FLY.setVisible(renderer, false); + FLY_UP.setVisible(renderer, false); + FLY_DOWN.setVisible(renderer, false); + FLY_END.setVisible(renderer, false); + break; + case IN_GUI_NO_BACK: + DPAD_UP.setVisible(renderer, false); + DPAD_LEFT.setVisible(renderer, false); + DPAD_RIGHT.setVisible(renderer, false); + DPAD_DOWN.setVisible(renderer, false); + DPAD_UP_LEFT.setVisible(renderer, false); + DPAD_UP_RIGHT.setVisible(renderer, false); + JUMP.setVisible(renderer, false); + SNEAK.setVisible(renderer, false); + BACK.setVisible(renderer, false); + BACK_DISABLED.setVisible(renderer, true); + KEYBOARD.setVisible(renderer, true); + PAUSE.setVisible(renderer, false); + CHAT.setVisible(renderer, false); + F3.setVisible(renderer, false); + F5.setVisible(renderer, false); + PASTE.setVisible(renderer, false); + COPY.setVisible(renderer, false); + PICK.setVisible(renderer, false); + FLY.setVisible(renderer, false); + FLY_UP.setVisible(renderer, false); + FLY_DOWN.setVisible(renderer, false); + FLY_END.setVisible(renderer, false); + break; + case IN_GAME: + DPAD_UP.setVisible(renderer, true); + DPAD_LEFT.setVisible(renderer, true); + DPAD_RIGHT.setVisible(renderer, true); + DPAD_DOWN.setVisible(renderer, true); + DPAD_UP_LEFT.setVisible(renderer, false); + DPAD_UP_RIGHT.setVisible(renderer, false); + JUMP.setVisible(renderer, true); + SNEAK.setVisible(renderer, true); + BACK.setVisible(renderer, false); + BACK_DISABLED.setVisible(renderer, false); + KEYBOARD.setVisible(renderer, false); + PAUSE.setVisible(renderer, true); + CHAT.setVisible(renderer, true); + F3.setVisible(renderer, true); + F5.setVisible(renderer, true); + PASTE.setVisible(renderer, false); + COPY.setVisible(renderer, false); + PICK.setVisible(renderer, true); + FLY.setVisible(renderer, false); + FLY_UP.setVisible(renderer, false); + FLY_DOWN.setVisible(renderer, false); + FLY_END.setVisible(renderer, false); + break; + case IN_GAME_WALK: + DPAD_UP.setVisible(renderer, true); + DPAD_LEFT.setVisible(renderer, true); + DPAD_RIGHT.setVisible(renderer, true); + DPAD_DOWN.setVisible(renderer, true); + DPAD_UP_LEFT.setVisible(renderer, true); + DPAD_UP_RIGHT.setVisible(renderer, true); + JUMP.setVisible(renderer, true); + SNEAK.setVisible(renderer, true); + BACK.setVisible(renderer, false); + BACK_DISABLED.setVisible(renderer, false); + KEYBOARD.setVisible(renderer, false); + PAUSE.setVisible(renderer, true); + CHAT.setVisible(renderer, true); + F3.setVisible(renderer, true); + F5.setVisible(renderer, true); + PASTE.setVisible(renderer, false); + COPY.setVisible(renderer, false); + PICK.setVisible(renderer, true); + FLY.setVisible(renderer, false); + FLY_UP.setVisible(renderer, false); + FLY_DOWN.setVisible(renderer, false); + FLY_END.setVisible(renderer, false); + break; + case IN_GAME_CAN_FLY: + DPAD_UP.setVisible(renderer, true); + DPAD_LEFT.setVisible(renderer, true); + DPAD_RIGHT.setVisible(renderer, true); + DPAD_DOWN.setVisible(renderer, true); + DPAD_UP_LEFT.setVisible(renderer, false); + DPAD_UP_RIGHT.setVisible(renderer, false); + JUMP.setVisible(renderer, true); + SNEAK.setVisible(renderer, true); + BACK.setVisible(renderer, false); + BACK_DISABLED.setVisible(renderer, false); + KEYBOARD.setVisible(renderer, false); + PAUSE.setVisible(renderer, true); + CHAT.setVisible(renderer, true); + F3.setVisible(renderer, true); + F5.setVisible(renderer, true); + PASTE.setVisible(renderer, false); + COPY.setVisible(renderer, false); + PICK.setVisible(renderer, true); + FLY.setVisible(renderer, true); + FLY_UP.setVisible(renderer, false); + FLY_DOWN.setVisible(renderer, false); + FLY_END.setVisible(renderer, false); + break; + case IN_GAME_WALK_CAN_FLY: + DPAD_UP.setVisible(renderer, true); + DPAD_LEFT.setVisible(renderer, true); + DPAD_RIGHT.setVisible(renderer, true); + DPAD_DOWN.setVisible(renderer, true); + DPAD_UP_LEFT.setVisible(renderer, true); + DPAD_UP_RIGHT.setVisible(renderer, true); + JUMP.setVisible(renderer, true); + SNEAK.setVisible(renderer, true); + BACK.setVisible(renderer, false); + BACK_DISABLED.setVisible(renderer, false); + KEYBOARD.setVisible(renderer, false); + PAUSE.setVisible(renderer, true); + CHAT.setVisible(renderer, true); + F3.setVisible(renderer, true); + F5.setVisible(renderer, true); + PASTE.setVisible(renderer, false); + COPY.setVisible(renderer, false); + PICK.setVisible(renderer, true); + FLY.setVisible(renderer, true); + FLY_UP.setVisible(renderer, false); + FLY_DOWN.setVisible(renderer, false); + FLY_END.setVisible(renderer, false); + break; + case IN_GAME_FLYING: + DPAD_UP.setVisible(renderer, true); + DPAD_LEFT.setVisible(renderer, true); + DPAD_RIGHT.setVisible(renderer, true); + DPAD_DOWN.setVisible(renderer, true); + DPAD_UP_LEFT.setVisible(renderer, false); + DPAD_UP_RIGHT.setVisible(renderer, false); + JUMP.setVisible(renderer, false); + SNEAK.setVisible(renderer, true); + BACK.setVisible(renderer, false); + BACK_DISABLED.setVisible(renderer, false); + KEYBOARD.setVisible(renderer, false); + PAUSE.setVisible(renderer, true); + CHAT.setVisible(renderer, true); + F3.setVisible(renderer, true); + F5.setVisible(renderer, true); + PASTE.setVisible(renderer, false); + COPY.setVisible(renderer, false); + PICK.setVisible(renderer, true); + FLY.setVisible(renderer, false); + FLY_UP.setVisible(renderer, true); + FLY_DOWN.setVisible(renderer, true); + FLY_END.setVisible(renderer, true); + break; + case IN_GAME_WALK_FLYING: + DPAD_UP.setVisible(renderer, true); + DPAD_LEFT.setVisible(renderer, true); + DPAD_RIGHT.setVisible(renderer, true); + DPAD_DOWN.setVisible(renderer, true); + DPAD_UP_LEFT.setVisible(renderer, true); + DPAD_UP_RIGHT.setVisible(renderer, true); + JUMP.setVisible(renderer, false); + SNEAK.setVisible(renderer, true); + BACK.setVisible(renderer, false); + BACK_DISABLED.setVisible(renderer, false); + KEYBOARD.setVisible(renderer, false); + PAUSE.setVisible(renderer, true); + CHAT.setVisible(renderer, true); + F3.setVisible(renderer, true); + F5.setVisible(renderer, true); + PASTE.setVisible(renderer, false); + COPY.setVisible(renderer, false); + PICK.setVisible(renderer, true); + FLY.setVisible(renderer, false); + FLY_UP.setVisible(renderer, true); + FLY_DOWN.setVisible(renderer, true); + FLY_END.setVisible(renderer, true); + break; + default: + throw new IllegalStateException(); + } + currentLayout = layout; + } + +} \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchControlPos.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchControlPos.java new file mode 100644 index 00000000..fa10bd87 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchControlPos.java @@ -0,0 +1,20 @@ +package net.lax1dude.eaglercraft.v1_8.touch_gui; + +/** + * Copyright (c) 2024 ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public enum EnumTouchControlPos { + TOP_LEFT, TOP, TOP_RIGHT, LEFT, RIGHT, BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT +} \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchLayoutState.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchLayoutState.java new file mode 100644 index 00000000..55601f14 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/EnumTouchLayoutState.java @@ -0,0 +1,28 @@ +package net.lax1dude.eaglercraft.v1_8.touch_gui; + +/** + * Copyright (c) 2024 lax1due. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public enum EnumTouchLayoutState { + IN_GUI, + IN_GUI_TYPING, + IN_GUI_NO_BACK, + IN_GAME, + IN_GAME_WALK, + IN_GAME_CAN_FLY, + IN_GAME_WALK_CAN_FLY, + IN_GAME_FLYING, + IN_GAME_WALK_FLYING; +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchControlInput.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchControlInput.java new file mode 100644 index 00000000..03d01059 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchControlInput.java @@ -0,0 +1,27 @@ +package net.lax1dude.eaglercraft.v1_8.touch_gui; + +/** + * Copyright (c) 2024 ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class TouchControlInput { + public int x; + public int y; + public final EnumTouchControl control; + public TouchControlInput(int x, int y, EnumTouchControl control) { + this.x = x; + this.y = y; + this.control = control; + } +} \ No newline at end of file diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchControls.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchControls.java new file mode 100644 index 00000000..aa1b59cf --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchControls.java @@ -0,0 +1,164 @@ +package net.lax1dude.eaglercraft.v1_8.touch_gui; + +import net.lax1dude.eaglercraft.v1_8.Touch; +import net.lax1dude.eaglercraft.v1_8.touch_gui.EnumTouchControl.TouchAction; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; + +import java.util.*; + +/** + * Copyright (c) 2024 lax1dude, ayunami2000. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class TouchControls { + + public static final Map touchControls = new HashMap<>(); + protected static Set touchControlPressed = EnumSet.noneOf(EnumTouchControl.class); + + protected static boolean isSneakToggled = false; + + public static void update(boolean screenTouched) { + Minecraft mc = Minecraft.getMinecraft(); + int h = mc.displayHeight; + final ScaledResolution sr = mc.scaledResolution; + int fac = sr.getScaleFactor(); + if(screenTouched) { + int touchPoints = Touch.touchPointCount(); + int[] loc; + for(int i = 0; i < touchPoints; ++i) { + int x = Touch.touchPointX(i); + int y = h - Touch.touchPointY(i) - 1; + int uid = Touch.touchPointUID(i); + TouchControlInput input = touchControls.get(uid); + if(input != null) { + EnumTouchControl ctrl = input.control; + loc = ctrl.getLocation(sr, TouchOverlayRenderer._fuck); + loc[0] *= fac; + loc[1] *= fac; + int size = ctrl.getSize() * fac; + if (x >= loc[0] && y >= loc[1] && x < loc[0] + size && y < loc[1] + size) { + continue; + } + EnumTouchControl[] en = EnumTouchControl._VALUES; + for (int j = 0; j < en.length; ++j) { + EnumTouchControl control = en[j]; + if(!control.visible) continue; + loc = control.getLocation(sr, TouchOverlayRenderer._fuck); + loc[0] *= fac; + loc[1] *= fac; + size = control.getSize() * fac; + if (x >= loc[0] && y >= loc[1] && x < loc[0] + size && y < loc[1] + size) { + touchControls.put(uid, new TouchControlInput(x / fac, y / fac, control)); + break; + } + } + } + } + mc.ingameGUI.updateTouchEagler(mc.currentScreen == null); + }else { + touchControls.clear(); + touchControlPressed.clear(); + mc.ingameGUI.updateTouchEagler(false); + } + } + + public static boolean handleTouchBegin(int uid, int pointX, int pointY) { + Minecraft mc = Minecraft.getMinecraft(); + pointY = mc.displayHeight - pointY - 1; + EnumTouchControl control = overlappingControl0(pointX, pointY, mc.scaledResolution); + if(control != null) { + int fac = mc.scaledResolution.getScaleFactor(); + touchControls.put(uid, new TouchControlInput(pointX / fac, pointY / fac, control)); + return true; + }else { + return mc.currentScreen == null && Minecraft.getMinecraft().ingameGUI.handleTouchBeginEagler(uid, pointX, pointY); + } + } + + public static boolean handleTouchEnd(int uid, int pointX, int pointY) { + if(touchControls.remove(uid) != null) { + return true; + }else { + Minecraft mc = Minecraft.getMinecraft(); + return mc.currentScreen == null && mc.ingameGUI.handleTouchEndEagler(uid, pointX, mc.displayHeight - pointY - 1); + } + } + + public static void resetSneak() { + isSneakToggled = false; + } + + public static void resetSneakInvalidate() { + if(isSneakToggled) { + isSneakToggled = false; + EnumTouchControl.SNEAK.invalid = true; + Minecraft.getMinecraft().touchOverlayRenderer.invalidate(); + } + } + + public static void handleInput() { + if(!touchControls.isEmpty()) { + Set newPressed = EnumSet.noneOf(EnumTouchControl.class); + TouchOverlayRenderer renderer = Minecraft.getMinecraft().touchOverlayRenderer; + for (TouchControlInput input : touchControls.values()) { + TouchAction action = input.control.getAction(); + if(action != null) { + action.call(input.control, input.x, input.y); + } + if(input.control.invalid) { + renderer.invalidate(); + } + newPressed.add(input.control); + } + touchControlPressed = newPressed; + }else { + touchControlPressed.clear(); + } + } + + public static boolean isPressed(EnumTouchControl control) { + return touchControlPressed.contains(control); + } + + public static boolean getSneakToggled() { + return isSneakToggled; + } + + public static EnumTouchControl overlappingControl(int tx, int ty) { + Minecraft mc = Minecraft.getMinecraft(); + ty = mc.displayHeight - ty - 1; + return overlappingControl0(tx, ty, mc.scaledResolution); + } + + private static EnumTouchControl overlappingControl0(int pointX, int pointY, ScaledResolution sr) { + EnumTouchControl[] en = EnumTouchControl._VALUES; + int[] loc; + int fac = sr.getScaleFactor(); + int size; + for (int j = 0; j < en.length; ++j) { + EnumTouchControl control = en[j]; + if(!control.visible) continue; + loc = control.getLocation(sr, TouchOverlayRenderer._fuck); + loc[0] *= fac; + loc[1] *= fac; + size = control.getSize() * fac; + if (pointX >= loc[0] && pointY >= loc[1] && pointX < loc[0] + size && pointY < loc[1] + size) { + return control; + } + } + return null; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchOverlayRenderer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchOverlayRenderer.java new file mode 100644 index 00000000..040f28ac --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/touch_gui/TouchOverlayRenderer.java @@ -0,0 +1,197 @@ +package net.lax1dude.eaglercraft.v1_8.touch_gui; + +import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; +import net.lax1dude.eaglercraft.v1_8.Touch; +import net.lax1dude.eaglercraft.v1_8.opengl.GameOverlayFramebuffer; +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; +import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.util.MathHelper; +import net.minecraft.util.ResourceLocation; + +import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; + +import java.util.Set; + +import com.google.common.collect.Sets; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class TouchOverlayRenderer { + + public static final ResourceLocation spriteSheet = new ResourceLocation("eagler:gui/touch_gui.png"); + + static final int[] _fuck = new int[2]; + + private GameOverlayFramebuffer overlayFramebuffer; + private final Minecraft mc; + private boolean invalid = false; + private boolean invalidDeep = false; + private int currentWidth = -1; + private int currentHeight = -1; + + public TouchOverlayRenderer(Minecraft mc) { + this.mc = mc; + this.overlayFramebuffer = new GameOverlayFramebuffer(false); + EnumTouchControl.currentLayout = null; + EnumTouchControl.setLayoutState(this, EnumTouchLayoutState.IN_GUI); + } + + public void invalidate() { + invalid = true; + } + + public void invalidateDeep() { + invalid = true; + invalidDeep = true; + } + + public void render(int w, int h, ScaledResolution scaledResolution) { + if(PointerInputAbstraction.isTouchMode()) { + render0(w, h, scaledResolution); + if(EnumTouchControl.KEYBOARD.visible) { + int[] pos = EnumTouchControl.KEYBOARD.getLocation(scaledResolution, _fuck); + int scale = scaledResolution.getScaleFactor(); + int size = EnumTouchControl.KEYBOARD.size * scale; + Touch.touchSetOpenKeyboardZone(pos[0] * scale, + (scaledResolution.getScaledHeight() - pos[1] - 1) * scale - size, size, size); + }else { + Touch.touchSetOpenKeyboardZone(0, 0, 0, 0); + } + }else { + Touch.touchSetOpenKeyboardZone(0, 0, 0, 0); + } + } + + private void render0(int w, int h, ScaledResolution scaledResolution) { + EnumTouchControl.setLayoutState(this, hashLayoutState()); + int sw = scaledResolution.getScaledWidth(); + int sh = scaledResolution.getScaledHeight(); + if(currentWidth != sw || currentHeight != sh) { + invalidateDeep(); + } + GlStateManager.disableDepth(); + GlStateManager.disableBlend(); + GlStateManager.disableLighting(); + GlStateManager.enableAlpha(); + GlStateManager.depthMask(false); + if(invalid) { + GlStateManager.pushMatrix(); + invalidDeep |= overlayFramebuffer.beginRender(sw, sh); + GlStateManager.viewport(0, 0, sw, sh); + if(invalidDeep) { + currentWidth = sw; + currentHeight = sh; + GlStateManager.clearColor(0.0f, 0.0f, 0.0f, 0.0f); + GlStateManager.clear(GL_COLOR_BUFFER_BIT); + } + Set controls = Sets.newHashSet(EnumTouchControl._VALUES); + for (TouchControlInput input : TouchControls.touchControls.values()) { + controls.remove(input.control); + } + for (EnumTouchControl control : controls) { + if(invalidDeep || control.invalid) { + if(control.visible) { + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); + control.getRender().call(control, 0, 0, false, mc, scaledResolution); + } + control.invalid = false; + } + } + for (TouchControlInput input : TouchControls.touchControls.values()) { + EnumTouchControl control = input.control; + if(invalidDeep || control.invalid) { + if(control.visible) { + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); + control.getRender().call(control, input.x, input.y, true, mc, scaledResolution); + } + control.invalid = false; + } + } + overlayFramebuffer.endRender(); + invalid = false; + invalidDeep = false; + GlStateManager.popMatrix(); + GlStateManager.viewport(0, 0, w, h); + } + GlStateManager.bindTexture(overlayFramebuffer.getTexture()); + GlStateManager.enableBlend(); + GlStateManager.blendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + GlStateManager.enableAlpha(); + GlStateManager.color(1.0f, 1.0f, 1.0f, MathHelper.clamp_float(mc.gameSettings.touchControlOpacity, 0.0f, 1.0f)); + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer worldrenderer = tessellator.getWorldRenderer(); + worldrenderer.begin(7, DefaultVertexFormats.POSITION_TEX); + worldrenderer.pos(0.0D, (double) sh, 500.0D).tex(0.0D, 0.0D).endVertex(); + worldrenderer.pos((double) sw, (double) sh, 500.0D).tex(1.0D, 0.0D).endVertex(); + worldrenderer.pos((double) sw, 0.0D, 500.0D).tex(1.0D, 1.0D).endVertex(); + worldrenderer.pos(0.0D, 0.0D, 500.0D).tex(0.0D, 1.0D).endVertex(); + tessellator.draw(); + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); + GlStateManager.enableDepth(); + GlStateManager.depthMask(true); + } + + private EnumTouchLayoutState hashLayoutState() { + if(mc.currentScreen != null) { + return mc.currentScreen.showCopyPasteButtons() ? EnumTouchLayoutState.IN_GUI_TYPING + : (mc.currentScreen.canCloseGui() ? EnumTouchLayoutState.IN_GUI + : EnumTouchLayoutState.IN_GUI_NO_BACK); + } + EntityPlayerSP player = mc.thePlayer; + if(player != null) { + if(player.capabilities.isFlying) { + return showDiagButtons() ? EnumTouchLayoutState.IN_GAME_WALK_FLYING : EnumTouchLayoutState.IN_GAME_FLYING; + }else { + if(player.capabilities.allowFlying) { + return showDiagButtons() ? EnumTouchLayoutState.IN_GAME_WALK_CAN_FLY : EnumTouchLayoutState.IN_GAME_CAN_FLY; + }else { + return showDiagButtons() ? EnumTouchLayoutState.IN_GAME_WALK : EnumTouchLayoutState.IN_GAME; + } + } + }else { + return showDiagButtons() ? EnumTouchLayoutState.IN_GAME_WALK : EnumTouchLayoutState.IN_GAME; + } + } + + private boolean showDiagButtons() { + return TouchControls.isPressed(EnumTouchControl.DPAD_UP) + || TouchControls.isPressed(EnumTouchControl.DPAD_UP_LEFT) + || TouchControls.isPressed(EnumTouchControl.DPAD_UP_RIGHT); + } + + protected static void drawTexturedModalRect(float xCoord, float yCoord, int minU, int minV, int maxU, int maxV, int scaleFac) { + float f = 0.00390625F; + float f1 = 0.00390625F; + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer worldrenderer = tessellator.getWorldRenderer(); + worldrenderer.begin(7, DefaultVertexFormats.POSITION_TEX); + worldrenderer.pos((double) (xCoord + 0.0F), (double) (yCoord + (float) maxV * scaleFac), 0.0) + .tex((double) ((float) (minU + 0) * f), (double) ((float) (minV + maxV) * f1)).endVertex(); + worldrenderer.pos((double) (xCoord + (float) maxU * scaleFac), (double) (yCoord + (float) maxV * scaleFac), 0.0) + .tex((double) ((float) (minU + maxU) * f), (double) ((float) (minV + maxV) * f1)).endVertex(); + worldrenderer.pos((double) (xCoord + (float) maxU * scaleFac), (double) (yCoord + 0.0F), 0.0) + .tex((double) ((float) (minU + maxU) * f), (double) ((float) (minV + 0) * f1)).endVertex(); + worldrenderer.pos((double) (xCoord + 0.0F), (double) (yCoord + 0.0F), 0.0) + .tex((double) ((float) (minU + 0) * f), (double) ((float) (minV + 0) * f1)).endVertex(); + tessellator.draw(); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateDownloadSuccess.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateDownloadSuccess.java new file mode 100644 index 00000000..00a79a86 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateDownloadSuccess.java @@ -0,0 +1,60 @@ +package net.lax1dude.eaglercraft.v1_8.update; + +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiUpdateDownloadSuccess extends GuiScreen { + + protected final GuiScreen parent; + protected final UpdateDataObj updateData; + + public GuiUpdateDownloadSuccess(GuiScreen parent, UpdateDataObj updateData) { + this.parent = parent; + this.updateData = updateData; + } + + public void initGui() { + this.buttonList.clear(); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 56, I18n.format("updateSuccess.downloadOffline"))); + this.buttonList.add(new GuiButton(1, this.width / 2 - 100, this.height / 6 + 86, I18n.format("updateSuccess.installToBootMenu"))); + this.buttonList.add(new GuiButton(2, this.width / 2 - 100, this.height / 6 + 130, I18n.format("gui.cancel"))); + } + + public void actionPerformed(GuiButton btn) { + if(btn.id == 0) { + this.mc.loadingScreen.eaglerShow(I18n.format("updateSuccess.downloading"), null); + UpdateService.quine(updateData.clientSignature, updateData.clientBundle); + this.mc.displayGuiScreen(parent); + }else if(btn.id == 1) { + this.mc.displayGuiScreen(new GuiUpdateInstallOptions(this, parent, updateData)); + }else if(btn.id == 2) { + this.mc.displayGuiScreen(parent); + } + } + + public void drawScreen(int par1, int par2, float par3) { + this.drawDefaultBackground(); + this.drawCenteredString(fontRendererObj, I18n.format("updateSuccess.title"), this.width / 2, 50, 11184810); + this.drawCenteredString(fontRendererObj, + updateData.clientSignature.bundleDisplayName + " " + updateData.clientSignature.bundleDisplayVersion, + this.width / 2, 70, 0xFFFFAA); + super.drawScreen(par1, par2, par3); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateInstallOptions.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateInstallOptions.java new file mode 100644 index 00000000..444e25b3 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateInstallOptions.java @@ -0,0 +1,84 @@ +package net.lax1dude.eaglercraft.v1_8.update; + +import net.lax1dude.eaglercraft.v1_8.EaglercraftVersion; +import net.lax1dude.eaglercraft.v1_8.minecraft.GuiScreenGenericErrorMessage; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiUpdateInstallOptions extends GuiScreen { + + protected final GuiScreen parent; + protected final GuiScreen onDone; + protected final UpdateDataObj updateData; + protected boolean makeDefault; + protected boolean enableCountdown; + protected GuiButton makeDefaultBtn; + protected GuiButton enableCountdownBtn; + + public GuiUpdateInstallOptions(GuiScreen parent, GuiScreen onDone, UpdateDataObj updateData) { + this.parent = parent; + this.onDone = onDone; + this.updateData = updateData; + makeDefault = updateData.clientSignature.bundleVersionInteger > EaglercraftVersion.updateBundlePackageVersionInt; + enableCountdown = makeDefault; + } + + public void initGui() { + this.buttonList.clear(); + this.buttonList.add(makeDefaultBtn = new GuiButton(0, this.width / 2 - 100, this.height / 6 + 46, + I18n.format("updateInstall.setDefault") + ": " + I18n.format(makeDefault ? "gui.yes" : "gui.no"))); + this.buttonList.add(enableCountdownBtn = new GuiButton(1, this.width / 2 - 100, this.height / 6 + 76, + I18n.format("updateInstall.setCountdown") + ": " + + I18n.format(enableCountdown ? "gui.yes" : "gui.no"))); + this.buttonList.add(new GuiButton(2, this.width / 2 - 100, this.height / 6 + 110, I18n.format("updateInstall.install"))); + this.buttonList.add(new GuiButton(3, this.width / 2 - 100, this.height / 6 + 140, I18n.format("gui.cancel"))); + + } + + public void actionPerformed(GuiButton btn) { + if(btn.id == 0) { + makeDefault = !makeDefault; + makeDefaultBtn.displayString = I18n.format("updateInstall.setDefault") + ": " + I18n.format(makeDefault ? "gui.yes" : "gui.no"); + }else if(btn.id == 1) { + enableCountdown = !enableCountdown; + enableCountdownBtn.displayString = I18n.format("updateInstall.setCountdown") + ": " + I18n.format(enableCountdown ? "gui.yes" : "gui.no"); + }else if(btn.id == 2) { + mc.loadingScreen.eaglerShow(I18n.format("updateSuccess.installing"), null); + try { + UpdateService.installSignedClient(updateData.clientSignature, updateData.clientBundle, makeDefault, enableCountdown); + }catch(Throwable t) { + mc.displayGuiScreen(new GuiScreenGenericErrorMessage("installFailed.title", t.toString(), onDone)); + return; + } + mc.displayGuiScreen(onDone); + }else if(btn.id == 3) { + mc.displayGuiScreen(parent); + } + } + + public void drawScreen(int mx, int my, float partialTicks) { + this.drawDefaultBackground(); + this.drawCenteredString(fontRendererObj, I18n.format("updateInstall.title"), this.width / 2, 40, 11184810); + this.drawCenteredString(fontRendererObj, + updateData.clientSignature.bundleDisplayName + " " + updateData.clientSignature.bundleDisplayVersion, + this.width / 2, 60, 0xFFFFAA); + super.drawScreen(mx, my, partialTicks); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateVersionList.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateVersionList.java index 905fc161..b965e909 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateVersionList.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateVersionList.java @@ -1,6 +1,8 @@ package net.lax1dude.eaglercraft.v1_8.update; import java.io.IOException; + +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; @@ -60,12 +62,13 @@ public class GuiUpdateVersionList extends GuiScreen { UpdateService.startClientUpdateFrom(slots.certList.get(selected)); } case 0: - default: mc.displayGuiScreen(back); break; case 2: this.initGui(); break; + default: + break; } } @@ -79,6 +82,7 @@ public class GuiUpdateVersionList extends GuiScreen { super.drawScreen(par1, par2, par3); if(tooltip != null) { drawHoveringText(mc.fontRendererObj.listFormattedStringToWidth(tooltip, 180), par1, par2); + GlStateManager.disableLighting(); tooltip = null; } } @@ -88,4 +92,11 @@ public class GuiUpdateVersionList extends GuiScreen { super.handleMouseInput(); slots.handleMouseInput(); } + + @Override + public void handleTouchInput() throws IOException { + super.handleTouchInput(); + slots.handleTouchInput(); + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateVersionSlot.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateVersionSlot.java index 0ed90084..37a2d499 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateVersionSlot.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/GuiUpdateVersionSlot.java @@ -8,7 +8,6 @@ import java.util.Collection; import java.util.Date; import java.util.List; -import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EaglercraftVersion; import net.lax1dude.eaglercraft.v1_8.opengl.EaglercraftGPU; import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; @@ -35,7 +34,7 @@ public class GuiUpdateVersionSlot extends GuiSlot { private static final ResourceLocation eaglerGuiTex = new ResourceLocation("eagler:gui/eagler_gui.png"); - final List certList = new ArrayList(); + final List certList = new ArrayList<>(); final GuiUpdateVersionList screen; @@ -86,7 +85,7 @@ public class GuiUpdateVersionSlot extends GuiSlot { screen.drawBackground(0); } - public static final SimpleDateFormat dateFmt = EagRuntime.fixDateFormat(new SimpleDateFormat("M/dd/yyyy")); + public static final SimpleDateFormat dateFmt = new SimpleDateFormat("M/dd/yyyy"); private static final char[] hexChars = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; @Override diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/RelayUpdateChecker.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/RelayUpdateChecker.java index 2ee5eeb9..f5746807 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/RelayUpdateChecker.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/RelayUpdateChecker.java @@ -13,7 +13,7 @@ import net.lax1dude.eaglercraft.v1_8.internal.PlatformApplication; import net.lax1dude.eaglercraft.v1_8.internal.PlatformWebRTC; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayManager; import net.lax1dude.eaglercraft.v1_8.sp.relay.RelayServerSocket; -import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IPacket00Handshake; +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket00Handshake; import net.minecraft.client.Minecraft; /** @@ -46,7 +46,7 @@ public class RelayUpdateChecker { } - private static final List relaysList = new ArrayList(); + private static final List relaysList = new ArrayList<>(); private static long lastUpdateCheck = -1l; private static boolean hasInit = false; @@ -74,7 +74,8 @@ public class RelayUpdateChecker { } long millis = System.currentTimeMillis(); Minecraft mc = Minecraft.getMinecraft(); - if((mc.theWorld == null || mc.isSingleplayer()) && millis - lastUpdateCheck > updateCheckRate) { + if ((mc.theWorld == null || mc.isSingleplayer()) + && (millis - lastUpdateCheck > updateCheckRate || millis + 60000l < lastUpdateCheck)) { lastUpdateCheck = millis; try { EaglerOutputStream bao = new EaglerOutputStream(8); @@ -120,12 +121,13 @@ public class RelayUpdateChecker { private static void updateRelay(RelayEntry socket) { try { + socket.currentSocket.update(); if(socket.currentSocket.isClosed()) { socket.currentSocket = null; }else if(socket.currentSocket.isOpen()) { if(!socket.handshake) { socket.handshake = true; - socket.currentSocket.writePacket(new IPacket00Handshake(0x02, RelayManager.preferredRelayVersion, magic)); + socket.currentSocket.writePacket(new RelayPacket00Handshake(0x02, RelayManager.preferredRelayVersion, magic)); }else { // close immediately if(socket.currentSocket.nextPacket() != null) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateDataObj.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateDataObj.java new file mode 100644 index 00000000..e039d16b --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateDataObj.java @@ -0,0 +1,28 @@ +package net.lax1dude.eaglercraft.v1_8.update; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class UpdateDataObj { + + public final UpdateCertificate clientSignature; + public final byte[] clientBundle; + + public UpdateDataObj(UpdateCertificate clientSignature, byte[] clientBundle) { + this.clientSignature = clientSignature; + this.clientBundle = clientBundle; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket06ClientFailure.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateResultObj.java similarity index 53% rename from sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket06ClientFailure.java rename to sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateResultObj.java index 9506c373..238d8183 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/IPacket06ClientFailure.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateResultObj.java @@ -1,11 +1,7 @@ -package net.lax1dude.eaglercraft.v1_8.sp.relay.pkt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +package net.lax1dude.eaglercraft.v1_8.update; /** - * Copyright (c) 2022 lax1dude. All Rights Reserved. + * Copyright (c) 2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -19,27 +15,34 @@ import java.io.IOException; * POSSIBILITY OF SUCH DAMAGE. * */ -public class IPacket06ClientFailure extends IPacket { - - public String clientId; - - public IPacket06ClientFailure() { - } - - public IPacket06ClientFailure(String clientId) { - this.clientId = clientId; - } - - public void read(DataInputStream input) throws IOException { - clientId = readASCII8(input); +public class UpdateResultObj { + + private final boolean success; + private final Object dataObj; + + private UpdateResultObj(boolean success, Object dataObj) { + this.success = success; + this.dataObj = dataObj; } - public void write(DataOutputStream output) throws IOException { - writeASCII8(output, clientId); + public static UpdateResultObj createSuccess(UpdateDataObj dataObj) { + return new UpdateResultObj(true, dataObj); } - - public int packetLength() { - return 1 + clientId.length(); + + public static UpdateResultObj createFailure(String dataObj) { + return new UpdateResultObj(false, dataObj); } - + + public boolean isSuccess() { + return success; + } + + public UpdateDataObj getSuccess() { + return (UpdateDataObj)dataObj; + } + + public String getFailure() { + return (String)dataObj; + } + } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateService.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateService.java index 32c1b68d..0e7216d6 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateService.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/update/UpdateService.java @@ -37,9 +37,9 @@ public class UpdateService { private static boolean isBundleDataValid = false; private static UpdateCertificate latestUpdateFound = null; - private static final Set availableUpdates = new HashSet(); - private static final Set fastUpdateKnownCheckSet = new HashSet(); - private static final Set dismissedUpdates = new HashSet(); + private static final Set availableUpdates = new HashSet<>(); + private static final Set fastUpdateKnownCheckSet = new HashSet<>(); + private static final Set dismissedUpdates = new HashSet<>(); private static class RawKnownCertHolder { @@ -50,7 +50,7 @@ public class UpdateService { public RawKnownCertHolder(byte[] data) { this.data = data; this.hashcode = Arrays.hashCode(data); - this.age = System.currentTimeMillis(); + this.age = EagRuntime.steadyTimeMillis(); } public int hashCode() { @@ -176,7 +176,7 @@ public class UpdateService { private static void freeMemory() { if(fastUpdateKnownCheckSet.size() > 127) { - List lst = new ArrayList(fastUpdateKnownCheckSet); + List lst = new ArrayList<>(fastUpdateKnownCheckSet); fastUpdateKnownCheckSet.clear(); lst.sort((c1, c2) -> { return (int)(c2.age - c1.age); }); for(int i = 0; i < 64; ++i) { @@ -193,6 +193,15 @@ public class UpdateService { return PlatformUpdateSvc.getUpdatingStatus(); } + public static UpdateResultObj getUpdateResult() { + return PlatformUpdateSvc.getUpdateResult(); + } + + public static void installSignedClient(UpdateCertificate clientCert, byte[] clientPayload, boolean setDefault, + boolean setTimeout) { + PlatformUpdateSvc.installSignedClient(clientCert, clientPayload, setDefault, setTimeout); + } + public static UpdateCertificate getLatestUpdateFound() { return latestUpdateFound; } @@ -221,6 +230,10 @@ public class UpdateService { } } + public static void quine(UpdateCertificate cert, byte[] payload) { + PlatformUpdateSvc.quine(cert, payload); + } + public static boolean shouldDisableDownloadButton() { return EagRuntime.getConfiguration().getDownloadOfflineButtonLink() == null && (myUpdateCert == null || (getClientBundleData() == null && PlatformUpdateSvc.getUpdatingStatus().isBusy)); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/ExpiringSet.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/ExpiringSet.java index 79af3a27..a3228f67 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/ExpiringSet.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/ExpiringSet.java @@ -5,6 +5,8 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Map; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; + /** * Copyright (c) 2022 ayunami2000. All Rights Reserved. * @@ -42,7 +44,7 @@ public class ExpiringSet extends HashSet { public void checkForExpirations() { Iterator iterator = this.timestamps.keySet().iterator(); - long now = System.currentTimeMillis(); + long now = EagRuntime.steadyTimeMillis(); while (iterator.hasNext()) { T element = iterator.next(); if (super.contains(element)) { @@ -61,7 +63,7 @@ public class ExpiringSet extends HashSet { public boolean add(T o) { checkForExpirations(); boolean success = super.add(o); - if (success) timestamps.put(o, System.currentTimeMillis()); + if (success) timestamps.put(o, EagRuntime.steadyTimeMillis()); return success; } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/GuiVoiceMenu.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/GuiVoiceMenu.java index 2794a68d..caa72908 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/GuiVoiceMenu.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/GuiVoiceMenu.java @@ -5,8 +5,10 @@ import static net.lax1dude.eaglercraft.v1_8.opengl.RealOpenGLEnums.*; import java.util.List; import java.util.Set; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; import net.lax1dude.eaglercraft.v1_8.Keyboard; +import net.lax1dude.eaglercraft.v1_8.PointerInputAbstraction; import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; import net.lax1dude.eaglercraft.v1_8.sp.gui.GuiSlider2; import net.minecraft.client.Minecraft; @@ -124,17 +126,8 @@ public class GuiVoiceMenu extends Gui { public void initGui() { this.sliderBlocks = new GuiSlider2(-1, (width - 150) / 2, height / 3 + 20, 150, 20, (VoiceClientController.getVoiceProximity() - 5) / 17.0f, 1.0f) { - public boolean mousePressed(Minecraft par1Minecraft, int par2, int par3) { - if(super.mousePressed(par1Minecraft, par2, par3)) { - this.displayString = "" + (int)((sliderValue * 17.0f) + 5.0f) + " Blocks"; - return true; - }else { - return false; - } - } - public void mouseDragged(Minecraft par1Minecraft, int par2, int par3) { - super.mouseDragged(par1Minecraft, par2, par3); - this.displayString = "" + (int)((sliderValue * 17.0f) + 5.0f) + " Blocks"; + protected String updateDisplayString() { + return (int)((sliderValue * 17.0f) + 5.0f) + " Blocks"; } }; sliderBlocks.displayString = "" + VoiceClientController.getVoiceProximity() + " Blocks"; @@ -386,7 +379,7 @@ public class GuiVoiceMenu extends Gui { } }else if(status == EnumVoiceChannelStatus.CONNECTING) { - float fadeTimer = MathHelper.sin((float)((System.currentTimeMillis() % 700l) * 0.0014d) * 3.14159f) * 0.35f + 0.3f; + float fadeTimer = MathHelper.sin((float)((EagRuntime.steadyTimeMillis() % 700l) * 0.0014d) * 3.14159f) * 0.35f + 0.3f; txt = I18n.format("voice.connecting"); GlStateManager.enableBlend(); GlStateManager.blendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -487,7 +480,7 @@ public class GuiVoiceMenu extends Gui { drawNotice(I18n.format("voice.unsupportedWarning1"), false, I18n.format("voice.unsupportedWarning2"), I18n.format("voice.unsupportedWarning3"), "", I18n.format("voice.unsupportedWarning4"), I18n.format("voice.unsupportedWarning5"), I18n.format("voice.unsupportedWarning6"), - I18n.format("voice.unsupportedWarning7"), I18n.format("voice.unsupportedWarning8"), I18n.format("voice.unsupportedWarning9")); + I18n.format("voice.unsupportedWarning7"), "", I18n.format("voice.unsupportedWarning8"), I18n.format("voice.unsupportedWarning9")); noticeContinueButton.visible = true; noticeCancelButton.visible = false; @@ -495,8 +488,7 @@ public class GuiVoiceMenu extends Gui { drawNotice(I18n.format("voice.ipGrabWarning1"), true, I18n.format("voice.ipGrabWarning2"), I18n.format("voice.ipGrabWarning3"), I18n.format("voice.ipGrabWarning4"), "", I18n.format("voice.ipGrabWarning5"), I18n.format("voice.ipGrabWarning6"), - I18n.format("voice.ipGrabWarning7"), I18n.format("voice.ipGrabWarning8"), I18n.format("voice.ipGrabWarning9"), - I18n.format("voice.ipGrabWarning10"), I18n.format("voice.ipGrabWarning11"), I18n.format("voice.ipGrabWarning12")); + I18n.format("voice.ipGrabWarning7")); noticeContinueButton.visible = true; noticeCancelButton.visible = true; @@ -590,17 +582,21 @@ public class GuiVoiceMenu extends Gui { } public void mouseReleased(int par1, int par2, int par3) { - applyRadiusButton.mouseReleased(par1, par2); - applyVolumeButton.mouseReleased(par1, par2); - noticeContinueButton.mouseReleased(par1, par2); - noticeCancelButton.mouseReleased(par1, par2); + if(par3 != 0 && par3 != 12345) return; + boolean touchMode = PointerInputAbstraction.isTouchMode(); + if(!touchMode || par3 == 0) { + applyRadiusButton.mouseReleased(par1, par2); + applyVolumeButton.mouseReleased(par1, par2); + noticeContinueButton.mouseReleased(par1, par2); + noticeCancelButton.mouseReleased(par1, par2); + } if(showSliderBlocks || showSliderVolume) { if(showSliderBlocks) { - if(par3 == 0) { + if(!touchMode || par3 == 12345) { sliderBlocks.mouseReleased(par1, par2); } }else if(showSliderVolume) { - if(par3 == 0) { + if(!touchMode || par3 == 12345) { sliderListenVolume.mouseReleased(par1, par2); sliderSpeakVolume.mouseReleased(par1, par2); } @@ -624,19 +620,23 @@ public class GuiVoiceMenu extends Gui { } public void mouseClicked(int mx, int my, int button) { + if(button != 0 && button != 12345) return; + boolean touchMode = PointerInputAbstraction.isTouchMode(); if(showSliderBlocks || showSliderVolume || showPTTKeyConfig || showingCompatWarning || showingTrackingWarning) { if(showSliderBlocks) { - sliderBlocks.mousePressed(mc, mx, my); + if(!touchMode || button == 12345) { + sliderBlocks.mousePressed(mc, mx, my); + } }else if(showSliderVolume) { - sliderListenVolume.mousePressed(mc, mx, my); - sliderSpeakVolume.mousePressed(mc, mx, my); - } - if(button == 0) { - if(applyRadiusButton.mousePressed(mc, mx, my)) actionPerformed(applyRadiusButton); - if(applyVolumeButton.mousePressed(mc, mx, my)) actionPerformed(applyVolumeButton); - if(noticeContinueButton.mousePressed(mc, mx, my)) actionPerformed(noticeContinueButton); - if(noticeCancelButton.mousePressed(mc, mx, my)) actionPerformed(noticeCancelButton); + if(!touchMode || button == 12345) { + sliderListenVolume.mousePressed(mc, mx, my); + sliderSpeakVolume.mousePressed(mc, mx, my); + } } + if((!touchMode || button == 0) && applyRadiusButton.mousePressed(mc, mx, my)) actionPerformed(applyRadiusButton); + if((!touchMode || button == 0) && applyVolumeButton.mousePressed(mc, mx, my)) actionPerformed(applyVolumeButton); + if((!touchMode || button == 0) && noticeContinueButton.mousePressed(mc, mx, my)) actionPerformed(noticeContinueButton); + if((!touchMode || button == 0) && noticeCancelButton.mousePressed(mc, mx, my)) actionPerformed(noticeCancelButton); throw new AbortedException(); } diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/GuiVoiceOverlay.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/GuiVoiceOverlay.java index 2621c60b..b6534cb6 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/GuiVoiceOverlay.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/GuiVoiceOverlay.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import net.lax1dude.eaglercraft.v1_8.EagRuntime; import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; import net.lax1dude.eaglercraft.v1_8.Keyboard; import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; @@ -82,7 +83,7 @@ public class GuiVoiceOverlay extends Gui { mc.getTextureManager().bindTexture(voiceGuiIcons); if((mc.currentScreen == null || !mc.currentScreen.blockPTTKey()) && Keyboard.isKeyDown(mc.gameSettings.voicePTTKey)) { - long millis = System.currentTimeMillis(); + long millis = EagRuntime.steadyTimeMillis(); if(pttTimer == 0l) { pttTimer = millis; } @@ -118,7 +119,7 @@ public class GuiVoiceOverlay extends Gui { Set speakers = VoiceClientController.getVoiceSpeaking(); Set muted = VoiceClientController.getVoiceMuted(); - List listenerList = new ArrayList(); + List listenerList = new ArrayList<>(); listenerList.addAll(listeners); listenerList.removeAll(muted); @@ -145,7 +146,7 @@ public class GuiVoiceOverlay extends Gui { hh -= 15; } - List listenerListStr = new ArrayList(Math.min(5, listenerList.size())); + List listenerListStr = new ArrayList<>(Math.min(5, listenerList.size())); int left = 50; for(int i = 0, l = listenerList.size(); i < l && i < 5; ++i) { @@ -196,7 +197,7 @@ public class GuiVoiceOverlay extends Gui { Set speakers = VoiceClientController.getVoiceSpeaking(); Set muted = VoiceClientController.getVoiceMuted(); - List listenerList = new ArrayList(); + List listenerList = new ArrayList<>(); listenerList.addAll(speakers); listenerList.removeAll(muted); @@ -209,7 +210,7 @@ public class GuiVoiceOverlay extends Gui { hh -= 15; } - List listenerListStr = new ArrayList(Math.min(5, listenerList.size())); + List listenerListStr = new ArrayList<>(Math.min(5, listenerList.size())); int left = 50; for(int i = 0, l = listenerList.size(); i < l && i < 5; ++i) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceClientController.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceClientController.java index 5e3dc801..4e177ad1 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceClientController.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceClientController.java @@ -1,6 +1,7 @@ package net.lax1dude.eaglercraft.v1_8.voice; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -16,9 +17,11 @@ import net.lax1dude.eaglercraft.v1_8.internal.PlatformVoiceClient; import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; import net.lax1dude.eaglercraft.v1_8.log4j.Logger; import net.lax1dude.eaglercraft.v1_8.profile.EaglerProfile; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.*; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketVoiceSignalGlobalEAG; import net.minecraft.client.Minecraft; import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.network.PacketBuffer; /** * Copyright (c) 2022-2024 lax1dude, ayunami2000. All Rights Reserved. @@ -43,7 +46,8 @@ public class VoiceClientController { private static boolean clientSupport = false; private static boolean serverSupport = false; - private static Consumer packetSendCallback = null; + private static Consumer packetSendCallback = null; + private static int protocolVersion = -1; private static EnumVoiceChannelType voiceChannel = EnumVoiceChannelType.NONE; private static final HashSet nearbyPlayers = new HashSet<>(); private static final ExpiringSet recentlyNearbyPlayers = new ExpiringSet<>(5000, uuid -> { @@ -71,17 +75,14 @@ public class VoiceClientController { return serverSupport; } - public static void initializeVoiceClient(Consumer signalSendCallbackIn) { + public static void initializeVoiceClient(Consumer signalSendCallbackIn, int proto) { packetSendCallback = signalSendCallbackIn; + protocolVersion = proto; uuidToNameLookup.clear(); if (getVoiceChannel() != EnumVoiceChannelType.NONE) sendInitialVoice(); } - public static void handleVoiceSignalPacket(PacketBuffer packetData) { - VoiceSignalPackets.handleVoiceSignal(packetData); - } - - static void handleVoiceSignalPacketTypeGlobal(EaglercraftUUID[] voicePlayers, String[] voiceNames) { + public static void handleVoiceSignalPacketTypeGlobal(EaglercraftUUID[] voicePlayers, String[] voiceNames) { uuidToNameLookup.clear(); for (int i = 0; i < voicePlayers.length; i++) { if(voiceNames != null) { @@ -91,6 +92,17 @@ public class VoiceClientController { } } + public static void handleVoiceSignalPacketTypeGlobalNew(Collection voicePlayers) { + uuidToNameLookup.clear(); + for (SPacketVoiceSignalGlobalEAG.UserData player : voicePlayers) { + EaglercraftUUID uuid = new EaglercraftUUID(player.uuidMost, player.uuidLeast); + if(player.username != null) { + uuidToNameLookup.put(uuid, player.username); + } + sendPacketRequestIfNeeded(uuid); + } + } + public static void handleServerDisconnect() { if(!isClientSupported()) return; serverSupport = false; @@ -110,7 +122,7 @@ public class VoiceClientController { activateVoice(false); } - static void handleVoiceSignalPacketTypeAllowed(boolean voiceAvailableStat, String[] servs) { + public static void handleVoiceSignalPacketTypeAllowed(boolean voiceAvailableStat, String[] servs) { serverSupport = voiceAvailableStat; PlatformVoiceClient.setICEServers(servs); if(isSupported()) { @@ -120,23 +132,23 @@ public class VoiceClientController { } } - static void handleVoiceSignalPacketTypeConnect(EaglercraftUUID user, boolean offer) { + public static void handleVoiceSignalPacketTypeConnect(EaglercraftUUID user, boolean offer) { PlatformVoiceClient.signalConnect(user, offer); } - static void handleVoiceSignalPacketTypeConnectAnnounce(EaglercraftUUID user) { + public static void handleVoiceSignalPacketTypeConnectAnnounce(EaglercraftUUID user) { sendPacketRequest(user); } - static void handleVoiceSignalPacketTypeDisconnect(EaglercraftUUID user) { + public static void handleVoiceSignalPacketTypeDisconnect(EaglercraftUUID user) { PlatformVoiceClient.signalDisconnect(user, true); } - static void handleVoiceSignalPacketTypeICECandidate(EaglercraftUUID user, String ice) { + public static void handleVoiceSignalPacketTypeICECandidate(EaglercraftUUID user, String ice) { PlatformVoiceClient.signalICECandidate(user, ice); } - static void handleVoiceSignalPacketTypeDescription(EaglercraftUUID user, String desc) { + public static void handleVoiceSignalPacketTypeDescription(EaglercraftUUID user, String desc) { PlatformVoiceClient.signalDescription(user, desc); } @@ -211,7 +223,7 @@ public class VoiceClientController { for (EaglercraftUUID uuid : antiConcurrentModificationUUIDs) { PlatformVoiceClient.signalDisconnect(uuid, false); } - sendPacketDisconnect(null); + sendPacketDisconnect(); activateVoice(false); } else if (voiceChannel == EnumVoiceChannelType.PROXIMITY) { for (EaglercraftUUID uuid : nearbyPlayers) { @@ -222,7 +234,7 @@ public class VoiceClientController { } nearbyPlayers.clear(); recentlyNearbyPlayers.clear(); - sendPacketDisconnect(null); + sendPacketDisconnect(); } else if(voiceChannel == EnumVoiceChannelType.GLOBAL) { Set antiConcurrentModificationUUIDs = new HashSet<>(listeningSet); antiConcurrentModificationUUIDs.removeAll(nearbyPlayers); @@ -230,7 +242,7 @@ public class VoiceClientController { for (EaglercraftUUID uuid : antiConcurrentModificationUUIDs) { PlatformVoiceClient.signalDisconnect(uuid, false); } - sendPacketDisconnect(null); + sendPacketDisconnect(); } voiceChannel = channel; if (channel != EnumVoiceChannelType.NONE) { @@ -340,23 +352,35 @@ public class VoiceClientController { } public static void sendPacketICE(EaglercraftUUID peerId, String candidate) { - packetSendCallback.accept(VoiceSignalPackets.makeVoiceSignalPacketICE(peerId, candidate)); + packetSendCallback.accept(new CPacketVoiceSignalICEEAG(peerId.msb, peerId.lsb, candidate)); } public static void sendPacketDesc(EaglercraftUUID peerId, String desc) { - packetSendCallback.accept(VoiceSignalPackets.makeVoiceSignalPacketDesc(peerId, desc)); + packetSendCallback.accept(new CPacketVoiceSignalDescEAG(peerId.msb, peerId.lsb, desc)); } - public static void sendPacketDisconnect(EaglercraftUUID peerId) { - packetSendCallback.accept(VoiceSignalPackets.makeVoiceSignalPacketDisconnect(peerId)); + public static void sendPacketDisconnect() { + if(protocolVersion <= 3) { + packetSendCallback.accept(new CPacketVoiceSignalDisconnectV3EAG()); + }else { + packetSendCallback.accept(new CPacketVoiceSignalDisconnectV4EAG()); + } + } + + public static void sendPacketDisconnectPeer(EaglercraftUUID peerId) { + if(protocolVersion <= 3) { + packetSendCallback.accept(new CPacketVoiceSignalDisconnectV3EAG(true, peerId.msb, peerId.lsb)); + }else { + packetSendCallback.accept(new CPacketVoiceSignalDisconnectPeerV4EAG(peerId.msb, peerId.lsb)); + } } public static void sendPacketConnect() { - packetSendCallback.accept(VoiceSignalPackets.makeVoiceSignalPacketConnect()); + packetSendCallback.accept(new CPacketVoiceSignalConnectEAG()); } public static void sendPacketRequest(EaglercraftUUID peerId) { - packetSendCallback.accept(VoiceSignalPackets.makeVoiceSignalPacketRequest(peerId)); + packetSendCallback.accept(new CPacketVoiceSignalRequestEAG(peerId.msb, peerId.lsb)); } private static void sendPacketRequestIfNeeded(EaglercraftUUID uuid) { diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceSignalPackets.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceSignalPackets.java deleted file mode 100644 index 07643a8b..00000000 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceSignalPackets.java +++ /dev/null @@ -1,142 +0,0 @@ -package net.lax1dude.eaglercraft.v1_8.voice; - -import java.nio.charset.StandardCharsets; - -import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; -import net.lax1dude.eaglercraft.v1_8.netty.Unpooled; -import net.minecraft.network.PacketBuffer; - -/** - * Copyright (c) 2024 lax1dude, ayunami2000. All Rights Reserved. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ -public class VoiceSignalPackets { - - static final int VOICE_SIGNAL_ALLOWED = 0; - static final int VOICE_SIGNAL_REQUEST = 0; - static final int VOICE_SIGNAL_CONNECT = 1; - static final int VOICE_SIGNAL_DISCONNECT = 2; - static final int VOICE_SIGNAL_ICE = 3; - static final int VOICE_SIGNAL_DESC = 4; - static final int VOICE_SIGNAL_GLOBAL = 5; - - static void handleVoiceSignal(PacketBuffer streamIn) { - try { - int sig = streamIn.readUnsignedByte(); - switch(sig) { - case VOICE_SIGNAL_ALLOWED: { - boolean voiceAvailableStat = streamIn.readUnsignedByte() == 1; - String[] servs = null; - if(voiceAvailableStat) { - servs = new String[streamIn.readVarIntFromBuffer()]; - for(int i = 0; i < servs.length; i++) { - servs[i] = streamIn.readStringFromBuffer(1024); - } - } - VoiceClientController.handleVoiceSignalPacketTypeAllowed(voiceAvailableStat, servs); - break; - } - case VOICE_SIGNAL_GLOBAL: { - if (VoiceClientController.getVoiceChannel() != EnumVoiceChannelType.GLOBAL) return; - EaglercraftUUID[] voiceIds = new EaglercraftUUID[streamIn.readVarIntFromBuffer()]; - for(int i = 0; i < voiceIds.length; i++) { - voiceIds[i] = streamIn.readUuid(); - } - String[] voiceNames = null; - if (streamIn.isReadable()) { - voiceNames = new String[voiceIds.length]; - for(int i = 0; i < voiceNames.length; i++) { - voiceNames[i] = streamIn.readStringFromBuffer(16); - } - } - VoiceClientController.handleVoiceSignalPacketTypeGlobal(voiceIds, voiceNames); - break; - } - case VOICE_SIGNAL_CONNECT: { - EaglercraftUUID uuid = streamIn.readUuid(); - if (streamIn.isReadable()) { - VoiceClientController.handleVoiceSignalPacketTypeConnect(uuid, streamIn.readBoolean()); - } else if (VoiceClientController.getVoiceChannel() != EnumVoiceChannelType.PROXIMITY || VoiceClientController.getVoiceListening().contains(uuid)) { - VoiceClientController.handleVoiceSignalPacketTypeConnectAnnounce(uuid); - } - break; - } - case VOICE_SIGNAL_DISCONNECT: { - VoiceClientController.handleVoiceSignalPacketTypeDisconnect(streamIn.readableBytes() > 0 ? streamIn.readUuid() : null); - break; - } - case VOICE_SIGNAL_ICE: { - VoiceClientController.handleVoiceSignalPacketTypeICECandidate(streamIn.readUuid(), streamIn.readStringFromBuffer(32767)); - break; - } - case VOICE_SIGNAL_DESC: { - VoiceClientController.handleVoiceSignalPacketTypeDescription(streamIn.readUuid(), streamIn.readStringFromBuffer(32767)); - break; - } - default: { - VoiceClientController.logger.error("Unknown voice signal packet '{}'!", sig); - break; - } - } - }catch(Throwable ex) { - VoiceClientController.logger.error("Failed to handle signal packet!"); - VoiceClientController.logger.error(ex); - } - } - - static PacketBuffer makeVoiceSignalPacketRequest(EaglercraftUUID user) { - PacketBuffer ret = new PacketBuffer(Unpooled.buffer(17, 17)); - ret.writeByte(VOICE_SIGNAL_REQUEST); - ret.writeUuid(user); - return ret; - } - - static PacketBuffer makeVoiceSignalPacketICE(EaglercraftUUID user, String icePacket) { - byte[] str = icePacket.getBytes(StandardCharsets.UTF_8); - int estLen = 17 + PacketBuffer.getVarIntSize(str.length) + str.length; - PacketBuffer ret = new PacketBuffer(Unpooled.buffer(estLen, estLen)); - ret.writeByte(VOICE_SIGNAL_ICE); - ret.writeUuid(user); - ret.writeByteArray(str); - return ret; - } - - static PacketBuffer makeVoiceSignalPacketDesc(EaglercraftUUID user, String descPacket) { - byte[] str = descPacket.getBytes(StandardCharsets.UTF_8); - int estLen = 17 + PacketBuffer.getVarIntSize(str.length) + str.length; - PacketBuffer ret = new PacketBuffer(Unpooled.buffer(estLen, estLen)); - ret.writeByte(VOICE_SIGNAL_DESC); - ret.writeUuid(user); - ret.writeByteArray(str); - return ret; - } - - static PacketBuffer makeVoiceSignalPacketDisconnect(EaglercraftUUID user) { - if (user == null) { - PacketBuffer ret = new PacketBuffer(Unpooled.buffer(1, 1)); - ret.writeByte(VOICE_SIGNAL_DISCONNECT); - return ret; - } - PacketBuffer ret = new PacketBuffer(Unpooled.buffer(17, 17)); - ret.writeByte(VOICE_SIGNAL_DISCONNECT); - ret.writeUuid(user); - return ret; - } - - public static PacketBuffer makeVoiceSignalPacketConnect() { - PacketBuffer ret = new PacketBuffer(Unpooled.buffer(1, 1)); - ret.writeByte(VOICE_SIGNAL_CONNECT); - return ret; - } -} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceTagRenderer.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceTagRenderer.java index 5c047152..b04bf214 100644 --- a/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceTagRenderer.java +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/voice/VoiceTagRenderer.java @@ -33,7 +33,7 @@ public class VoiceTagRenderer { private static final ResourceLocation voiceGuiIcons = new ResourceLocation("eagler:gui/eagler_gui.png"); - private static final Set voiceTagsDrawnThisFrame = new HashSet(); + private static final Set voiceTagsDrawnThisFrame = new HashSet<>(); public static void renderVoiceNameTag(Minecraft mc, EntityOtherPlayerMP player, int offset) { EaglercraftUUID uuid = player.getUniqueID(); diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenPhishingWaring.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenPhishingWaring.java new file mode 100644 index 00000000..e96895fa --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenPhishingWaring.java @@ -0,0 +1,104 @@ +package net.lax1dude.eaglercraft.v1_8.webview; + +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenPhishingWaring extends GuiScreen { + + public static boolean hasShownMessage = false; + + private static final ResourceLocation beaconGuiTexture = new ResourceLocation("textures/gui/container/beacon.png"); + + private GuiScreen cont; + private boolean mouseOverCheck; + private boolean hasCheckedBox; + + public GuiScreenPhishingWaring(GuiScreen cont) { + this.cont = cont; + } + + public void initGui() { + this.buttonList.clear(); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 134, I18n.format("webviewPhishingWaring.continue"))); + } + + public void drawScreen(int mx, int my, float pt) { + this.drawDefaultBackground(); + this.drawCenteredString(fontRendererObj, EnumChatFormatting.BOLD + I18n.format("webviewPhishingWaring.title"), this.width / 2, 70, 0xFF4444); + this.drawCenteredString(fontRendererObj, I18n.format("webviewPhishingWaring.text0"), this.width / 2, 90, 16777215); + this.drawCenteredString(fontRendererObj, I18n.format("webviewPhishingWaring.text1"), this.width / 2, 102, 16777215); + this.drawCenteredString(fontRendererObj, I18n.format("webviewPhishingWaring.text2"), this.width / 2, 114, 16777215); + + String dontShowAgain = I18n.format("webviewPhishingWaring.dontShowAgain"); + int w = fontRendererObj.getStringWidth(dontShowAgain) + 20; + int ww = (this.width - w) / 2; + this.drawString(fontRendererObj, dontShowAgain, ww + 20, 137, 0xCCCCCC); + + mouseOverCheck = ww < mx && ww + 17 > mx && 133 < my && 150 > my; + + if(mouseOverCheck) { + GlStateManager.color(0.7f, 0.7f, 1.0f, 1.0f); + }else { + GlStateManager.color(0.6f, 0.6f, 0.6f, 1.0f); + } + + mc.getTextureManager().bindTexture(beaconGuiTexture); + + GlStateManager.pushMatrix(); + GlStateManager.scale(0.75f, 0.75f, 0.75f); + drawTexturedModalRect(ww * 4 / 3, 133 * 4 / 3, 22, 219, 22, 22); + GlStateManager.popMatrix(); + + if(hasCheckedBox) { + GlStateManager.pushMatrix(); + GlStateManager.color(1.1f, 1.1f, 1.1f, 1.0f); + GlStateManager.translate(0.5f, 0.5f, 0.0f); + drawTexturedModalRect(ww, 133, 90, 222, 16, 16); + GlStateManager.popMatrix(); + } + + super.drawScreen(mx, my, pt); + } + + protected void actionPerformed(GuiButton par1GuiButton) { + if(par1GuiButton.id == 0) { + if(hasCheckedBox && !mc.gameSettings.hasHiddenPhishWarning) { + mc.gameSettings.hasHiddenPhishWarning = true; + mc.gameSettings.saveOptions(); + } + hasShownMessage = true; + mc.displayGuiScreen(cont); + } + } + + @Override + protected void mouseClicked(int mx, int my, int btn) { + if(btn == 0 && mouseOverCheck) { + hasCheckedBox = !hasCheckedBox; + mc.getSoundHandler().playSound(PositionedSoundRecord.create(new ResourceLocation("gui.button.press"), 1.0F)); + return; + } + super.mouseClicked(mx, my, btn); + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenRecieveServerInfo.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenRecieveServerInfo.java new file mode 100644 index 00000000..995a35c0 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenRecieveServerInfo.java @@ -0,0 +1,203 @@ +package net.lax1dude.eaglercraft.v1_8.webview; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import net.lax1dude.eaglercraft.v1_8.EaglerInputStream; +import net.lax1dude.eaglercraft.v1_8.EaglerZLIB; +import net.lax1dude.eaglercraft.v1_8.IOUtils; +import net.lax1dude.eaglercraft.v1_8.crypto.SHA1Digest; +import net.lax1dude.eaglercraft.v1_8.internal.WebViewOptions; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.minecraft.GuiScreenGenericErrorMessage; +import net.lax1dude.eaglercraft.v1_8.opengl.GlStateManager; +import net.lax1dude.eaglercraft.v1_8.opengl.WorldRenderer; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.client.CPacketRequestServerInfoV4EAG; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenRecieveServerInfo extends GuiScreen { + + private static final Logger logger = LogManager.getLogger("GuiScreenRecieveServerInfo"); + + protected final GuiScreen parent; + protected final byte[] expectHash; + protected int timer; + protected int timer2; + protected String statusString = "recieveServerInfo.checkingCache"; + + public GuiScreenRecieveServerInfo(GuiScreen parent, byte[] expectHash) { + this.parent = parent; + this.expectHash = expectHash; + } + + public void initGui() { + this.buttonList.clear(); + this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 106, I18n.format("gui.cancel"))); + } + + public void drawScreen(int par1, int par2, float par3) { + this.drawDefaultBackground(); + this.drawCenteredString(fontRendererObj, I18n.format("recieveServerInfo.title"), this.width / 2, 70, 11184810); + this.drawCenteredString(fontRendererObj, I18n.format(statusString), this.width / 2, 90, 16777215); + if(Arrays.equals(ServerInfoCache.chunkRecieveHash, expectHash) && ServerInfoCache.chunkFinalSize > 0) { + int progress = ServerInfoCache.chunkCurrentSize * 100 / ServerInfoCache.chunkFinalSize; + if(progress < 0) progress = 0; + if(progress > 100) progress = 100; + if(ServerInfoCache.hasLastChunk) { + progress = 100; + } + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer worldrenderer = tessellator.getWorldRenderer(); + byte b0 = 100; + byte b1 = 2; + int i1 = width / 2 - b0 / 2; + int j1 = 103; + GlStateManager.disableTexture2D(); + worldrenderer.begin(7, DefaultVertexFormats.POSITION_COLOR); + worldrenderer.pos((double) i1, (double) j1, 0.0D).color(128, 128, 128, 255).endVertex(); + worldrenderer.pos((double) i1, (double) (j1 + b1), 0.0D).color(128, 128, 128, 255).endVertex(); + worldrenderer.pos((double) (i1 + b0), (double) (j1 + b1), 0.0D).color(128, 128, 128, 255) + .endVertex(); + worldrenderer.pos((double) (i1 + b0), (double) j1, 0.0D).color(128, 128, 128, 255).endVertex(); + worldrenderer.pos((double) i1, (double) j1, 0.0D).color(128, 255, 128, 255).endVertex(); + worldrenderer.pos((double) i1, (double) (j1 + b1), 0.0D).color(128, 255, 128, 255).endVertex(); + worldrenderer.pos((double) (i1 + progress), (double) (j1 + b1), 0.0D).color(128, 255, 128, 255) + .endVertex(); + worldrenderer.pos((double) (i1 + progress), (double) j1, 0.0D).color(128, 255, 128, 255) + .endVertex(); + tessellator.draw(); + GlStateManager.enableTexture2D(); + } + super.drawScreen(par1, par2, par3); + } + + public void actionPerformed(GuiButton button) { + if(button.id == 0) { + mc.displayGuiScreen(parent); + } + } + + public void updateScreen() { + if(mc.thePlayer == null) { + mc.displayGuiScreen(parent); + return; + } + ++timer; + if(timer == 1) { + byte[] data = ServerInfoCache.loadFromCache(expectHash); + if(data != null) { + mc.displayGuiScreen(GuiScreenServerInfo.createForCurrentState(parent, data, WebViewOptions.getEmbedOriginUUID(expectHash))); + }else { + byte[] b = mc.thePlayer.sendQueue.cachedServerInfoData; + if(b != null) { + if(b.length == 0) { + mc.displayGuiScreen(new GuiScreenGenericErrorMessage("serverInfoFailure.title", "serverInfoFailure.desc", parent)); + }else { + ServerInfoCache.storeInCache(expectHash, b); + mc.displayGuiScreen(GuiScreenServerInfo.createForCurrentState(parent, b, WebViewOptions.getEmbedOriginUUID(expectHash))); + } + }else { + statusString = "recieveServerInfo.contactingServer"; + if(!mc.thePlayer.sendQueue.hasRequestedServerInfo) { + if(!ServerInfoCache.hasLastChunk || !Arrays.equals(ServerInfoCache.chunkRecieveHash, expectHash)) { + ServerInfoCache.clearDownload(); + mc.thePlayer.sendQueue.sendEaglerMessage(new CPacketRequestServerInfoV4EAG(expectHash)); + mc.thePlayer.sendQueue.hasRequestedServerInfo = true; + } + } + } + } + }else if(timer > 1) { + if(Arrays.equals(ServerInfoCache.chunkRecieveHash, expectHash)) { + if(ServerInfoCache.hasLastChunk) { + statusString = "recieveServerInfo.decompressing"; + ++timer2; + if(timer2 == 2) { + byte[] finalData = new byte[ServerInfoCache.chunkCurrentSize]; + int i = 0; + for(byte[] b : ServerInfoCache.chunkRecieveBuffer) { + System.arraycopy(b, 0, finalData, i, b.length); + i += b.length; + } + if(i != ServerInfoCache.chunkCurrentSize) { + logger.error("An unknown error occured!"); + mc.thePlayer.sendQueue.cachedServerInfoData = new byte[0]; + mc.displayGuiScreen(new GuiScreenGenericErrorMessage("serverInfoFailure.title", "serverInfoFailure.desc", parent)); + return; + } + ServerInfoCache.clearDownload(); + try { + EaglerInputStream bis = new EaglerInputStream(finalData); + int finalSize = (new DataInputStream(bis)).readInt(); + if(finalSize < 0) { + logger.error("The response data was corrupt, decompressed size is negative!"); + mc.thePlayer.sendQueue.cachedServerInfoData = new byte[0]; + mc.displayGuiScreen(new GuiScreenGenericErrorMessage("serverInfoFailure.title", "serverInfoFailure.desc", parent)); + return; + } + if(finalSize > ServerInfoCache.CACHE_MAX_SIZE * 2) { + logger.error("Failed to decompress/verify server info response! Size is massive, {} " + finalSize + " bytes reported!"); + logger.error("Aborting decompression. Rejoin the server to try again."); + mc.thePlayer.sendQueue.cachedServerInfoData = new byte[0]; + mc.displayGuiScreen(new GuiScreenGenericErrorMessage("serverInfoFailure.title", "serverInfoFailure.desc", parent)); + return; + } + byte[] decompressed = new byte[finalSize]; + try(InputStream is = EaglerZLIB.newGZIPInputStream(bis)) { + IOUtils.readFully(is, decompressed); + } + SHA1Digest digest = new SHA1Digest(); + digest.update(decompressed, 0, decompressed.length); + byte[] csum = new byte[20]; + digest.doFinal(csum, 0); + if(Arrays.equals(csum, expectHash)) { + ServerInfoCache.storeInCache(csum, decompressed); + mc.thePlayer.sendQueue.cachedServerInfoData = decompressed; + mc.displayGuiScreen(GuiScreenServerInfo.createForCurrentState(parent, decompressed, WebViewOptions.getEmbedOriginUUID(expectHash))); + }else { + logger.error("The data recieved from the server did not have the correct SHA1 checksum! Rejoin the server to try again."); + mc.thePlayer.sendQueue.cachedServerInfoData = new byte[0]; + mc.displayGuiScreen(new GuiScreenGenericErrorMessage("serverInfoFailure.title", "serverInfoFailure.desc", parent)); + } + }catch(IOException ex) { + logger.error("Failed to decompress/verify server info response! Rejoin the server to try again."); + logger.error(ex); + mc.thePlayer.sendQueue.cachedServerInfoData = new byte[0]; + mc.displayGuiScreen(new GuiScreenGenericErrorMessage("serverInfoFailure.title", "serverInfoFailure.desc", parent)); + } + } + }else { + statusString = "recieveServerInfo.recievingData"; + } + }else { + statusString = "recieveServerInfo.contactingServer"; + } + } + } + + protected boolean isPartOfPauseMenu() { + return true; + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenServerInfo.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenServerInfo.java new file mode 100644 index 00000000..b2a3e6bb --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenServerInfo.java @@ -0,0 +1,128 @@ +package net.lax1dude.eaglercraft.v1_8.webview; + +import java.net.URI; +import java.net.URISyntaxException; + +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; +import net.lax1dude.eaglercraft.v1_8.PauseMenuCustomizeState; +import net.lax1dude.eaglercraft.v1_8.internal.EnumWebViewContentMode; +import net.lax1dude.eaglercraft.v1_8.internal.WebViewOptions; +import net.lax1dude.eaglercraft.v1_8.log4j.LogManager; +import net.lax1dude.eaglercraft.v1_8.log4j.Logger; +import net.lax1dude.eaglercraft.v1_8.minecraft.GuiScreenGenericErrorMessage; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenServerInfo extends GuiScreen { + + private static final Logger logger = LogManager.getLogger("GuiScreenServerInfo"); + + private final GuiScreen parent; + private final WebViewOptions opts; + private boolean isShowing = false; + + public GuiScreenServerInfo(GuiScreen parent, WebViewOptions opts) { + this.parent = parent; + this.opts = opts; + } + + public static GuiScreen createForCurrentState(GuiScreen parent, String url) { + URI urlObj; + try { + urlObj = new URI(url); + }catch(URISyntaxException ex) { + logger.error("Refusing to iframe an invalid URL: {}", url); + logger.error(ex); + return new GuiScreenGenericErrorMessage("webviewInvalidURL.title", "webviewInvalidURL.desc", parent); + } + return createForCurrentState(parent, urlObj); + } + + public static GuiScreen createForCurrentState(GuiScreen parent, URI url) { + boolean support = WebViewOverlayController.supported(); + boolean fallbackSupport = WebViewOverlayController.fallbackSupported(); + if(!support && !fallbackSupport) { + return new GuiScreenGenericErrorMessage("webviewNotSupported.title", "webviewNotSupported.desc", parent); + } + WebViewOptions opts = new WebViewOptions(); + opts.contentMode = EnumWebViewContentMode.URL_BASED; + opts.url = url; + setupState(opts); + opts.permissionsOriginUUID = WebViewOptions.getURLOriginUUID(url); + return support ? new GuiScreenServerInfo(parent, opts) : new GuiScreenServerInfoDesktop(parent, opts); + } + + public static GuiScreen createForCurrentState(GuiScreen parent, byte[] blob, EaglercraftUUID permissionsOriginUUID) { + boolean support = WebViewOverlayController.supported(); + boolean fallbackSupport = WebViewOverlayController.fallbackSupported(); + if(!support && !fallbackSupport) { + return new GuiScreenGenericErrorMessage("webviewNotSupported.title", "webviewNotSupported.desc", parent); + } + WebViewOptions opts = new WebViewOptions(); + opts.contentMode = EnumWebViewContentMode.BLOB_BASED; + opts.blob = blob; + setupState(opts); + opts.permissionsOriginUUID = permissionsOriginUUID; + return support ? new GuiScreenServerInfo(parent, opts) : new GuiScreenServerInfoDesktop(parent, opts); + } + + public static void setupState(WebViewOptions opts) { + opts.scriptEnabled = (PauseMenuCustomizeState.serverInfoEmbedPerms & PauseMenuCustomizeState.SERVER_INFO_EMBED_PERMS_JAVASCRIPT) != 0; + opts.strictCSPEnable = (PauseMenuCustomizeState.serverInfoEmbedPerms & PauseMenuCustomizeState.SERVER_INFO_EMBED_PERMS_STRICT_CSP) != 0; + opts.serverMessageAPIEnabled = (PauseMenuCustomizeState.serverInfoEmbedPerms & PauseMenuCustomizeState.SERVER_INFO_EMBED_PERMS_MESSAGE_API) != 0; + opts.fallbackTitle = PauseMenuCustomizeState.serverInfoEmbedTitle; + } + + public void initGui() { + ScaledResolution res = mc.scaledResolution; + if(!isShowing) { + isShowing = true; + WebViewOverlayController.beginShowingSmart(opts, res, 30, 30, width - 60, height - 60); + }else { + WebViewOverlayController.resizeSmart(res, 30, 30, width - 60, height - 60); + } + buttonList.clear(); + buttonList.add(new GuiButton(0, (width - 200) / 2, height - 25, I18n.format("gui.done"))); + } + + public void onGuiClosed() { + if(isShowing) { + isShowing = false; + WebViewOverlayController.endShowing(); + } + } + + public void actionPerformed(GuiButton btn) { + if(btn.id == 0) { + mc.displayGuiScreen(parent); + } + } + + public void drawScreen(int mx, int my, float pt) { + drawDefaultBackground(); + drawCenteredString(fontRendererObj, PauseMenuCustomizeState.serverInfoEmbedTitle == null ? "Server Info" + : PauseMenuCustomizeState.serverInfoEmbedTitle, width / 2, 13, 0xFFFFFF); + super.drawScreen(mx, my, pt); + } + + protected boolean isPartOfPauseMenu() { + return true; + } +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenServerInfoDesktop.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenServerInfoDesktop.java new file mode 100644 index 00000000..19f0b2d3 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/GuiScreenServerInfoDesktop.java @@ -0,0 +1,92 @@ +package net.lax1dude.eaglercraft.v1_8.webview; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.PauseMenuCustomizeState; +import net.lax1dude.eaglercraft.v1_8.internal.WebViewOptions; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class GuiScreenServerInfoDesktop extends GuiScreen { + + private final GuiScreen parent; + private final WebViewOptions opts; + + private int timer = 0; + private boolean hasStarted = false; + + private GuiButton btnOpen; + + public GuiScreenServerInfoDesktop(GuiScreen parent, WebViewOptions opts) { + this.parent = parent; + this.opts = opts; + } + + public void initGui() { + buttonList.clear(); + buttonList.add(btnOpen = new GuiButton(0, (width - 200) / 2, height / 6 + 110, I18n.format("fallbackWebViewScreen.openButton"))); + btnOpen.enabled = false; + buttonList.add(new GuiButton(1, (width - 200) / 2, height / 6 + 140, I18n.format("fallbackWebViewScreen.exitButton"))); + } + + public void updateScreen() { + ++timer; + if(timer == 2) { + WebViewOverlayController.endFallbackServer(); + WebViewOverlayController.launchFallback(opts); + }else if(timer > 2) { + if(WebViewOverlayController.fallbackRunning()) { + btnOpen.enabled = WebViewOverlayController.getFallbackURL() != null; + hasStarted = true; + }else { + btnOpen.enabled = false; + } + } + } + + public void actionPerformed(GuiButton button) { + if(button.id == 0) { + String link = WebViewOverlayController.getFallbackURL(); + if(link != null) { + EagRuntime.openLink(link); + } + }else if(button.id == 1) { + mc.displayGuiScreen(parent); + } + } + + public void onGuiClosed() { + WebViewOverlayController.endFallbackServer(); + } + + public void drawScreen(int mx, int my, float pt) { + drawDefaultBackground(); + drawCenteredString(fontRendererObj, PauseMenuCustomizeState.serverInfoEmbedTitle, this.width / 2, 70, 16777215); + drawCenteredString(fontRendererObj, I18n.format("fallbackWebViewScreen.text0"), this.width / 2, 90, 11184810); + String link = WebViewOverlayController.fallbackRunning() ? WebViewOverlayController.getFallbackURL() + : I18n.format(hasStarted ? "fallbackWebViewScreen.exited" : "fallbackWebViewScreen.startingUp"); + drawCenteredString(fontRendererObj, link != null ? link : I18n.format("fallbackWebViewScreen.pleaseWait"), + width / 2, 110, 16777215); + super.drawScreen(mx, my, pt); + } + + protected boolean isPartOfPauseMenu() { + return true; + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/PermissionsCache.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/PermissionsCache.java new file mode 100644 index 00000000..bcd5e6ac --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/PermissionsCache.java @@ -0,0 +1,64 @@ +package net.lax1dude.eaglercraft.v1_8.webview; + +import java.util.HashMap; +import java.util.Map; + +import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class PermissionsCache { + + public static class Permission { + + public final int perm; + public final boolean choice; + + private Permission(int perm, boolean choice) { + this.perm = perm; + this.choice = choice; + } + + } + + private static final Map javaScriptAllowed = new HashMap<>(); + + public static Permission getJavaScriptAllowed(EaglercraftUUID uuid, int flags) { + synchronized(javaScriptAllowed) { + if(uuid == null) { + return null; + } + Permission p = javaScriptAllowed.get(uuid); + if(p == null) { + return null; + } + return (p.perm | flags) != p.perm ? null : p; + } + } + + public static void setJavaScriptAllowed(EaglercraftUUID uuid, int flags, boolean allowed) { + synchronized(javaScriptAllowed) { + if(uuid != null) javaScriptAllowed.put(uuid, new Permission(flags, allowed)); + } + } + + public static void clearJavaScriptAllowed(EaglercraftUUID uuid) { + synchronized(javaScriptAllowed) { + if(uuid != null) javaScriptAllowed.remove(uuid); + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/ServerInfoCache.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/ServerInfoCache.java new file mode 100644 index 00000000..2faa3562 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/ServerInfoCache.java @@ -0,0 +1,130 @@ +package net.lax1dude.eaglercraft.v1_8.webview; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import net.lax1dude.eaglercraft.v1_8.EagRuntime; +import net.lax1dude.eaglercraft.v1_8.HashKey; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketServerInfoDataChunkV4EAG; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ServerInfoCache { + + public static final int CACHE_MAX_SIZE = 0x200000; // 2 MB + + private static final Map cache = new HashMap<>(); + private static int cacheSize = 0; + + private static class CacheEntry { + + private final byte[] data; + private final HashKey hash; + private long lastHit; + + private CacheEntry(byte[] data, HashKey hash) { + this.data = data; + this.hash = hash; + this.lastHit = EagRuntime.steadyTimeMillis(); + } + + } + + protected static final List chunkRecieveBuffer = new LinkedList<>(); + protected static byte[] chunkRecieveHash = null; + protected static int chunkCurrentSize = 0; + protected static int chunkFinalSize = 0; + protected static boolean hasLastChunk = false; + + public static void handleChunk(SPacketServerInfoDataChunkV4EAG chunk) { + //System.out.println("p: " + chunk.seqId + " " + chunk.finalSize + " " + Base64.encodeBase64String(chunk.finalHash) + " " + chunk.lastChunk); + if (chunkRecieveHash == null || hasLastChunk || !Arrays.equals(chunk.finalHash, chunkRecieveHash) + || chunk.seqId != chunkRecieveBuffer.size()) { + chunkRecieveBuffer.clear(); + hasLastChunk = false; + chunkRecieveHash = null; + chunkCurrentSize = 0; + chunkFinalSize = 0; + if(chunk.seqId != 0) { + return; + } + chunkRecieveHash = chunk.finalHash; + } + chunkRecieveBuffer.add(chunk.data); + chunkCurrentSize += chunk.data.length; + chunkFinalSize = chunk.finalSize; + hasLastChunk = chunk.lastChunk; + } + + public static void clearDownload() { + chunkRecieveBuffer.clear(); + hasLastChunk = false; + chunkRecieveHash = null; + chunkCurrentSize = 0; + chunkFinalSize = 0; + } + + public static byte[] loadFromCache(byte[] hash) { + if(hash == null || hash.length != 20) { + return null; + } + CacheEntry etr = cache.get(new HashKey(hash)); + if(etr != null) { + etr.lastHit = EagRuntime.steadyTimeMillis(); + return etr.data; + }else { + return null; + } + } + + public static void storeInCache(byte[] hash, byte[] data) { + if(hash == null || hash.length != 20 || data == null) { + return; + } + HashKey hashObj = new HashKey(hash); + if(cache.containsKey(hashObj)) { + return; + } + shrink(data.length); + cache.put(hashObj, new CacheEntry(data, hashObj)); + cacheSize += data.length; + } + + private static void shrink(int toAdd) { + if(toAdd > CACHE_MAX_SIZE) { + cache.clear(); + cacheSize = 0; + return; + } + while(!cache.isEmpty() && cacheSize + toAdd > CACHE_MAX_SIZE) { + CacheEntry oldest = null; + for(CacheEntry e : cache.values()) { + if(oldest == null || e.lastHit < oldest.lastHit) { + oldest = e; + } + } + if(cache.remove(oldest.hash) != null) { + cacheSize -= oldest.data.length; + }else { + break; //wtf? + } + } + } + +} diff --git a/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/WebViewOverlayController.java b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/WebViewOverlayController.java new file mode 100644 index 00000000..3c6e4d75 --- /dev/null +++ b/sources/main/java/net/lax1dude/eaglercraft/v1_8/webview/WebViewOverlayController.java @@ -0,0 +1,92 @@ +package net.lax1dude.eaglercraft.v1_8.webview; + +import net.lax1dude.eaglercraft.v1_8.internal.PlatformWebView; +import net.lax1dude.eaglercraft.v1_8.internal.WebViewOptions; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.GameMessagePacket; +import net.lax1dude.eaglercraft.v1_8.socket.protocol.pkt.server.SPacketWebViewMessageV4EAG; +import net.minecraft.client.gui.ScaledResolution; + +/** + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class WebViewOverlayController { + + public static boolean supported() { + return PlatformWebView.supported(); + } + + public static boolean isShowing() { + return PlatformWebView.isShowing(); + } + + public static void beginShowing(WebViewOptions options, int x, int y, int w, int h) { + PlatformWebView.beginShowing(options, x, y, w, h); + } + + public static void resize(int x, int y, int w, int h) { + PlatformWebView.resize(x, y, w, h); + } + + public static void beginShowingSmart(WebViewOptions options, ScaledResolution res, int x, int y, int w, int h) { + int fac = res.getScaleFactor(); + PlatformWebView.beginShowing(options, x * fac, y * fac, w * fac, h * fac); + } + + public static void resizeSmart(ScaledResolution res, int x, int y, int w, int h) { + int fac = res.getScaleFactor(); + PlatformWebView.resize(x * fac, y * fac, w * fac, h * fac); + } + + public static void endShowing() { + PlatformWebView.endShowing(); + } + + public static boolean fallbackSupported() { + return PlatformWebView.fallbackSupported(); + } + + public static void launchFallback(WebViewOptions options) { + PlatformWebView.launchFallback(options); + } + + public static boolean fallbackRunning() { + return PlatformWebView.fallbackRunning(); + } + + public static String getFallbackURL() { + return PlatformWebView.getFallbackURL(); + } + + public static void endFallbackServer() { + PlatformWebView.endFallbackServer(); + } + + public static void handleMessagePacket(SPacketWebViewMessageV4EAG packet) { + PlatformWebView.handleMessageFromServer(packet); + } + + public static interface IPacketSendCallback { + boolean sendPacket(GameMessagePacket packet); + } + + public static void setPacketSendCallback(IPacketSendCallback callback) { + PlatformWebView.setPacketSendCallback(callback); + } + + public static void runTick() { + PlatformWebView.runTick(); + } + +} diff --git a/sources/main/java/org/json/JSONArray.java b/sources/main/java/org/json/JSONArray.java index 3be3e145..f86075e6 100644 --- a/sources/main/java/org/json/JSONArray.java +++ b/sources/main/java/org/json/JSONArray.java @@ -149,11 +149,40 @@ public class JSONArray implements Iterable { * A Collection. */ public JSONArray(Collection collection) { + this(collection, 0, new JSONParserConfiguration()); + } + + /** + * Construct a JSONArray from a Collection. + * + * @param collection + * A Collection. + * @param jsonParserConfiguration + * Configuration object for the JSON parser + */ + public JSONArray(Collection collection, JSONParserConfiguration jsonParserConfiguration) { + this(collection, 0, jsonParserConfiguration); + } + + /** + * Construct a JSONArray from a collection with recursion depth. + * + * @param collection + * A Collection. + * @param recursionDepth + * Variable for tracking the count of nested object creations. + * @param jsonParserConfiguration + * Configuration object for the JSON parser + */ + JSONArray(Collection collection, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) { + if (recursionDepth > jsonParserConfiguration.getMaxNestingDepth()) { + throw new JSONException("JSONArray has reached recursion depth limit of " + jsonParserConfiguration.getMaxNestingDepth()); + } if (collection == null) { this.myArrayList = new ArrayList(); } else { this.myArrayList = new ArrayList(collection.size()); - this.addAll(collection, true); + this.addAll(collection, true, recursionDepth, jsonParserConfiguration); } } @@ -205,7 +234,7 @@ public class JSONArray implements Iterable { throw new JSONException( "JSONArray initial value should be a string or collection or array."); } - this.addAll(array, true); + this.addAll(array, true, 0); } /** @@ -599,6 +628,38 @@ public class JSONArray implements Iterable { } } + /** + * Get the optional Boolean object associated with an index. It returns false + * if there is no value at that index, or if the value is not Boolean.TRUE + * or the String "true". + * + * @param index + * The index must be between 0 and length() - 1. + * @return The truth. + */ + public Boolean optBooleanObject(int index) { + return this.optBooleanObject(index, false); + } + + /** + * Get the optional Boolean object associated with an index. It returns the + * defaultValue if there is no value at that index or if it is not a Boolean + * or the String "true" or "false" (case insensitive). + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * A boolean default. + * @return The truth. + */ + public Boolean optBooleanObject(int index, Boolean defaultValue) { + try { + return this.getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + /** * Get the optional double value associated with an index. NaN is returned * if there is no value for the index, or if the value is not a number and @@ -635,6 +696,42 @@ public class JSONArray implements Iterable { return doubleValue; } + /** + * Get the optional Double object associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The object. + */ + public Double optDoubleObject(int index) { + return this.optDoubleObject(index, Double.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * subscript + * @param defaultValue + * The default object. + * @return The object. + */ + public Double optDoubleObject(int index, Double defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final Double doubleValue = val.doubleValue(); + // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + // return defaultValue; + // } + return doubleValue; + } + /** * Get the optional float value associated with an index. NaN is returned * if there is no value for the index, or if the value is not a number and @@ -671,6 +768,42 @@ public class JSONArray implements Iterable { return floatValue; } + /** + * Get the optional Float object associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The object. + */ + public Float optFloatObject(int index) { + return this.optFloatObject(index, Float.NaN); + } + + /** + * Get the optional Float object associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * subscript + * @param defaultValue + * The default object. + * @return The object. + */ + public Float optFloatObject(int index, Float defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + final Float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return floatValue; + // } + return floatValue; + } + /** * Get the optional int value associated with an index. Zero is returned if * there is no value for the index, or if the value is not a number and @@ -703,6 +836,38 @@ public class JSONArray implements Iterable { return val.intValue(); } + /** + * Get the optional Integer object associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The object. + */ + public Integer optIntegerObject(int index) { + return this.optIntegerObject(index, 0); + } + + /** + * Get the optional Integer object associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default object. + * @return The object. + */ + public Integer optIntegerObject(int index, Integer defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + /** * Get the enum value associated with a key. * @@ -788,30 +953,57 @@ public class JSONArray implements Iterable { } /** - * Get the optional JSONArray associated with an index. + * Get the optional JSONArray associated with an index. Null is returned if + * there is no value at that index or if the value is not a JSONArray. * * @param index - * subscript - * @return A JSONArray value, or null if the index has no value, or if the - * value is not a JSONArray. + * The index must be between 0 and length() - 1. + * @return A JSONArray value. */ public JSONArray optJSONArray(int index) { - Object o = this.opt(index); - return o instanceof JSONArray ? (JSONArray) o : null; + return this.optJSONArray(index, null); + } + + /** + * Get the optional JSONArray associated with an index. The defaultValue is returned if + * there is no value at that index or if the value is not a JSONArray. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default. + * @return A JSONArray value. + */ + public JSONArray optJSONArray(int index, JSONArray defaultValue) { + Object object = this.opt(index); + return object instanceof JSONArray ? (JSONArray) object : defaultValue; } /** * Get the optional JSONObject associated with an index. Null is returned if - * the key is not found, or null if the index has no value, or if the value - * is not a JSONObject. + * there is no value at that index or if the value is not a JSONObject. * * @param index * The index must be between 0 and length() - 1. * @return A JSONObject value. */ public JSONObject optJSONObject(int index) { - Object o = this.opt(index); - return o instanceof JSONObject ? (JSONObject) o : null; + return this.optJSONObject(index, null); + } + + /** + * Get the optional JSONObject associated with an index. The defaultValue is returned if + * there is no value at that index or if the value is not a JSONObject. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default. + * @return A JSONObject value. + */ + public JSONObject optJSONObject(int index, JSONObject defaultValue) { + Object object = this.opt(index); + return object instanceof JSONObject ? (JSONObject) object : defaultValue; } /** @@ -846,6 +1038,38 @@ public class JSONArray implements Iterable { return val.longValue(); } + /** + * Get the optional Long object associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The object. + */ + public Long optLongObject(int index) { + return this.optLongObject(index, 0L); + } + + /** + * Get the optional Long object associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default object. + * @return The object. + */ + public Long optLongObject(int index, Long defaultValue) { + final Number val = this.optNumber(index, null); + if (val == null) { + return defaultValue; + } + return val.longValue(); + } + /** * Get an optional {@link Number} value associated with a key, or null * if there is no such key or if the value is not a number. If the value is a string, @@ -1135,7 +1359,8 @@ public class JSONArray implements Iterable { * The subscript. * @param value * The Map value. - * @return this. + * @return + * reference to self * @throws JSONException * If the index is negative or if the value is an invalid * number. @@ -1143,7 +1368,27 @@ public class JSONArray implements Iterable { * If a key in the map is null */ public JSONArray put(int index, Map value) throws JSONException { - this.put(index, new JSONObject(value)); + this.put(index, new JSONObject(value, new JSONParserConfiguration())); + return this; + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject that + * is produced from a Map. + * + * @param index + * The subscript + * @param value + * The Map value. + * @param jsonParserConfiguration + * Configuration object for the JSON parser + * @return reference to self + * @throws JSONException + * If the index is negative or if the value is an invalid + * number. + */ + public JSONArray put(int index, Map value, JSONParserConfiguration jsonParserConfiguration) throws JSONException { + this.put(index, new JSONObject(value, jsonParserConfiguration)); return this; } @@ -1451,9 +1696,7 @@ public class JSONArray implements Iterable { @SuppressWarnings("resource") public String toString(int indentFactor) throws JSONException { StringWriter sw = new StringWriter(); - synchronized (sw.getBuffer()) { - return this.write(sw, indentFactor, 0).toString(); - } + return this.write(sw, indentFactor, 0).toString(); } /** @@ -1586,13 +1829,14 @@ public class JSONArray implements Iterable { * @param wrap * {@code true} to call {@link JSONObject#wrap(Object)} for each item, * {@code false} to add the items directly - * + * @param recursionDepth + * Variable for tracking the count of nested object creations. */ - private void addAll(Collection collection, boolean wrap) { + private void addAll(Collection collection, boolean wrap, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) { this.myArrayList.ensureCapacity(this.myArrayList.size() + collection.size()); if (wrap) { for (Object o: collection){ - this.put(JSONObject.wrap(o)); + this.put(JSONObject.wrap(o, recursionDepth + 1, jsonParserConfiguration)); } } else { for (Object o: collection){ @@ -1621,7 +1865,24 @@ public class JSONArray implements Iterable { } } } - + + /** + * Add an array's elements to the JSONArray. + * + * @param array + * Array. If the parameter passed is null, or not an array, + * JSONArray, Collection, or Iterable, an exception will be + * thrown. + * @param wrap + * {@code true} to call {@link JSONObject#wrap(Object)} for each item, + * {@code false} to add the items directly + * @throws JSONException + * If not an array or if an array value is non-finite number. + */ + private void addAll(Object array, boolean wrap) throws JSONException { + this.addAll(array, wrap, 0); + } + /** * Add an array's elements to the JSONArray. * @@ -1630,21 +1891,40 @@ public class JSONArray implements Iterable { * JSONArray, Collection, or Iterable, an exception will be * thrown. * @param wrap + * {@code true} to call {@link JSONObject#wrap(Object)} for each item, + * {@code false} to add the items directly + * @param recursionDepth + * Variable for tracking the count of nested object creations. + */ + private void addAll(Object array, boolean wrap, int recursionDepth) { + addAll(array, wrap, recursionDepth, new JSONParserConfiguration()); + } + /** + * Add an array's elements to the JSONArray. + *` + * @param array + * Array. If the parameter passed is null, or not an array, + * JSONArray, Collection, or Iterable, an exception will be + * thrown. + * @param wrap * {@code true} to call {@link JSONObject#wrap(Object)} for each item, * {@code false} to add the items directly - * + * @param recursionDepth + * Variable for tracking the count of nested object creations. + * @param jsonParserConfiguration + * Variable to pass parser custom configuration for json parsing. * @throws JSONException * If not an array or if an array value is non-finite number. * @throws NullPointerException * Thrown if the array parameter is null. */ - private void addAll(Object array, boolean wrap) throws JSONException { + private void addAll(Object array, boolean wrap, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) throws JSONException { if (array.getClass().isArray()) { int length = Array.getLength(array); this.myArrayList.ensureCapacity(this.myArrayList.size() + length); if (wrap) { for (int i = 0; i < length; i += 1) { - this.put(JSONObject.wrap(Array.get(array, i))); + this.put(JSONObject.wrap(Array.get(array, i), recursionDepth + 1, jsonParserConfiguration)); } } else { for (int i = 0; i < length; i += 1) { @@ -1657,7 +1937,7 @@ public class JSONArray implements Iterable { // JSONArray this.myArrayList.addAll(((JSONArray)array).myArrayList); } else if (array instanceof Collection) { - this.addAll((Collection)array, wrap); + this.addAll((Collection)array, wrap, recursionDepth); } else if (array instanceof Iterable) { this.addAll((Iterable)array, wrap); } else { diff --git a/sources/main/java/org/json/JSONObject.java b/sources/main/java/org/json/JSONObject.java index b133a7f3..6494f934 100644 --- a/sources/main/java/org/json/JSONObject.java +++ b/sources/main/java/org/json/JSONObject.java @@ -145,6 +145,11 @@ public class JSONObject { */ private final Map map; + /** + * Retrieves the type of the underlying Map in this class. + * + * @return The class object representing the type of the underlying Map. + */ public Class getMapType() { return map.getClass(); } @@ -208,22 +213,14 @@ public class JSONObject { throw x.syntaxError("A JSONObject text must begin with '{'"); } for (;;) { - char prev = x.getPrevious(); c = x.nextClean(); switch (c) { case 0: throw x.syntaxError("A JSONObject text must end with '}'"); case '}': return; - case '{': - case '[': - if(prev=='{') { - throw x.syntaxError("A JSON Object can not directly nest another JSON Object or JSON Array."); - } - // fall through default: - x.back(); - key = x.nextValue().toString(); + key = x.nextSimpleValue(c).toString(); } // The key is followed by ':'. @@ -237,12 +234,10 @@ public class JSONObject { if (key != null) { // Check if key exists - /* if (this.opt(key) != null) { // key already exists throw x.syntaxError("Duplicate key \"" + key + "\""); } - */ // Only add value if non-null Object value = x.nextValue(); if (value!=null) { @@ -258,6 +253,9 @@ public class JSONObject { if (x.nextClean() == '}') { return; } + if (x.end()) { + throw x.syntaxError("A JSONObject text must end with '}'"); + } x.back(); break; case '}': @@ -280,6 +278,30 @@ public class JSONObject { * If a key in the map is null */ public JSONObject(Map m) { + this(m, 0, new JSONParserConfiguration()); + } + + /** + * Construct a JSONObject from a Map with custom json parse configurations. + * + * @param m + * A map object that can be used to initialize the contents of + * the JSONObject. + * @param jsonParserConfiguration + * Variable to pass parser custom configuration for json parsing. + */ + public JSONObject(Map m, JSONParserConfiguration jsonParserConfiguration) { + this(m, 0, jsonParserConfiguration); + } + + /** + * Construct a JSONObject from a map with recursion depth. + * + */ + private JSONObject(Map m, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) { + if (recursionDepth > jsonParserConfiguration.getMaxNestingDepth()) { + throw new JSONException("JSONObject has reached recursion depth limit of " + jsonParserConfiguration.getMaxNestingDepth()); + } if (m == null) { this.map = new HashMap(); } else { @@ -290,7 +312,8 @@ public class JSONObject { } final Object value = e.getValue(); if (value != null) { - this.map.put(String.valueOf(e.getKey()), wrap(value)); + testValidity(value); + this.map.put(String.valueOf(e.getKey()), wrap(value, recursionDepth + 1, jsonParserConfiguration)); } } } @@ -348,11 +371,12 @@ public class JSONObject { * @JSONPropertyIgnore * public String getName() { return this.name; } * - *

* * @param bean * An object that has getter methods that should be used to make * a JSONObject. + * @throws JSONException + * If a getter returned a non-finite number. */ public JSONObject(Object bean) { this(); @@ -1133,6 +1157,45 @@ public class JSONObject { } } + /** + * Get an optional boolean object associated with a key. It returns false if there + * is no such key, or if the value is not Boolean.TRUE or the String "true". + * + * @param key + * A key string. + * @return The truth. + */ + public Boolean optBooleanObject(String key) { + return this.optBooleanObject(key, false); + } + + /** + * Get an optional boolean object associated with a key. It returns the + * defaultValue if there is no such key, or if it is not a Boolean or the + * String "true" or "false" (case insensitive). + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return The truth. + */ + public Boolean optBooleanObject(String key, Boolean defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Boolean){ + return ((Boolean) val).booleanValue(); + } + try { + // we'll use the get anyway because it does string conversion. + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + /** * Get an optional BigDecimal associated with a key, or the defaultValue if * there is no such key or if its value is not a number. If the value is a @@ -1292,15 +1355,43 @@ public class JSONObject { if (val == null) { return defaultValue; } - final double doubleValue = val.doubleValue(); - // if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { - // return defaultValue; - // } - return doubleValue; + return val.doubleValue(); } /** - * Get the optional double value associated with an index. NaN is returned + * Get an optional Double object associated with a key, or NaN if there is no such + * key or if its value is not a number. If the value is a string, an attempt + * will be made to evaluate it as a number. + * + * @param key + * A string which is the key. + * @return An object which is the value. + */ + public Double optDoubleObject(String key) { + return this.optDoubleObject(key, Double.NaN); + } + + /** + * Get an optional Double object associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Double optDoubleObject(String key, Double defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + return val.doubleValue(); + } + + /** + * Get the optional float value associated with an index. NaN is returned * if there is no value for the index, or if the value is not a number and * cannot be converted to a number. * @@ -1313,7 +1404,7 @@ public class JSONObject { } /** - * Get the optional double value associated with an index. The defaultValue + * Get the optional float value associated with an index. The defaultValue * is returned if there is no value for the index, or if the value is not a * number and cannot be converted to a number. * @@ -1335,6 +1426,42 @@ public class JSONObject { return floatValue; } + /** + * Get the optional Float object associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param key + * A key string. + * @return The object. + */ + public Float optFloatObject(String key) { + return this.optFloatObject(key, Float.NaN); + } + + /** + * Get the optional Float object associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param key + * A key string. + * @param defaultValue + * The default object. + * @return The object. + */ + public Float optFloatObject(String key, Float defaultValue) { + Number val = this.optNumber(key); + if (val == null) { + return defaultValue; + } + final Float floatValue = val.floatValue(); + // if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) { + // return defaultValue; + // } + return floatValue; + } + /** * Get an optional int value associated with a key, or zero if there is no * such key or if the value is not a number. If the value is a string, an @@ -1367,6 +1494,38 @@ public class JSONObject { return val.intValue(); } + /** + * Get an optional Integer object associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public Integer optIntegerObject(String key) { + return this.optIntegerObject(key, 0); + } + + /** + * Get an optional Integer object associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Integer optIntegerObject(String key, Integer defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + return val.intValue(); + } + /** * Get an optional JSONArray associated with a key. It returns null if there * is no such key, or if its value is not a JSONArray. @@ -1376,8 +1535,22 @@ public class JSONObject { * @return A JSONArray which is the value. */ public JSONArray optJSONArray(String key) { - Object o = this.opt(key); - return o instanceof JSONArray ? (JSONArray) o : null; + return this.optJSONArray(key, null); + } + + /** + * Get an optional JSONArray associated with a key, or the default if there + * is no such key, or if its value is not a JSONArray. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key, JSONArray defaultValue) { + Object object = this.opt(key); + return object instanceof JSONArray ? (JSONArray) object : defaultValue; } /** @@ -1438,6 +1611,39 @@ public class JSONObject { return val.longValue(); } + /** + * Get an optional Long object associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public Long optLongObject(String key) { + return this.optLongObject(key, 0L); + } + + /** + * Get an optional Long object associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Long optLongObject(String key, Long defaultValue) { + final Number val = this.optNumber(key, null); + if (val == null) { + return defaultValue; + } + + return val.longValue(); + } + /** * Get an optional {@link Number} value associated with a key, or null * if there is no such key or if the value is not a number. If the value is a string, @@ -1516,6 +1722,8 @@ public class JSONObject { * * @param bean * the bean + * @throws JSONException + * If a getter returned a non-finite number. */ private void populateMap(Object bean) { populateMap(bean, Collections.newSetFromMap(new IdentityHashMap())); @@ -1543,21 +1751,22 @@ public class JSONObject { final Object result = method.invoke(bean); if (result != null) { // check cyclic dependency and throw error if needed - // the wrap and populateMap combination method is + // the wrap and populateMap combination method is // itself DFS recursive if (objectsRecord.contains(result)) { throw recursivelyDefinedObjectException(key); } - + objectsRecord.add(result); + testValidity(result); this.map.put(key, wrap(result, objectsRecord)); objectsRecord.remove(result); // we don't use the result anywhere outside of wrap // if it's a resource we should be sure to close it - // after calling toString + // after calling toString if (result instanceof Closeable) { try { ((Closeable) result).close(); @@ -1657,6 +1866,10 @@ public class JSONObject { } } + //If the superclass is Object, no annotations will be found any more + if (c.getSuperclass().equals(Object.class)) + return null; + try { return getAnnotation( c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), @@ -1711,6 +1924,10 @@ public class JSONObject { } } + //If the superclass is Object, no annotations will be found any more + if (c.getSuperclass().equals(Object.class)) + return -1; + try { int d = getAnnotationDepth( c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()), @@ -2008,16 +2225,22 @@ public class JSONObject { @SuppressWarnings("resource") public static String quote(String string) { StringWriter sw = new StringWriter(); - synchronized (sw.getBuffer()) { - try { - return quote(string, sw).toString(); - } catch (IOException ignored) { - // will never happen - we are writing to a string writer - return ""; - } + try { + return quote(string, sw).toString(); + } catch (IOException ignored) { + // will never happen - we are writing to a string writer + return ""; } } + /** + * Quotes a string and appends the result to a given Writer. + * + * @param string The input string to be quoted. + * @param w The Writer to which the quoted string will be appended. + * @return The same Writer instance after appending the quoted string. + * @throws IOException If an I/O error occurs while writing to the Writer. + */ public static Writer quote(String string, Writer w) throws IOException { if (string == null || string.isEmpty()) { w.write("\"\""); @@ -2201,6 +2424,49 @@ public class JSONObject { || val.indexOf('E') > -1 || "-0".equals(val); } + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * + * @param string + * A String. can not be null. + * @return A simple JSON value. + * @throws NullPointerException + * Thrown if the string is null. + */ + // Changes to this method must be copied to the corresponding method in + // the XML class to keep full support for Android + public static Object stringToValue(String string) { + if ("".equals(string)) { + return string; + } + + // check JSON key words true/false/null + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + if ("null".equalsIgnoreCase(string)) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char initial = string.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + try { + return stringToNumber(string); + } catch (Exception ignore) { + } + } + return string; + } + /** * Converts a string to a number using the narrowest possible type. Possible * returns for this function are BigDecimal, Double, BigInteger, Long, and Integer. @@ -2271,49 +2537,6 @@ public class JSONObject { throw new NumberFormatException("val ["+val+"] is not a valid number."); } - /** - * Try to convert a string into a number, boolean, or null. If the string - * can't be converted, return the string. - * - * @param string - * A String. can not be null. - * @return A simple JSON value. - * @throws NullPointerException - * Thrown if the string is null. - */ - // Changes to this method must be copied to the corresponding method in - // the XML class to keep full support for Android - public static Object stringToValue(String string) { - if ("".equals(string)) { - return string; - } - - // check JSON key words true/false/null - if ("true".equalsIgnoreCase(string)) { - return Boolean.TRUE; - } - if ("false".equalsIgnoreCase(string)) { - return Boolean.FALSE; - } - if ("null".equalsIgnoreCase(string)) { - return JSONObject.NULL; - } - - /* - * If it might be a number, try converting it. If a number cannot be - * produced, then the value will just be a string. - */ - - char initial = string.charAt(0); - if ((initial >= '0' && initial <= '9') || initial == '-') { - try { - return stringToNumber(string); - } catch (Exception ignore) { - } - } - return string; - } - /** * Throw an exception if the object is a NaN or infinite number. * @@ -2401,9 +2624,7 @@ public class JSONObject { @SuppressWarnings("resource") public String toString(int indentFactor) throws JSONException { StringWriter w = new StringWriter(); - synchronized (w.getBuffer()) { - return this.write(w, indentFactor, 0).toString(); - } + return this.write(w, indentFactor, 0).toString(); } /** @@ -2454,7 +2675,31 @@ public class JSONObject { return wrap(object, null); } + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If it is + * a map, wrap it in a JSONObject. If it is a standard property (Double, + * String, et al) then it is already wrapped. Otherwise, if it comes from + * one of the java packages, turn it into a string. And if it doesn't, try + * to wrap it in a JSONObject. If the wrapping fails, then null is returned. + * + * @param object + * The object to wrap + * @param recursionDepth + * Variable for tracking the count of nested object creations. + * @param jsonParserConfiguration + * Variable to pass parser custom configuration for json parsing. + * @return The wrapped value + */ + static Object wrap(Object object, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) { + return wrap(object, null, recursionDepth, jsonParserConfiguration); + } + private static Object wrap(Object object, Set objectsRecord) { + return wrap(object, objectsRecord, 0, new JSONParserConfiguration()); + } + + private static Object wrap(Object object, Set objectsRecord, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) { try { if (NULL.equals(object)) { return NULL; @@ -2472,14 +2717,14 @@ public class JSONObject { if (object instanceof Collection) { Collection coll = (Collection) object; - return new JSONArray(coll); + return new JSONArray(coll, recursionDepth, jsonParserConfiguration); } if (object.getClass().isArray()) { return new JSONArray(object); } if (object instanceof Map) { Map map = (Map) object; - return new JSONObject(map); + return new JSONObject(map, recursionDepth, jsonParserConfiguration); } Package objectPackage = object.getClass().getPackage(); String objectPackageName = objectPackage != null ? objectPackage @@ -2715,4 +2960,24 @@ public class JSONObject { "JavaBean object contains recursively defined member variable of key " + quote(key) ); } + + /** + * For a prospective number, remove the leading zeros + * @param value prospective number + * @return number without leading zeros + */ + private static String removeLeadingZerosOfNumber(String value){ + if (value.equals("-")){return value;} + boolean negativeFirstChar = (value.charAt(0) == '-'); + int counter = negativeFirstChar ? 1:0; + while (counter < value.length()){ + if (value.charAt(counter) != '0'){ + if (negativeFirstChar) {return "-".concat(value.substring(counter));} + return value.substring(counter); + } + ++counter; + } + if (negativeFirstChar) {return "-0";} + return "0"; + } } diff --git a/sources/main/java/org/json/JSONParserConfiguration.java b/sources/main/java/org/json/JSONParserConfiguration.java new file mode 100644 index 00000000..f95e2442 --- /dev/null +++ b/sources/main/java/org/json/JSONParserConfiguration.java @@ -0,0 +1,26 @@ +package org.json; + +/** + * Configuration object for the JSON parser. The configuration is immutable. + */ +public class JSONParserConfiguration extends ParserConfiguration { + + /** + * Configuration with the default values. + */ + public JSONParserConfiguration() { + super(); + } + + @Override + protected JSONParserConfiguration clone() { + return new JSONParserConfiguration(); + } + + @SuppressWarnings("unchecked") + @Override + public JSONParserConfiguration withMaxNestingDepth(final int maxNestingDepth) { + return super.withMaxNestingDepth(maxNestingDepth); + } + +} diff --git a/sources/main/java/org/json/JSONPointer.java b/sources/main/java/org/json/JSONPointer.java index b3b39c81..3553b063 100644 --- a/sources/main/java/org/json/JSONPointer.java +++ b/sources/main/java/org/json/JSONPointer.java @@ -42,6 +42,12 @@ public class JSONPointer { */ public static class Builder { + /** + * Constructs a new Builder object. + */ + public Builder() { + } + // Segments for the eventual JSONPointer string private final List refTokens = new ArrayList(); @@ -163,6 +169,12 @@ public class JSONPointer { //} } + /** + * Constructs a new JSONPointer instance with the provided list of reference tokens. + * + * @param refTokens A list of strings representing the reference tokens for the JSON Pointer. + * Each token identifies a step in the path to the targeted value. + */ public JSONPointer(List refTokens) { this.refTokens = new ArrayList(refTokens); } diff --git a/sources/main/java/org/json/JSONPointerException.java b/sources/main/java/org/json/JSONPointerException.java index a0e128cd..dc5a25ad 100644 --- a/sources/main/java/org/json/JSONPointerException.java +++ b/sources/main/java/org/json/JSONPointerException.java @@ -14,10 +14,21 @@ Public Domain. public class JSONPointerException extends JSONException { private static final long serialVersionUID = 8872944667561856751L; + /** + * Constructs a new JSONPointerException with the specified error message. + * + * @param message The detail message describing the reason for the exception. + */ public JSONPointerException(String message) { super(message); } + /** + * Constructs a new JSONPointerException with the specified error message and cause. + * + * @param message The detail message describing the reason for the exception. + * @param cause The cause of the exception. + */ public JSONPointerException(String message, Throwable cause) { super(message, cause); } diff --git a/sources/main/java/org/json/JSONPropertyIgnore.java b/sources/main/java/org/json/JSONPropertyIgnore.java index 7c5fa538..d3a5bc5a 100644 --- a/sources/main/java/org/json/JSONPropertyIgnore.java +++ b/sources/main/java/org/json/JSONPropertyIgnore.java @@ -11,13 +11,13 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; -@Documented -@Retention(RUNTIME) -@Target({METHOD}) /** * Use this annotation on a getter method to override the Bean name * parser for Bean -> JSONObject mapping. If this annotation is * present at any level in the class hierarchy, then the method will * not be serialized from the bean into the JSONObject. */ +@Documented +@Retention(RUNTIME) +@Target({METHOD}) public @interface JSONPropertyIgnore { } diff --git a/sources/main/java/org/json/JSONPropertyName.java b/sources/main/java/org/json/JSONPropertyName.java index a66f4ad4..0e4123f3 100644 --- a/sources/main/java/org/json/JSONPropertyName.java +++ b/sources/main/java/org/json/JSONPropertyName.java @@ -11,16 +11,17 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; -@Documented -@Retention(RUNTIME) -@Target({METHOD}) /** * Use this annotation on a getter method to override the Bean name * parser for Bean -> JSONObject mapping. A value set to empty string "" * will have the Bean parser fall back to the default field name processing. */ +@Documented +@Retention(RUNTIME) +@Target({METHOD}) public @interface JSONPropertyName { /** + * The value of the JSON property. * @return The name of the property as to be used in the JSON Object. */ String value(); diff --git a/sources/main/java/org/json/JSONString.java b/sources/main/java/org/json/JSONString.java index cd8d1847..ee82720a 100644 --- a/sources/main/java/org/json/JSONString.java +++ b/sources/main/java/org/json/JSONString.java @@ -21,3 +21,4 @@ public interface JSONString { */ public String toJSONString(); } + diff --git a/sources/main/java/org/json/JSONTokener.java b/sources/main/java/org/json/JSONTokener.java index e9ffff66..0bc6dfb6 100644 --- a/sources/main/java/org/json/JSONTokener.java +++ b/sources/main/java/org/json/JSONTokener.java @@ -1,11 +1,7 @@ package org.json; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.StringReader; +import java.io.*; +import java.nio.charset.Charset; /* Public Domain. @@ -61,7 +57,7 @@ public class JSONTokener { * @param inputStream The source. */ public JSONTokener(InputStream inputStream) { - this(new InputStreamReader(inputStream)); + this(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); } @@ -125,7 +121,7 @@ public class JSONTokener { /** * Checks if the end of the input has been reached. - * + * * @return true if at the end of the file and we didn't step back */ public boolean end() { @@ -189,7 +185,7 @@ public class JSONTokener { this.previous = (char) c; return this.previous; } - + /** * Get the last character read from the input or '\0' if nothing has been read yet. * @return the last character read from the input. @@ -301,9 +297,9 @@ public class JSONTokener { c = this.next(); switch (c) { case 0: + case '\n': + case '\r': throw this.syntaxError("Unterminated string"); - case '\r': - break; case '\\': c = this.next(); switch (c) { @@ -406,12 +402,7 @@ public class JSONTokener { */ public Object nextValue() throws JSONException { char c = this.nextClean(); - String string; - switch (c) { - case '"': - case '\'': - return this.nextString(c); case '{': this.back(); try { @@ -427,6 +418,17 @@ public class JSONTokener { throw new JSONException("JSON Array or Object depth too large to process.", e); } } + return nextSimpleValue(c); + } + + Object nextSimpleValue(char c) { + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + } /* * Handle unquoted text. This could be the values true, false, or @@ -522,4 +524,15 @@ public class JSONTokener { return " at " + this.index + " [character " + this.character + " line " + this.line + "]"; } + + /** + * Closes the underlying reader, releasing any resources associated with it. + * + * @throws IOException If an I/O error occurs while closing the reader. + */ + public void close() throws IOException { + if(reader!=null){ + reader.close(); + } + } } diff --git a/sources/main/java/org/json/ParserConfiguration.java b/sources/main/java/org/json/ParserConfiguration.java new file mode 100644 index 00000000..5cdc10d8 --- /dev/null +++ b/sources/main/java/org/json/ParserConfiguration.java @@ -0,0 +1,126 @@ +package org.json; +/* +Public Domain. +*/ + +/** + * Configuration base object for parsers. The configuration is immutable. + */ +@SuppressWarnings({""}) +public class ParserConfiguration { + /** + * Used to indicate there's no defined limit to the maximum nesting depth when parsing a document. + */ + public static final int UNDEFINED_MAXIMUM_NESTING_DEPTH = -1; + + /** + * The default maximum nesting depth when parsing a document. + */ + public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512; + + /** + * Specifies if values should be kept as strings (true), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + */ + protected boolean keepStrings; + + /** + * The maximum nesting depth when parsing a document. + */ + protected int maxNestingDepth; + + /** + * Constructs a new ParserConfiguration with default settings. + */ + public ParserConfiguration() { + this.keepStrings = false; + this.maxNestingDepth = DEFAULT_MAXIMUM_NESTING_DEPTH; + } + + /** + * Constructs a new ParserConfiguration with the specified settings. + * + * @param keepStrings A boolean indicating whether to preserve strings during parsing. + * @param maxNestingDepth An integer representing the maximum allowed nesting depth. + */ + protected ParserConfiguration(final boolean keepStrings, final int maxNestingDepth) { + this.keepStrings = keepStrings; + this.maxNestingDepth = maxNestingDepth; + } + + /** + * Provides a new instance of the same configuration. + */ + @Override + protected ParserConfiguration clone() { + // future modifications to this method should always ensure a "deep" + // clone in the case of collections. i.e. if a Map is added as a configuration + // item, a new map instance should be created and if possible each value in the + // map should be cloned as well. If the values of the map are known to also + // be immutable, then a shallow clone of the map is acceptable. + return new ParserConfiguration( + this.keepStrings, + this.maxNestingDepth + ); + } + + /** + * When parsing the XML into JSONML, specifies if values should be kept as strings (true), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + * + * @return The keepStrings configuration value. + */ + public boolean isKeepStrings() { + return this.keepStrings; + } + + /** + * When parsing the XML into JSONML, specifies if values should be kept as strings (true), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + * + * @param newVal + * new value to use for the keepStrings configuration option. + * @param the type of the configuration object + * + * @return The existing configuration will not be modified. A new configuration is returned. + */ + @SuppressWarnings("unchecked") + public T withKeepStrings(final boolean newVal) { + T newConfig = (T)this.clone(); + newConfig.keepStrings = newVal; + return newConfig; + } + + /** + * The maximum nesting depth that the parser will descend before throwing an exception + * when parsing the XML into JSONML. + * @return the maximum nesting depth set for this configuration + */ + public int getMaxNestingDepth() { + return maxNestingDepth; + } + + /** + * Defines the maximum nesting depth that the parser will descend before throwing an exception + * when parsing the XML into JSONML. The default max nesting depth is 512, which means the parser + * will throw a JsonException if the maximum depth is reached. + * Using any negative value as a parameter is equivalent to setting no limit to the nesting depth, + * which means the parses will go as deep as the maximum call stack size allows. + * @param maxNestingDepth the maximum nesting depth allowed to the XML parser + * @param the type of the configuration object + * + * @return The existing configuration will not be modified. A new configuration is returned. + */ + @SuppressWarnings("unchecked") + public T withMaxNestingDepth(int maxNestingDepth) { + T newConfig = (T)this.clone(); + + if (maxNestingDepth > UNDEFINED_MAXIMUM_NESTING_DEPTH) { + newConfig.maxNestingDepth = maxNestingDepth; + } else { + newConfig.maxNestingDepth = UNDEFINED_MAXIMUM_NESTING_DEPTH; + } + + return newConfig; + } +} diff --git a/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket.java b/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket.java index fd793eaf..3f5f1ee2 100644 --- a/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket.java +++ b/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket.java @@ -27,8 +27,8 @@ import java.util.Map; */ public class RelayPacket { - private static final Map> definedPacketClasses = new HashMap(); - private static final Map,Integer> definedPacketIds = new HashMap(); + private static final Map> definedPacketClasses = new HashMap<>(); + private static final Map,Integer> definedPacketIds = new HashMap<>(); private static void register(int id, Class clazz) { definedPacketClasses.put(id, clazz); diff --git a/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket01ICEServers.java b/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket01ICEServers.java index a7bc7f63..84829891 100644 --- a/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket01ICEServers.java +++ b/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket01ICEServers.java @@ -55,7 +55,7 @@ public class RelayPacket01ICEServers extends RelayPacket { } public RelayPacket01ICEServers() { - this.servers = new ArrayList(); + this.servers = new ArrayList<>(); } public RelayPacket01ICEServers(Collection servers) { diff --git a/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket07LocalWorlds.java b/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket07LocalWorlds.java index 23ece948..b53f29b5 100644 --- a/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket07LocalWorlds.java +++ b/sources/protocol-relay/java/net/lax1dude/eaglercraft/v1_8/sp/relay/pkt/RelayPacket07LocalWorlds.java @@ -64,7 +64,7 @@ public class RelayPacket07LocalWorlds extends RelayPacket { public void read(DataInputStream input) throws IOException { int l = input.read(); if(worldsList == null) { - worldsList = new ArrayList(l); + worldsList = new ArrayList<>(l); }else { worldsList.clear(); } diff --git a/sources/resources/SignedClientTemplate.txt b/sources/resources/SignedClientTemplate.txt index 32731d94..247d3f69 100644 --- a/sources/resources/SignedClientTemplate.txt +++ b/sources/resources/SignedClientTemplate.txt @@ -1,8 +1,8 @@ - + - + EaglercraftX 1.8 @@ -12,7 +12,7 @@ - +

This file is from ${date}

-

Game will launch in 5...

-
+

Game will launch in 5...

+
+

diff --git a/sources/resources/assets/eagler/CREDITS.txt b/sources/resources/assets/eagler/CREDITS.txt index 0bade97b..5a34b1d6 100644 --- a/sources/resources/assets/eagler/CREDITS.txt +++ b/sources/resources/assets/eagler/CREDITS.txt @@ -3,24 +3,26 @@ ~~~~~~~~~~~~~~~~~~~~~~~ lax1dude: - + - Creator of Eaglercraft - Ported the Minecraft 1.8 src to TeaVM - Wrote HW accelerated OpenGL 1.3 emulator - Wrote the default shader pack - Made the integrated PBR resource pack + - Added touch and mobile device support - Wrote all desktop emulation code - Wrote EaglercraftXBungee - Wrote EaglercraftXVelocity - Wrote WebRTC relay server - Wrote voice chat server - Wrote the patch and build system - + ayunami2000: - + - Many bug fixes - WebRTC LAN worlds - WebRTC voice chat + - Worked on touch support - Made velocity plugin work - Added resource packs - Added screen recording @@ -410,7 +412,7 @@ Project Author: The Legion of the Bouncy Castle Project URL: https://www.bouncycastle.org/java.html - Used For: MD5, SHA-1, SHA-256 implementations + Used For: MD5, SHA-1, SHA-256, and AES implementations * Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) * @@ -668,23 +670,23 @@ Project Author: ymnk, JCraft Inc. Project URL: http://www.jcraft.com/jorbis/ - Used For: Audio in desktop runtime + Used For: Audio in desktop runtime and browsers that don't support OGG * JOrbis * Copyright (C) 2000 ymnk, JCraft,Inc. - * + * * Written by: 2000 ymnk * * Many thanks to * Monty and * The XIPHOPHORUS Company http://www.xiph.org/ . * JOrbis has been based on their awesome works, Vorbis codec. - * + * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * as published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. - + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the @@ -696,6 +698,44 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + Project Name: NanoHTTPD + Project Author: NanoHTTPD + Project URL: http://nanohttpd.org/ + + Used For: HTTP server in the desktop runtime + + * Copyright (c) 2012-2013 by Paul S. Hawke, + * 2001,2005-2013 by Jarno Elonen, + * 2010 by Konstantinos Togias All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of the NanoHttpd organization nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + Project Name: sqlite-jdbc Project Author: Taro L. Saito (xerial) Project URL: https://github.com/xerial/sqlite-jdbc diff --git a/sources/resources/assets/eagler/audioctx_test_ogg.dat b/sources/resources/assets/eagler/audioctx_test_ogg.dat new file mode 100644 index 0000000000000000000000000000000000000000..ff379a7ceae88520037fd572272fae9f8849c371 GIT binary patch literal 3980 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 Sb?xg8eI%auvENs%9)AbWp+dg^ literal 0 HcmV?d00001 diff --git a/sources/resources/assets/eagler/audioctx_test_wav16.dat b/sources/resources/assets/eagler/audioctx_test_wav16.dat new file mode 100644 index 0000000000000000000000000000000000000000..cfa8ce9c66935e16f64cb45f6a57f13a48441ab6 GIT binary patch literal 2106 zcmWMn2~br>7QNl?y}!W!{11wVA}|swBM2g>xKA9F1SG_TxDFB)K~T{sA-IlY(J`Z? z(P&20jF!7j6eSWFjG&;7yC^C_aae=_l|?@M<-LA==T+CO+qe4kJ*VpQGAkxJI>G=j zck-NR>C1BhtpGr@v1yVv*#JPn3TeqX$&d5^eQCZv#cZLoKB>}ozm^I8k8b=KRO!>R_p95e82 zEorZ9{r>gE*OFJ1rND{ebN-F-Zl9C-u8_UmR)d${nz>@uREIy|K#J7Ri0Q^a(UmVn~jNA=QiG)blcEfl3tbA7C#|tsG-7P%Dz?W zci1Pk@x?@9{)kaI*L*2F#p6>SOUP$1k=49+CBmCLGd&*^AOw5Pm&P*-iS$TtGv5pM zi5((d_?Y?yR=nPRue0}a%h9G|t!a%1TQYlxn3JrV?t=oJUFLn#udb`PDbAJEH^VfD73zGAH=Zw|X5-E-D_{8XZ@6(l zM0G}Mpn1CSE9F)5vUhcm+*_fPn|`(|Y4NZ{{9!vi)RyO;YfUhOTU(r)eH@+(I5Um& z&llGUUE(0WeL-j1oW`wgb;gro`kSd9b3N|39zc!da_79xUzA~;JDds~!mf0jRif1b zyQ8ZZ4m&@zU$wp8)up(t!;|p6SR;w}W4c!0(g(`ivYk;ZT zRHzyicYUY(W;+fXN4>~+YpCu3P8Ls@YV?`bTe_vHNt#LHnZ)-13 zTB_Tlx;KJN=OdrC>2<_@xc!1~UUnCH-u=o~AlmOkp0kS-^v)Ib(q4>o7NCbY!?Bf2 z!3223iF!jk;)oQUiR&;_JfU><+?Vyj79j+BRH_)%=e)+AB=#6{I~J>+(okgw%a@Mn z18ldjM}LjBaD#Nj{Vpky<00JGW%yQ_q06UCc%q9G21!AtCD;XN`bF|VVRXOgwk@o` z`%}80`-nczu+8QzomMW(b9fPq5b_i*+Z-LnHI4#)-g#YFDy^53oGtvZ?n8O6{eiFs z_PZ9+21Q}Vh539o|5}ZpLH5<;j@u#bZ&#R+K39t9TG5;PI2`Dp6ql-$__8SBwCHd9 zSsh`XtY+HhxE+L{iiem9rQ&lojz$UZXbj!v+A6-)hl)*DgcA${F@xL}Gk6bn=vE0o z$qPguDTs_f7cwCY!i7|p&Ps5%ZWbge+i|BZhz#vzO8S+uI zlLo#=PLTTYJ?bPLD%=3Vns_ETK=QGSpMhNd32qd}sxG-*xQ1iZcEu^K=J9l}EGoP7 z;Td_rGkG@IN58}_nocS(hU}*g6ffz3zPxvmI7mFsP2@JU z)6dvSFp4hprTp2WW+Qos6cNT(Y_c9}5fdwz5{k4U*R|tF#72u^D)W z{Kj?@C;3!e3vaLw_oU}gzzE@UmI{BRdNl@r5T3}Z;1#Ljh1^q^jKg?ee59@-jrb)L zV+iXbCPBQC1Bb;|t_re-T;v(5Q-~wuU5lh?Sf#1G21S^|h6^Py#aX4BMnv@kdJcjy z0P8WCZIZ%>8;=8(XG;>RS5Fa7GFfdGT=c4I3;hTELM|m`Lb37$+J&1~%7$<|B+!Q> zL9OTS;bQVo^y7`Pg8vf6i$AiN@=W0aV!$NcUpk2nC5bnPW{6U%$xt#F``{F^2y&6( zF?F*rggu2knyF@y6ugNtZ=|6xnC&OGUW<^d17(By zN@yjO{5to5mADSv@IE<$>HIu-p~jF)>_<`tPtb!D@Evf7pP`d*H!CE6rz4>stY)RK zOk9Zv*l{wF9@891fJDvV-Ab}>nB=QX_)>`H@54Ou8y}`{M*czB1wT3o?KY{BVp z6n0=bRO2kX2p!moN3{?2BpP5e{15*Oc~F4e8ulZI1Q~}xC3c{U)!2%$;0qQwqIu;8 zaoYZ&V1yIeU9~^}^w;`xG($7`Y83%91Z#+AI1uJTUnoHl`oLgl)7Zv9ucp_I1GM@f zR^cd(y%r_#fEF}@4(hai6f)By14n8rF;gQ2Edc~4%$qk)%Yj-f&Q4vCws;l9CFiD& Y8XGn$JZwaGc;K?kl-w1oQZiTn9~UI?Y5)KL literal 0 HcmV?d00001 diff --git a/sources/resources/assets/eagler/audioctx_test_wav32f.dat b/sources/resources/assets/eagler/audioctx_test_wav32f.dat new file mode 100644 index 0000000000000000000000000000000000000000..13ed4a860ec99188b9cfcc7ab055d9a222ac88a4 GIT binary patch literal 4144 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 literal 0 HcmV?d00001 diff --git a/sources/resources/assets/eagler/boot_menu/boot_menu_markup.html b/sources/resources/assets/eagler/boot_menu/boot_menu_markup.html new file mode 100644 index 00000000..ec3abaae --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/boot_menu_markup.html @@ -0,0 +1,88 @@ + +
+
+
+

EaglercraftX 1.8 Boot Manager

+
+
+
+ + + +
+
+ +
+
\ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/boot_menu_style.css b/sources/resources/assets/eagler/boot_menu/boot_menu_style.css new file mode 100644 index 00000000..1f2fb19e --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/boot_menu_style.css @@ -0,0 +1,328 @@ +@font-face { + font-family: "{% global `root_class_gen` %}_font0"; + src: url("data:font/woff;base64,{% embed base64 `web_cl_eagleiii_8x16.woff` %}") format("woff"); +} +.{% global `root_class_gen` %} { + font: 24px "{% global `root_class_gen` %}_font0"; + color: #CCCCCC; + background-color: #000000; + user-select: none; + width: 100%; + height: 100%; + overflow-y: auto; +} +.{% global `root_class_gen` %}::-moz-selection { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %}::selection { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %}::-webkit-scrollbar { + width: 12px; +} +.{% global `root_class_gen` %} ::-webkit-scrollbar { + width: 12px; +} +.{% global `root_class_gen` %}::-webkit-scrollbar-track, .{% global `root_class_gen` %} ::-webkit-scrollbar-track { + background-color: #000000; +} +.{% global `root_class_gen` %}::-webkit-scrollbar-thumb, .{% global `root_class_gen` %} ::-webkit-scrollbar-thumb { + background-color: #CCCCCC; +} +.{% global `root_class_gen` %}::-webkit-scrollbar-button, .{% global `root_class_gen` %} ::-webkit-scrollbar-button { + display: none; +} +.{% global `root_class_gen` %}::-webkit-scrollbar-corner, .{% global `root_class_gen` %} ::-webkit-scrollbar-corner { + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_inner { + width: 100%; + height: 100%; + min-height: 480px; + display: flex; + flex-flow: column; +} +.{% global `root_class_gen` %} p { + margin-block-start: 0px; + margin-block-end: 0px; + -webkit-margin-before:0px; + -webkit-margin-after:0px; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_header { + flex: 0 1 auto; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_header_title { + text-align: center; + padding: 32px 0px 0px 0px; + color: #CCCCCC; + white-space: pre-wrap; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_content { + flex: 1 1 auto; + position: relative; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_content_inner { + position: absolute; + top: 32px; + left: 32px; + bottom: 32px; + right: 32px; + border: 2px solid white; + z-index: 1; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup { + position: absolute; + top: 128px; + left: 64px; + bottom: 64px; + right: 64px; + z-index: 10; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_inner { + width: 50%; + min-width: min(calc(100% - 20px), 400px); + max-width: 800px; + max-height: calc(100% - 20px); + margin-left: auto; + margin-right: auto; + border: 2px solid white; + background-color: #000000; + padding: 10px; + overflow-y: auto; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_confirm_title { + text-align: center; + padding: 16px; + color: #CCCCCC; + white-space: pre-wrap; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_confirm_opts { + text-align: center; + padding: 16px; + color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_confirm_opt { + cursor: pointer; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_confirm_opt_selected { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_confirm_opt_disabled { + color: #888888; + cursor: default; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_selection_title { + text-align: center; + padding: 16px; + color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_selection { + width: calc(100% - 8px); + padding: 8px 4px; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_input_title { + text-align: center; + padding: 16px; + color: #CCCCCC; + white-space: pre-wrap; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_input_opts { + text-align: center; + padding: 16px; + color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_input_opt { + cursor: pointer; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_input_opt_selected { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_input_opt_disabled { + color: #888888; + cursor: default; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_input_val_container { + text-align: center; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_input_val { + min-width: 15em; + width: calc(90% - 50px); + font: 24px "{% global `root_class_gen` %}_font0"; + outline: none; + resize: none; + background-color: #000000; + color: #CCCCCC; + border: 2px solid white; + padding: 2px 4px; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_input_val:disabled { + color: #888888; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_input_val::-moz-selection { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_popup_input_val::selection { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_content_view_selection { + width: 100%; + height: 100%; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_content_selection { + width: calc(100% - 8px); + height: calc(100% - 16px); + padding: 8px 4px; + overflow-y: auto; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_content_item { + width: 100%; + overflow-y: auto; + cursor: pointer; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_content_item::before { + content: "\00a0"; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_content_item_selected { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_content_item_selected::before { + content: "*"; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_content_item_disabled { + color: #888888; + cursor: default; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_content_view_editor { + width: 100%; + height: 100%; + position: relative; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf { + position: absolute; + top: 0px; + left: 0px; + right: 0px; + height: calc(25% - 20px); + padding: 10px; + overflow-x: hidden; + overflow-y: auto; + color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_item_wide { + width: 100%; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_item { + display: inline-block; + padding: 20px; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_item input[type=text] { + min-width: 15em; + font: 24px "{% global `root_class_gen` %}_font0"; + outline: none; + resize: none; + background-color: #000000; + color: #CCCCCC; + border: 2px solid white; + padding: 2px 4px; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_item input[type=text]:disabled { + color: #888888; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_item input[type=checkbox] { + zoom: 2; + padding: 2px 4px; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_item select { + font: 24px "{% global `root_class_gen` %}_font0"; + outline: none; + resize: none; + background-color: #000000; + color: #CCCCCC; + border: 2px solid white; + padding: 2px 4px; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_item option:checked { + background-color: #CCCCCC; + color: #000000; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_item option:disabled { + color: #888888; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_item input::-moz-selection { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_item input::selection { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_val_profile_name { + width: calc(100% - 10em); + font: 24px "{% global `root_class_gen` %}_font0"; + outline: none; + resize: none; + background-color: #000000; + color: #CCCCCC; + border: 2px solid white; + padding: 2px 4px; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_val_profile_name::-moz-selection { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_val_profile_name::selection { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_val_profile_name:disabled { + color: #888888; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_conf_val_data_format { + padding: 2px 4px; + border: 2px solid white; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_opt_editor { + position: absolute; + bottom: 0px; + left: 0px; + right: 0px; + height: calc(75% - 22px); + width: calc(100% - 20px); + margin: 0px; + padding: 10px; + font: 24px "{% global `root_class_gen` %}_font0"; + border: none; + border-top: 2px solid white; + outline: none; + resize: none; + background-color: #000000; + color: #CCCCCC; + overflow: auto; + tab-size: 4; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_opt_editor:disabled { + color: #888888; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_opt_editor::-moz-selection { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_launch_opt_editor::selection { + color: #000000; + background-color: #CCCCCC; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_footer { + flex: 0 1 auto; +} +.{% global `root_class_gen` %} ._eaglercraftX_boot_menu_footer_text { + text-align: left; + padding: 0px 0px 32px 64px; + color: #CCCCCC; +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraftX_1_8.json b/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraftX_1_8.json new file mode 100644 index 00000000..5e28b463 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraftX_1_8.json @@ -0,0 +1,4 @@ +{ + "client_launch_type": "EAGLERX_V1", + "clear_cookies_before_launch": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraftX_1_8_signed.json b/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraftX_1_8_signed.json new file mode 100644 index 00000000..5cc54c53 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraftX_1_8_signed.json @@ -0,0 +1,4 @@ +{ + "client_launch_type": "EAGLERX_SIGNED_V1", + "clear_cookies_before_launch": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_1_5.json b/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_1_5.json new file mode 100644 index 00000000..fc1f47f6 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_1_5.json @@ -0,0 +1,4 @@ +{ + "client_launch_type": "EAGLER_1_5_V2", + "clear_cookies_before_launch": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_1_5_legacy.json b/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_1_5_legacy.json new file mode 100644 index 00000000..b3190b8e --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_1_5_legacy.json @@ -0,0 +1,5 @@ +{ + "client_launch_type": "EAGLER_1_5_V1", + "join_server": "", + "clear_cookies_before_launch": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_b1_3.json b/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_b1_3.json new file mode 100644 index 00000000..9abb4f24 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/conf_template_eaglercraft_b1_3.json @@ -0,0 +1,5 @@ +{ + "client_launch_type": "EAGLER_BETA_V1", + "join_server": "", + "clear_cookies_before_launch": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_a1_2_6.json b/sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_a1_2_6.json new file mode 100644 index 00000000..7d79d672 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_a1_2_6.json @@ -0,0 +1,4 @@ +{ + "client_launch_type": "PEYTON_V2", + "clear_cookies_before_launch": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_b1_7_3.json b/sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_b1_7_3.json new file mode 100644 index 00000000..7d79d672 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_b1_7_3.json @@ -0,0 +1,4 @@ +{ + "client_launch_type": "PEYTON_V2", + "clear_cookies_before_launch": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_indev.json b/sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_indev.json new file mode 100644 index 00000000..e9fdd8c9 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/conf_template_peytonplayz585_indev.json @@ -0,0 +1,4 @@ +{ + "client_launch_type": "PEYTON_V1", + "clear_cookies_before_launch": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/conf_template_standard_offline.json b/sources/resources/assets/eagler/boot_menu/conf_template_standard_offline.json new file mode 100644 index 00000000..5cf08d9c --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/conf_template_standard_offline.json @@ -0,0 +1,8 @@ +{ + "client_launch_type": "STANDARD_OFFLINE_V1", + "client_launch_opts_var": "eaglercraftXOpts", + "client_launch_opts_assetsURI_var": "assetsURI", + "client_launch_opts_container_var": "container", + "client_launch_main_func": "main", + "clear_cookies_before_launch": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/meta_opts_templates.json b/sources/resources/assets/eagler/boot_menu/meta_opts_templates.json new file mode 100644 index 00000000..46f98127 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/meta_opts_templates.json @@ -0,0 +1,192 @@ +{ + "defaults": { + "EAGLERX_SIGNED_V1": { + "conf": "conf_template_eaglercraftX_1_8_signed.json", + "opts": "opts_template_eaglercraftX_1_8.txt" + }, + "EAGLERX_V1": { + "conf": "conf_template_eaglercraftX_1_8.json", + "opts": "opts_template_eaglercraftX_1_8.txt" + }, + "EAGLER_BETA_V1": { + "conf": "conf_template_eaglercraft_b1_3.json", + "opts": null + }, + "EAGLER_1_5_V1": { + "conf": "conf_template_eaglercraft_1_5_legacy.json", + "opts": "opts_template_eaglercraft_1_5_legacy.txt" + }, + "EAGLER_1_5_V2": { + "conf": "conf_template_eaglercraft_1_5.json", + "opts": "opts_template_eaglercraft_1_5.txt" + }, + "PEYTON_V1": { + "conf": "conf_template_peytonplayz585_indev.json", + "opts": null + }, + "PEYTON_V2": { + "conf": "conf_template_peytonplayz585_a1_2_6.json", + "opts": "opts_template_peytonplayz585_a1_2_6.txt" + }, + "STANDARD_OFFLINE_V1": { + "conf": "conf_template_standard_offline.json", + "opts": null + } + }, + "templates": [ + { + "name": "EaglercraftX 1.8", + "conf": "conf_template_eaglercraftX_1_8.json", + "opts": "opts_template_eaglercraftX_1_8.txt", + "allow": [ + "EAGLER_STANDARD_OFFLINE" + ], + "parseTypes": [ + "EAGLERCRAFTX_1_8_OFFLINE" + ] + }, + { + "name": "EaglercraftX 1.8 Demo", + "conf": "conf_template_eaglercraftX_1_8.json", + "opts": "opts_template_eaglercraftX_1_8_demo.txt", + "allow": [ + "EAGLER_STANDARD_OFFLINE" + ], + "parseTypes": [ + "EAGLERCRAFTX_1_8_OFFLINE" + ] + }, + { + "name": "EaglercraftX 1.8 HTML5 Cursors", + "conf": "conf_template_eaglercraftX_1_8.json", + "opts": "opts_template_eaglercraftX_1_8_html5Cursors.txt", + "allow": [ + "EAGLER_STANDARD_OFFLINE" + ], + "parseTypes": [ + "EAGLERCRAFTX_1_8_OFFLINE" + ] + }, + { + "name": "EaglercraftX 1.8 Signed", + "conf": "conf_template_eaglercraftX_1_8_signed.json", + "opts": "opts_template_eaglercraftX_1_8.txt", + "allow": [ + "EAGLER_SIGNED_OFFLINE" + ], + "parseTypes": [ + "EAGLERCRAFTX_1_8_SIGNED" + ] + }, + { + "name": "EaglercraftX 1.8 Signed Demo", + "conf": "conf_template_eaglercraftX_1_8_signed.json", + "opts": "opts_template_eaglercraftX_1_8_demo.txt", + "allow": [ + "EAGLER_SIGNED_OFFLINE" + ], + "parseTypes": [ + "EAGLERCRAFTX_1_8_SIGNED" + ] + }, + { + "name": "EaglercraftX 1.8 Signed HTML5 Cursors", + "conf": "conf_template_eaglercraftX_1_8_signed.json", + "opts": "opts_template_eaglercraftX_1_8_html5Cursors.txt", + "allow": [ + "EAGLER_SIGNED_OFFLINE" + ], + "parseTypes": [ + "EAGLERCRAFTX_1_8_SIGNED" + ] + }, + { + "name": "Eaglercraft 1.5.2 (post-22w34a)", + "conf": "conf_template_eaglercraft_1_5.json", + "opts": "opts_template_eaglercraft_1_5.txt", + "allow": [ + "EAGLER_STANDARD_1_5_OFFLINE" + ], + "parseTypes": [ + "EAGLERCRAFT_1_5_NEW_OFFLINE" + ] + }, + { + "name": "Eaglercraft 1.5.2 Live Music (post-22w34a)", + "conf": "conf_template_eaglercraft_1_5.json", + "opts": "opts_template_eaglercraft_1_5_livestream.txt", + "allow": [ + "EAGLER_STANDARD_1_5_OFFLINE" + ], + "parseTypes": [ + "EAGLERCRAFT_1_5_NEW_OFFLINE" + ] + }, + { + "name": "Eaglercraft 1.5.2 (pre-22w34a)", + "conf": "conf_template_eaglercraft_1_5_legacy.json", + "opts": "opts_template_eaglercraft_1_5_legacy.txt", + "allow": [ + "EAGLER_STANDARD_OFFLINE" + ], + "parseTypes": [ + "EAGLERCRAFT_1_5_OLD_OFFLINE" + ] + }, + { + "name": "Eaglercraft Beta 1.3", + "conf": "conf_template_eaglercraft_b1_3.json", + "opts": null, + "allow": [ + "EAGLER_STANDARD_OFFLINE" + ], + "parseTypes": [ + "EAGLERCRAFT_BETA_B1_3_OFFLINE" + ] + }, + { + "name": "PeytonPlayz585 Beta 1.7.3", + "conf": "conf_template_peytonplayz585_b1_7_3.json", + "opts": "opts_template_peytonplayz585_b1_7_3.txt", + "allow": [ + "EAGLER_STANDARD_OFFLINE" + ], + "parseTypes": [ + "PEYTONPLAYZ585_ALPHA_BETA" + ] + }, + { + "name": "PeytonPlayz585 Alpha 1.2.6", + "conf": "conf_template_peytonplayz585_a1_2_6.json", + "opts": "opts_template_peytonplayz585_a1_2_6.txt", + "allow": [ + "EAGLER_STANDARD_OFFLINE" + ], + "parseTypes": [ + "PEYTONPLAYZ585_ALPHA_BETA" + ] + }, + { + "name": "PeytonPlayz585 Indev", + "conf": "conf_template_peytonplayz585_indev.json", + "opts": null, + "allow": [ + "EAGLER_STANDARD_OFFLINE" + ], + "parseTypes": [ + "PEYTONPLAYZ585_INDEV" + ] + }, + { + "name": "Standard Offline Download", + "conf": "conf_template_standard_offline.json", + "opts": null, + "allow": [ + "EAGLER_STANDARD_OFFLINE" + ], + "parseTypes": [ + "EXPORTED_STANDARD_OFFLINE" + ] + } + ] +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8.html b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8.html new file mode 100644 index 00000000..f0d3ef7c --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8.html @@ -0,0 +1,86 @@ + + + + + + + + + + +${client_name} + + + + + + + + + + + +
+
+

${client_name}

+

Game will launch in 5...

+
+

+
+
+ + diff --git a/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_fat_offline.html b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_fat_offline.html new file mode 100644 index 00000000..9813ea6f --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_fat_offline.html @@ -0,0 +1,85 @@ + + + + + + + + + + +EaglercraftX 1.8 + + + + + + + + + +${fat_offline_data} + + +
+
+

EaglercraftX 1.8 "Fat Offline"

+

Contains: ${num_clients} Client(s)

+

Game will launch in 5...

+
+

+
+
+ + diff --git a/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_fat_signed.html b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_fat_signed.html new file mode 100644 index 00000000..0666b518 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_fat_signed.html @@ -0,0 +1,268 @@ + + + + + + + + + + +EaglercraftX 1.8 + + + + + + + + + + + + +${fat_offline_data} + + +
+
+

EaglercraftX 1.8 "Fat Offline"

+

Contains: ${num_clients} Client(s)

+

Game will launch in 5...

+
+

+
+
+ + diff --git a/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_signed.html b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_signed.html new file mode 100644 index 00000000..ab884730 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraftX_1_8_signed.html @@ -0,0 +1,267 @@ + + + + + + + + + + +${client_name} + + + + + + + + + + + + + + +
+
+

${client_name_or_origin_date}

+

Game will launch in 5...

+
+

+
+
+ + diff --git a/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_1_5.html b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_1_5.html new file mode 100644 index 00000000..69e7333b --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_1_5.html @@ -0,0 +1,78 @@ + + + + + + + + +My Drive - Google Drive + + + + + + + + + + + + + + + + + + +
+
+

${client_name}

+

(Game will launch in 5)

+
+ + \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_1_5_legacy.html b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_1_5_legacy.html new file mode 100644 index 00000000..0b1ef3cb --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_1_5_legacy.html @@ -0,0 +1,59 @@ + + + + + + + + +${client_name} + + + + + + + + + + + + + + +
+
+

${client_name}

+

(Game will launch in 5)

+
+ + \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_b1_3.html b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_b1_3.html new file mode 100644 index 00000000..c060bb33 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/offline_template_eaglercraft_b1_3.html @@ -0,0 +1,59 @@ + + + + + + + + +${client_name} + + + + + + + + + + + + + + +
+
+

${client_name}

+

(Game will launch in 5)

+
+ + \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/offline_template_peytonplayz585_a_b.html b/sources/resources/assets/eagler/boot_menu/offline_template_peytonplayz585_a_b.html new file mode 100644 index 00000000..d1cb8d2b --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/offline_template_peytonplayz585_a_b.html @@ -0,0 +1,40 @@ + + + + + + + + +${client_name} + + + + + + + + + + + + + \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/offline_template_peytonplayz585_indev.html b/sources/resources/assets/eagler/boot_menu/offline_template_peytonplayz585_indev.html new file mode 100644 index 00000000..6abcb13e --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/offline_template_peytonplayz585_indev.html @@ -0,0 +1,38 @@ + + + + + + + + +${client_name} + + + + + + + + + + + + + \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/offline_template_standard_offline.html b/sources/resources/assets/eagler/boot_menu/offline_template_standard_offline.html new file mode 100644 index 00000000..cf505c67 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/offline_template_standard_offline.html @@ -0,0 +1,77 @@ + + + + + + + + + + +${client_name} + + + + + + + + + + + +
+
+

${client_name}

+

Game will launch in 5...

+
+
+
+ + diff --git a/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8.txt b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8.txt new file mode 100644 index 00000000..0b43e98e --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8.txt @@ -0,0 +1,66 @@ +{ + "joinServer": null, + "servers": [ + { + "addr": "ws://localhost:8081/", + "hideAddr": false, + "name": "Local test server" + } + ], + "relays": [ + { + "addr": "wss://relay.deev.is/", + "primary": "$random_relay_primary_0", + "comment": "lax1dude relay #1" + }, + { + "addr": "wss://relay.lax1dude.net/", + "primary": "$random_relay_primary_1", + "comment": "lax1dude relay #2" + }, + { + "addr": "wss://relay.shhnowisnottheti.me/", + "primary": "$random_relay_primary_2", + "comment": "ayunami relay #1" + } + ], + "openDebugConsoleOnLaunch": false, + "showBootMenuOnLaunch": false, + "bootMenuBlocksUnsignedClients": false, + "allowBootMenu": true, + "forceWebViewSupport": false, + "enableServerCookies": true, + "enableDownloadOfflineButton": true, + "resourcePacksDB": "resourcePacks", + "enableWebViewCSP": true, + "checkRelaysForUpdates": true, + "allowServerRedirects": true, + "allowUpdateSvc": true, + "html5CursorSupport": false, + "allowFNAWSkins": true, + "allowVoiceClient": true, + "worldsDB": "worlds", + "demoMode": false, + "localStorageNamespace": "_eaglercraftX", + "enableSignatureBadge": false, + "lang": "en_US", + "enableMinceraft": true, + "autoFixLegacyStyleAttr": true, + "allowUpdateDL": true, + "logInvalidCerts": false, + "checkShaderGLErrors": false, + "crashOnUncaughtExceptions": false, + "forceWebGL1": false, + "forceWebGL2": false, + "allowExperimentalWebGL1": true, + "useWebGLExt": true, + "useDelayOnSwap": false, + "useJOrbisAudioDecoder": false, + "useXHRFetch": false, + "useVisualViewport": true, + "deobfStackTraces": true, + "disableBlobURLs": false, + "eaglerNoDelay": false, + "ramdiskMode": false, + "singleThreadMode": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8_demo.txt b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8_demo.txt new file mode 100644 index 00000000..e801ad14 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8_demo.txt @@ -0,0 +1,66 @@ +{ + "joinServer": null, + "servers": [ + { + "addr": "ws://localhost:8081/", + "hideAddr": false, + "name": "Local test server" + } + ], + "relays": [ + { + "addr": "wss://relay.deev.is/", + "primary": "$random_relay_primary_0", + "comment": "lax1dude relay #1" + }, + { + "addr": "wss://relay.lax1dude.net/", + "primary": "$random_relay_primary_1", + "comment": "lax1dude relay #2" + }, + { + "addr": "wss://relay.shhnowisnottheti.me/", + "primary": "$random_relay_primary_2", + "comment": "ayunami relay #1" + } + ], + "openDebugConsoleOnLaunch": false, + "showBootMenuOnLaunch": false, + "bootMenuBlocksUnsignedClients": false, + "allowBootMenu": true, + "forceWebViewSupport": false, + "enableServerCookies": true, + "enableDownloadOfflineButton": true, + "resourcePacksDB": "resourcePacks", + "enableWebViewCSP": true, + "checkRelaysForUpdates": true, + "allowServerRedirects": true, + "allowUpdateSvc": true, + "html5CursorSupport": false, + "allowFNAWSkins": true, + "allowVoiceClient": true, + "worldsDB": "worlds", + "demoMode": true, + "localStorageNamespace": "_eaglercraftX", + "enableSignatureBadge": false, + "lang": "en_US", + "enableMinceraft": true, + "autoFixLegacyStyleAttr": true, + "allowUpdateDL": true, + "logInvalidCerts": false, + "checkShaderGLErrors": false, + "crashOnUncaughtExceptions": false, + "forceWebGL1": false, + "forceWebGL2": false, + "allowExperimentalWebGL1": true, + "useWebGLExt": true, + "useDelayOnSwap": false, + "useJOrbisAudioDecoder": false, + "useXHRFetch": false, + "useVisualViewport": true, + "deobfStackTraces": true, + "disableBlobURLs": false, + "eaglerNoDelay": false, + "ramdiskMode": false, + "singleThreadMode": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8_html5Cursors.txt b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8_html5Cursors.txt new file mode 100644 index 00000000..ce8c63fd --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraftX_1_8_html5Cursors.txt @@ -0,0 +1,66 @@ +{ + "joinServer": null, + "servers": [ + { + "addr": "ws://localhost:8081/", + "hideAddr": false, + "name": "Local test server" + } + ], + "relays": [ + { + "addr": "wss://relay.deev.is/", + "primary": "$random_relay_primary_0", + "comment": "lax1dude relay #1" + }, + { + "addr": "wss://relay.lax1dude.net/", + "primary": "$random_relay_primary_1", + "comment": "lax1dude relay #2" + }, + { + "addr": "wss://relay.shhnowisnottheti.me/", + "primary": "$random_relay_primary_2", + "comment": "ayunami relay #1" + } + ], + "openDebugConsoleOnLaunch": false, + "showBootMenuOnLaunch": false, + "bootMenuBlocksUnsignedClients": false, + "allowBootMenu": true, + "forceWebViewSupport": false, + "enableServerCookies": true, + "enableDownloadOfflineButton": true, + "resourcePacksDB": "resourcePacks", + "enableWebViewCSP": true, + "checkRelaysForUpdates": true, + "allowServerRedirects": true, + "allowUpdateSvc": true, + "html5CursorSupport": true, + "allowFNAWSkins": true, + "allowVoiceClient": true, + "worldsDB": "worlds", + "demoMode": false, + "localStorageNamespace": "_eaglercraftX", + "enableSignatureBadge": false, + "lang": "en_US", + "enableMinceraft": true, + "autoFixLegacyStyleAttr": true, + "allowUpdateDL": true, + "logInvalidCerts": false, + "checkShaderGLErrors": false, + "crashOnUncaughtExceptions": false, + "forceWebGL1": false, + "forceWebGL2": false, + "allowExperimentalWebGL1": true, + "useWebGLExt": true, + "useDelayOnSwap": false, + "useJOrbisAudioDecoder": false, + "useXHRFetch": false, + "useVisualViewport": true, + "deobfStackTraces": true, + "disableBlobURLs": false, + "eaglerNoDelay": false, + "ramdiskMode": false, + "singleThreadMode": false +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5.txt b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5.txt new file mode 100644 index 00000000..d439dfbb --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5.txt @@ -0,0 +1,52 @@ +{ + "joinServer": null, + "servers": [ + { + "serverName": "Local Test Server", + "serverAddress": "localhost:25565", + "hideAddress": false + } + ], + "relays": [ + { + "addr": "wss://relay.deev.is/", + "name": "lax1dude relay #1", + "primary": "$random_relay_primary_0" + }, + { + "addr": "wss://relay.lax1dude.net/", + "name": "lax1dude relay #2", + "primary": "$random_relay_primary_1" + }, + { + "addr": "wss://relay.shhnowisnottheti.me/", + "name": "ayunami relay #1", + "primary": "$random_relay_primary_2" + } + ], + "mainMenu": { + "splashes": [ + "Darviglet!", + "eaglerenophile!", + "You Eagler!", + "Yeeeeeee!", + "yeee", + "EEEEEEEEE!", + "You Darvig!", + "You Vigg!", + ":>", + "|>", + "You Yumpster!" + ], + "eaglerLogo": false, + "itemLink": null, + "itemLine0": null, + "itemLine1": null, + "itemLine2": null + }, + "worldsFolder": "MAIN", + "profanity": false, + "hideDownServers": false, + "serverListTitle": null, + "serverListLink": null +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5_legacy.txt b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5_legacy.txt new file mode 100644 index 00000000..d3ebf139 --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5_legacy.txt @@ -0,0 +1,32 @@ +[NBT]{ + servers: [ + { + name: "Local Test Server", + ip: "localhost:25565", + hideAddress: false + } + ], + mainMenu: { + itemLink: "", + itemLine0: "", + itemLine1: "", + itemLine2: "", + splashes: [ + "Darviglet!", + "eaglerenophile!", + "You Eagler!", + "Yeeeeeee!", + "yeee", + "EEEEEEEEE!", + "You Darvig!", + "You Vigg!", + ":>", + "|>", + "You Yumpster!" + ] + }, + profanity: false, + hide_down: false, + serverListTitle: "", + serverListLink: "" +}[/NBT] \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5_livestream.txt b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5_livestream.txt new file mode 100644 index 00000000..74cd7e2a --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/opts_template_eaglercraft_1_5_livestream.txt @@ -0,0 +1,63 @@ +{ + "joinServer": null, + "servers": [ + { + "serverName": "Local Test Server", + "serverAddress": "localhost:25565", + "hideAddress": false + } + ], + "relays": [ + { + "addr": "wss://relay.deev.is/", + "name": "lax1dude relay #1", + "primary": "$random_relay_primary_0" + }, + { + "addr": "wss://relay.lax1dude.net/", + "name": "lax1dude relay #2", + "primary": "$random_relay_primary_1" + }, + { + "addr": "wss://relay.shhnowisnottheti.me/", + "name": "ayunami relay #1", + "primary": "$random_relay_primary_2" + } + ], + "mainMenu": { + "splashes": [ + "Darviglet!", + "eaglerenophile!", + "You Eagler!", + "Yeeeeeee!", + "yeee", + "EEEEEEEEE!", + "You Darvig!", + "You Vigg!", + ":>", + "|>", + "You Yumpster!" + ], + "eaglerLogo": false, + "itemLink": null, + "itemLine0": null, + "itemLine1": null, + "itemLine2": null + }, + "worldsFolder": "MAIN", + "assetOverrides": { + "title/no-pano-blur.flag": "false", + "records/wait.mp3": "wait.mp3", + "records/mellohi.mp3": "https://stream.nightride.fm/chillsynth.m4a", + "records/far.mp3": "https://stream.nightride.fm/nightride.m4a", + "records/cat.mp3": "http://usa9.fastcast4u.com/proxy/jamz?mp=/1", + "records/ward.mp3": "http://fr4.1mix.co.uk:8000/192h", + "records/strad.mp3": "http://listen.011fm.com:8028/stream15", + "records/blocks.mp3": "https://www.ophanim.net:8444/s/9780", + "records/13.mp3": "https://s2.radio.co/s2b2b68744/listen" + }, + "profanity": false, + "hideDownServers": false, + "serverListTitle": null, + "serverListLink": null +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/opts_template_peytonplayz585_a1_2_6.txt b/sources/resources/assets/eagler/boot_menu/opts_template_peytonplayz585_a1_2_6.txt new file mode 100644 index 00000000..f26221fe --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/opts_template_peytonplayz585_a1_2_6.txt @@ -0,0 +1,6 @@ +{ + "dataBaseName": "_net_PeytonPlayz585_eaglercraft_Alpha_IndexedDBFilesystem_1_2_6", + "playerUsername": null, + "serverIP": null, + "joinServerOnLaunch": null +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/opts_template_peytonplayz585_b1_7_3.txt b/sources/resources/assets/eagler/boot_menu/opts_template_peytonplayz585_b1_7_3.txt new file mode 100644 index 00000000..abda3bdb --- /dev/null +++ b/sources/resources/assets/eagler/boot_menu/opts_template_peytonplayz585_b1_7_3.txt @@ -0,0 +1,6 @@ +{ + "dataBaseName": "_net_PeytonPlayz585_eaglercraft_beta_IndexedDBFilesystem_1_7_3", + "playerUsername": null, + "serverIP": null, + "joinServerOnLaunch": null +} \ No newline at end of file diff --git a/sources/resources/assets/eagler/boot_menu/web_cl_eagleiii_8x16.woff b/sources/resources/assets/eagler/boot_menu/web_cl_eagleiii_8x16.woff new file mode 100644 index 0000000000000000000000000000000000000000..f0e9430a3027b0d4be6613f4b9db057fb1d6aba3 GIT binary patch literal 9876 zcmZv?Wl&vB6E2FoCTMVX4{pKT-2()7Htz23t_dW#yKfve?y_-rceuP?-COtkI5YKB zPj^3SW_qgDRISzRt|Tc50SWQhs8b-&{*x9zAfP_`|D{Pvsw;f*JU(57{~^MeSwu=) zLh{r0^HazBpB)Td%eP>||_~cP*f9eST;o}1W+QQb;{B!s@Ki(%#spSTe1) z{F&3!GuqR$|Ji$bDjvLXc*5f%@92tXk} zZV~3!-*E1O!)!ciAw0~-r2R*tvE}7YB`cnlesFH`1<}@l+c68PjwyfS|&Vy@v>56^t>eng8rJCi+)~-2bzWRNPGZxk2AKL5}=yBH>Qf$F7s(nSea^eBQ|m!XG%zn>AKs!z=WnPiIwO5$$>Mq(emvqU5ho$P?zcEw+; z#v;Yo9Rs|!Mjdw3?PmxBCvBK|L<=j3- zf_n@cKJRG-G+i-5`A0@Rh;LnW-A{bChkiySdFv-?!nEPXng*jKd~?vQMvdj($8l|c zbmtGsB-YhIWip)f>;L$f6opHxXNQjVZXMfh|KfyRDyDry10#`LSG#IP4jC>NSlL)G00uJA2CJB@EO_D8ZwRZcJbly49E0JP#ox|jwe_64Gtu!>#wd}RsS9%b#opFoh@2GaWA$CA9m zunrDiXj3%yX0V!)g($=D8OKoMT*;FLMarA7t7VLSFZnuxV$L4DERG2xtQa>VzKJx3 ztY!R#8LiW&x~)CvtXY$!(O;hhvrv=fT0xUY$yJl6U~7)7Cu2^qDnpj~eP)C-nbEB# z+2B{Q7_;Z8IlA}qkl!`$4@h|4eq$uwK9wk*L8qPhR6N{Gdq$95z)O!vaJYwu7pg1K zzMxOPbiPVg@!Ko(AWR%85?3Y@S)nL80j`jrXn(&(Si2CL^er1 zsoPf3fLz)jlU|p}UfJV3qRm2;UVcV{@lFXyuZh}WH7_YEEiW;%(e$9|G-s`2-TVH0 zHQ&ErbF&Z_Diw!A#BZqYd>(t<4E8%%e_)^&-UQy@vZx!iIk%SF7b^*MdBr{}5eV(v z*3;y--(7phOWjj%bUxPX%;E1mQcb2es$V@v;;s-}s6Eq93UID~t9NBOuQ$jZkx!k4 zSwCN@&pXs32enT@dQ}+$V)J&dbVTktsl+O4BRxM;UA#Y78WW#5O!~+;Vi*qyq?L$* zzoOhOlul$5m&8m|`6az<9C2F%#K5Ezj6lH`i;5u=`yEp&MmGb*GrjhO5kV0i#x&bb zXrc6~EnL#Cz*>#*jc9~~5F}Qd#fx3f)Dv z6=U;Br619+7qA==16jXu^u(}xmh{bKAeWFd?{Hi-J{oplU(_>3FB7)+beYF5$u&(b zuYGSGwgaG^hn z17hXh&gm>&7Qqa^G()+?8PIKI)nT%i%Eq+PoeeY|a1dY$w8XR9DMbWT=&*8wC~j6> zZRi#vQtKBg%jRo@^!#ntiJK2jvAq|CYWeRwgL70YMl;>Ha4oD0+AM1P=^CvaQ0g^ugD(&SNA2~(}`B9=I!&=aQm^heK)~FPjJh_oZjsyIMDsnz*CUqIZt>` z_(AKn>|?qcDFOo4jTP%x@R8Kd0=zO~jJS~5tuaTYHPm>Z8%&X{dIVVFVA>bXVxOZt zQ+J=|=1tgmBmeAwck{ZwLImZqW>=yLr-T}nLPE(rh}8vFWcsn>cA8owb{vJnBszjZ z2(wmPydMJ-Q%+pm7PGcDGzgJdpSHzuD+1Yj5E2)MXS)xXSzr6GFC49IASOZK7c5k#M8sj`;l=*|7MgIa7ANSqME(aFD>&r+V zPfQmbHE(;1;7W^Yt`4=6q1ETBc77K1^y*H%ql2*$JG9l3vvT3C=gQ;Q^6r)AzGVNy zlLs*b4$rXX0zX1GAPD2xtQxmjq1m$BYy%Z|x_j)>!dI=yHfZeT=IYf*!=e&?_Pdr- zauVq0!M27;530+2I4ja~@m*)ZwOeF4FsQFskysr*04i68(SDGiK40IypY9&5)+ZMV zwY>fi-Z9z*y#4%3f+n^^|F|o_7d@p*U;gG(tB-K!b!1l%DNPY*_MLlsGR4;jYHqYyNaoixZ zjO&+x`LpJy4J4uzQW|v}e zT4Op}>w8Qw6+s9}wy%k@e%-(TTV`?&Za{_kCeqKS9AV6)AZyA$G$KrFw_z zH6CAesrvfT2Y7F$*Sp0~>JS_gJ4=i0T7ued&ZJt=nG5JFB*TMa-Q|L_WDgo+mtoS1 zoq!9Q(zs)r^60(~oid+P(!TOb#Et;|24uFVo7-}E5A~LIo{EpY-rA+~W6{d%JlcY; z?iIDxlY_o$?HMQk+|8rX!8-qEw-^ebGmQpSdw%tcjmp~~Pe>d8m{J$zqS7F9f8Kz$ zq)-=Mls0!-%iZBfdYChqR8oz^5!{q{XuH!9bVAvAZ0TEJQwcXms8@!qDZ^fe?Fb?) zf4?TR<=|?rbHG)hXdhyEYkxc??8cGl8zkfqP9tBuyJe3 z8@1CbR+Ji_A!CbJ|8-Jh|2&s~GgAV$^(K+*`BlL6Qt zJ!X058sjt)PEoSaYyH=qI4+wo3EjYyQHY&Aig7hyflP}MD1WM+@4 zyH%}JxQ|&9xwS;wC6cYgH?6s?-KGnVtx}N>Oocx=U`ek7>iXra&WV13l6`ROm}15A z%R3?j4fWeZ#DJsXp1O20mgn>V|gutZ1>GS$(c9P!yb zh4bY7Y-S!SBa4F4VLsj&`7EgE|MYjUZ(Nd?Mo1N0?*9(x(1F8x(dT>xbW z8W(z(3p2CH)T?d;ZFQR~9)mxBO6_mE_lFQCw(x-pRvocx3es@dD=c1y#sUTB*~4q= zfeKq=oCC=IM$*y*nXc!evi-6zph$M&9MbpDR!oSvRezdv*$=MJv)|bh6t4=337lSxqq*$d#euzC`zdH!5!1ex5vtc7 zdKJQFZOooGU~frgtdeZ|RRf>FSD&2n#<~2#oIkR-{AY>JwO+b+6s#qxENYDzU(L-o z2y#*TsI=bxDV<_wv*OzPTzm|6FyUHp0sUNqp_(&393l}_* zRk$#(>>2(LiyjocgLIBHPZ&YAObDX6tPs{!C~?{T}WYR>uW6- zn9$R6C_8}!#o1>datoJ9`7G=BR2FOBEH>7rA-TmB(#*^A<}y`sv&tWeAB zk38Aky?fEtG;b<1pm359BS2bdpNf+#hr8-RMcF;p*I%@1Z0iUO8akW5UUMAY46`J^ zcKIF+#q`uQvIg!&-kV8)!7nBTLDqokCW(8Uj}7Zru)GZJOUK1FEmBvfU#k#3FCuQJ zPEm~kre_l|V}2Q5Q4QOTz)2ASO4VWhiPrs09Cgk>DGxT-xl6Mbj~loNTr%) zc+-lkeqY?wa$3t*B~z6twi3}>mJx#;)3jLq*qTI_>gPAAHEpK11+q2;N!ljAi9Hck zTNHZs^~+^|EzSkp+1?L>b4D5k$n+A7A!85vinPj_4KGjl6Tg*zrEWCW1JX)~4lHmP z!jt+#>J4A1Sfd;(4aiIJ)!A^`Bp)n1VJ%M`Nar6!47=G?QLzo8XsBS3(gXXs4(x?r zua4uq3osjnJiLq;PM4SW;3Xzbg(w1PqEFS^2{p$E`E=Cv{bPP{Nu{2=wyY7xhq1I^ zR69l0{QiRb>D2@p7 z9G-lVcx%yq%;4%snu{(+&=yP4l-IFhXp=YU4r|OVh`uHJla|Jbw-2KARtjow!sBsQ zF-n^I0!GGD&ZLp`7#FGcoFC}IR~B{0l> za7_JJm5Q={&fvO&IlO6*D9A)Lil5Y-;HqbSY7~F#FrLs74<+OWc7Y@}rTmJgWj``i zewof!&kTmBia?vmcB^7yfE)#t7@2kF>32m^sYykZXzkx0jseY z3M#iln9hVL4nwYuUO>BNgsML{OwL$XD%O{(v0B#PKr_;Kr{S0-V)o_unq%c(OR7 zPYT5GRLph4d7P0OJ3R(Y^t?|bphhOolv!6#;SO!Hj*>flt*q_8&;1adsgI9e2OqK# zu=!#zh06;X)NFkVu&VgC5z&>0k4N}Qhvd5%i;gEz`MdRNNI)DZQ1|8y}V4jV7Y zuxnj>M9!#^n1f|1Pk{y~^yy^i@jiE9KvLi6-3cG=(>qg) z_p)qyf5=^@emGP#uB94trZ*`%cP8@t66Znf7yaU$YGAJ!C0(LgELDQbzMnDxh@}A! zSK&l#{N5nu7Ew`8CLiLg-id_T^IB9VO4#?TzG4FvB1#D5OEE2~BY zay_OedI*1STVPUcxmw{lXzQ1BXoF4Tyf;3HAO~c2ZsS2pVCPQ!4R_SHR8+ggfNSE{ zz`6rw+&_c};naIu)VpUJQ%taaw-Jv-)P0AU6&W>HGK{V&55}%?m1E%{OQ_P9ZZOGr z)%e~&i$mjG*eSU0dxL7NFhfL5Nh~^h_FK=={cfgCoPDpj4kO6Jaq5L+Lgr zsPz^Z63aFfSxuF6#`WLCGJT7OQj*LXl0&oTyS{1DteUBuAT%#((tNVEuU)j7I8l9e zbCJ^|u{nSf)yB08zoC6b^N0mez+6E?VQQGV0i~GKZ9O6t$j_<$D%9u z!jscbj7LXiX=mg}d@UMj(ZW|5NX*nQ^N|W|3e3pe#lwMSY3R-uUpFak<3=y8fh*l# z;K2w!L&s_z!^kyj%$M7)cPqCqbYj_3rq~m}H3qhPD0F-0uVsP@heIgqgw=Y!gdJr2 ztWJYFGX$b>iWN3u9?fiHYMT?LpG}HEsUXK zTg+Pu=ZCdk5_A9dAY?Bs039$Zh57WjiR%6MRoLjOq)R*K@TcGiS5(Nv4cau8!Q>wF z?vuTn+OdkH0#RHs5LK zOag-w3$Uk02J(ms;BZm3J@#zz%xE+`fvc-|(NFowyAaPZ zXz~}`)pmhR-bew5E)<<_OT4lMOFTm>Snz7AJ%sMn`Nhl~KZY1x1kmf-8Z#wjtj||> z0X5kc$wp{}R69BzF!sUylAUhJi&7Ql=ghE~VJA$6P-?#T%n1Mm4L%kYm*uu^4ty#p08 zOHuQE;@G(=ClgJa^7_))iZ;BoeC7SiN>N!5eKk8_?0}gBvCst!o7nIp#|83w?`>V= zJ#nhqN`0m2UE%u&K7O&&_uvTa2?`3Qx>~>UYk?aqs%+~I>H&EBwAA+4=U5sr3qSu2^d%fHCM~|`GGnDE z2+CN5ai$Dl;l%@kRCeVK|DBd}N@Gs(&Oh+Js=zS^9?pIDuU50q5?p}G0TrL%IRK3_ zctY0&8rZ3~VqzXp2W!U3?@$?0Bl^cxGQC`fc1Zn%5xiyO@9qTZnVn9U@SQ!&$FZHk z+;xF|LDunYGXmgRu>Y+XryXRUyt3G@*{2*0@jHve<*~UG?%cPwYs(9eobD{~Y_><2tW2|V{`Bfrztwrex&5;vPV3Q>3(b8S2_ z%B)ruSulxSGG}x6I!&o;KC>we2_qc3A%_T;5O^=&AHE9i_+qnSWaf4?Ri3^21aRvMT=GtlVSx#(+pU6OcAJA^^<%uxK6H(m7Z2 zD0!jCoAgDPyfDn2+8KnWLF_-`b$+i=>7XQwDDQ-;#0S*vl0|;oQovI zr*>N#H?aCs&DYZBTYz9z#N$(H&c{oq#|?9)sOK$qnGRW7yEQw7q16Z1htO|;!jqxE z3{%iWzpmH%08dItW_S88kvq@37Rk;q;&&4_YGTRUW8OO@yA+FZ`e0NSxqwCn5gE+) z2jQk}@XMb=?9=Brdb!D99<#T{5pOH7wFZfDwd?$~VWG!3{I{|TFTPE3d%@?+H(aoM z$8d&SBkZ00T_6wDG`&h`_$^IM>Kr#&pZ^m?smENJ<|Q{Du}vuS`l=Ttm?%AgWqi`c zt5b5I`A#~C4%0m?Qonx3g-?|)p49azuo}>npvV&Q_&wv zPi(;buHi+{;BN}uHRuCS^$Hy+}3-7W@;G{1C!d1r%iE)_35$UxQmDc4XUeoq(?HkjrAo!D%ip zp7GT`Tt!`tmY8V;91}U?ojWjANo*BU#q(i-%oW6_o(dR~h%cV!`2&D=UUMJpe+vwc z?@(t?gig3pMWbPb;ngZ@CU^gsLF-xCz?-tlIU2s|9!Lxmn?O`8BC7Mov=EOmomw=f z$fZ*O&c}gCwiksjUEm4V_)teHx1wCXH!hBLBrj)#L~AO#EG2naBBEUQcz*#;rHffK ziq&<6rd8Z~y(QKV^ESv;(s+Y`dV0PfcY2!a#q^&KDKyLL_5t3}#Fb7x&NoQS0UR4&i^JT)0Auk_{;-c>0sE*AaaUbi{IIOmwKK6X;} zQi(z{QmMt2^mESh1=h_;^a;q2Z81-MxTbfcalb4VQLb9rF%soWe4$}u*jG8U0;-k$ z`F&%Z)PIvCTU%XO*~YqjnKuD_7lfg|5d7^yW=p>Ov^RL#Ylg?qNu(}Z<2`%f%R9I4 z#m=v0f_w9;MQBjUD zZSe7_@)aoO%k{w!euvtB%`z&cd*C5a-fHbWGBSPlLX(R32@q;{B+c{tDf3t2ggd&^ zhpY9&>vJ2~Tg||WkPXys+;`Gfcb|Xw2clA+RtBOWjhe3}Nqx2MouvYJ>7;Yp)$rAx$=1(MzxWpW&nlf^ zt|rG@XA4{@_1iaL3{Fo?a0q?XtkwXw>#NUMjzplJVJX$@Pzk|tl$4WP0Xk2Xk8%o@ zsYGPU%N^ezN}`Z+wwrC(L`zs*kCcHWd_a zyII1Gaq1U#Pq{_^2zzj1Zz=|i)q zwdFm~W!|NBrqi`?&g&z*@Bt~JLy_r`Q<>atKdX#2D9kToC8H}6svd@>WnigdM`JH8 z*F<}a@y}W%8cJF^(awOXq{?8KTjd3TQ2^Q&r2`U~f`Z};OW*K8_NpcpU%!cS?o^H@}0bd@&q{&*|@Db2EPtok~_b2Zc%kqoe~(DPYOE*AYy&#mnI{t ziKE-Vbj5eJRaP72Me_P}i;3VgaS-4vI!4At_hflY0RJ_${~y+g8vZ}V4GgtRT&J|C zOMR4WabkOo_RlU`sW*WC(74nz2 zt|eZZX-DY`ObcneY_*hPnJ*Ys&nK^@m1s2gPnAAA=>Xn_hKz=s0)QAx?!Sv?Mc&+5 z>veDdHx~fVTFA`(TD*4Y)oSeE;GtsA!{F{}UvKqMf)C)wQuI_Mu671s)pPxGzgvho zESj?#y4hB5%rfO3llmZ-FP3md8;JM^o+9p4@ShhtgvlLw_>k$ zt44M6omN4_*>~_z_hQ;1*GT^C=$w2-u5$P6=p#c6X}CVFb8I$J-g~;D{W_zAGVYh| za3AtyF8SqBrn?QnBl6dWbIs2>1cPKPz^Wux=wvO7%I_%ei_TRj1lgkYwsteQb8N~V zP&f!bXjAmJzpT%ppx2i z7f}&h_CrY=-rE9P-@_qQMxb*=k8DMsbw#gk1<=d3N7A;>+_qQK7U*``qj=h9f7)w! z3iKoF5h3g|ChS!~`4Ji>;DW^(L55NsVagNtl_ye-Czb=Rmv)df4pH6&$-)WkzzGu9 z2{GLXcD5zTCZm_J>NCN&jbp!qcD_w~z5_|Tjfrx;srVYu>=Dj#1=-bG{fOysiSNF} z;1eQz#pDOvctqYP4q+FP*(eT0Dyz9-BL?;o>U%t*3GHK&iig0k9c%ZH=WQeAZSndI z$#wKFbPSR_X12G6d$-2Fcm}|GA{gDU#$VD3?Luf+kE>Y(1`}?xMhk@HU(&_j!1!&+ z_{6bzM3s7mQ4sFo4e|EMC|)s6UJ0;VlCxZ~S372DdB$?&?74zB6~Nne;4K62j<3+B zm{3vgm$C0wwSDwH<_!@BkcZ5C`MY}Wi#aTIq{ca(P&b*z)ncr5SJ!tN@&)CjGn_tDrY zM>5iQEc^dW6QxN08jt9RaS?oyoMxkc=TXcSnoPtpoys_iO;_Bq%AXR8Oj-i%XiN46 zB?pnnhwcQatR)8|XE2u(7jzlhB|(g>v4XnCQ`p)nS&TLzh25LFsXVjlG6^>9E@f_O zy5HYmtN+OQ*;+_s9Njf((A8yGaW-b$0GIB|yDVBd<8bwif2EoZJ$O|-_5UNN+Dl~` zxKAEFMy_l-(aNpc^76~Efzc8J#s)Vr4yh@kmXY}f~T-gH>~Q4pObE8X-BrssiV}zw421tG}rWRykbQ4 z&aGy=V$AFIzV(;6fjvtD`qD&kvtM(;?(<%&{4*1Qx-Liaj0s+}C*g9K6&=WXFvo^n z&q;rgUGCka>QCQow8i!mTbaz13GIH#Vv*hvJ-_jCV_lpU3)N9cOJRTg-~HcjmMN7D zfyZYuQYjt=*yU+W$`(5%;v&{y9k=8p6Fo_5V?IgH!+4(Vm&lU3=a1r9@CUA%dPc!# x1{nhKvvv#We|1a&5X5kpa5$tOnEn_H6bPs>2>Sp0|B+UbpdbE||7We#{{vfIG)@2j literal 0 HcmV?d00001 diff --git a/sources/resources/assets/eagler/glsl/accel_font.fsh b/sources/resources/assets/eagler/glsl/accel_font.fsh index 6d1c80ad..10bb5b1e 100644 --- a/sources/resources/assets/eagler/glsl/accel_font.fsh +++ b/sources/resources/assets/eagler/glsl/accel_font.fsh @@ -1,7 +1,7 @@ #line 2 /* - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -16,21 +16,17 @@ * */ -precision lowp int; -precision mediump float; -precision mediump sampler2D; +EAGLER_IN(vec2, v_texCoord2f) +EAGLER_IN(vec4, v_color4f) -in vec2 v_texCoord2f; -in vec4 v_color4f; - -layout(location = 0) out vec4 output4f; +EAGLER_FRAG_OUT() uniform sampler2D u_inputTexture; uniform vec4 u_colorBias4f; void main() { - output4f = texture(u_inputTexture, v_texCoord2f) * v_color4f + u_colorBias4f; - if(output4f.a < 0.004) { + EAGLER_FRAG_COLOR = EAGLER_TEXTURE_2D(u_inputTexture, v_texCoord2f) * v_color4f + u_colorBias4f; + if(EAGLER_FRAG_COLOR.a < 0.004) { discard; } } diff --git a/sources/resources/assets/eagler/glsl/accel_font.vsh b/sources/resources/assets/eagler/glsl/accel_font.vsh index 50a99ccd..8a274fad 100644 --- a/sources/resources/assets/eagler/glsl/accel_font.vsh +++ b/sources/resources/assets/eagler/glsl/accel_font.vsh @@ -1,7 +1,7 @@ #line 2 /* - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -16,18 +16,15 @@ * */ -precision lowp int; -precision highp float; -precision mediump sampler2D; +EAGLER_VSH_LAYOUT_BEGIN() +EAGLER_IN(0, vec3, a_position3f) +EAGLER_IN(1, vec2, c_position2i) +EAGLER_IN(2, vec2, c_coords2i) +EAGLER_IN(3, vec4, c_color4f) +EAGLER_VSH_LAYOUT_END() -layout(location = 0) in vec3 a_position3f; - -layout(location = 1) in vec2 c_position2i; -layout(location = 2) in vec2 c_coords2i; -layout(location = 3) in vec4 c_color4f; - -out vec2 v_texCoord2f; -out vec4 v_color4f; +EAGLER_OUT(vec2, v_texCoord2f) +EAGLER_OUT(vec4, v_color4f) uniform mat4 u_matrixTransform; uniform vec2 u_charSize2f; @@ -49,5 +46,5 @@ void main() { pos2d.x -= (a_position3f.y - 0.5) * italicBit; v_color4f.a *= 2.0; v_color4f *= u_color4f; - gl_Position = u_matrixTransform * vec4(pos2d, 0.0, 1.0); + EAGLER_VERT_POSITION = u_matrixTransform * vec4(pos2d, 0.0, 1.0); } diff --git a/sources/resources/assets/eagler/glsl/accel_particle.fsh b/sources/resources/assets/eagler/glsl/accel_particle.fsh index 256290c6..bbf4ed05 100644 --- a/sources/resources/assets/eagler/glsl/accel_particle.fsh +++ b/sources/resources/assets/eagler/glsl/accel_particle.fsh @@ -1,7 +1,7 @@ #line 2 /* - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -16,20 +16,16 @@ * */ -precision lowp int; -precision mediump float; -precision mediump sampler2D; +EAGLER_IN(vec2, v_texCoord2f) +EAGLER_IN(vec4, v_color4f) -in vec2 v_texCoord2f; -in vec4 v_color4f; - -layout(location = 0) out vec4 output4f; +EAGLER_FRAG_OUT() uniform sampler2D u_inputTexture; void main() { - output4f = texture(u_inputTexture, v_texCoord2f) * v_color4f; - if(output4f.a < 0.004) { + EAGLER_FRAG_COLOR = EAGLER_TEXTURE_2D(u_inputTexture, v_texCoord2f) * v_color4f; + if(EAGLER_FRAG_COLOR.a < 0.004) { discard; } } diff --git a/sources/resources/assets/eagler/glsl/accel_particle.vsh b/sources/resources/assets/eagler/glsl/accel_particle.vsh index 59becc7c..4983ac0c 100644 --- a/sources/resources/assets/eagler/glsl/accel_particle.vsh +++ b/sources/resources/assets/eagler/glsl/accel_particle.vsh @@ -1,7 +1,7 @@ #line 2 /* - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -16,20 +16,17 @@ * */ -precision lowp int; -precision highp float; -precision mediump sampler2D; +EAGLER_VSH_LAYOUT_BEGIN() +EAGLER_IN(0, vec2, a_position2f) +EAGLER_IN(1, vec3, p_position3f) +EAGLER_IN(2, vec2, p_texCoords2i) +EAGLER_IN(3, vec2, p_lightMap2f) +EAGLER_IN(4, vec2, p_particleSize_texCoordsSize_2i) +EAGLER_IN(5, vec4, p_color4f) +EAGLER_VSH_LAYOUT_END() -layout(location = 0) in vec2 a_position2f; - -layout(location = 1) in vec3 p_position3f; -layout(location = 2) in vec2 p_texCoords2i; -layout(location = 3) in vec2 p_lightMap2f; -layout(location = 4) in vec2 p_particleSize_texCoordsSize_2i; -layout(location = 5) in vec4 p_color4f; - -out vec2 v_texCoord2f; -out vec4 v_color4f; +EAGLER_OUT(vec2, v_texCoord2f) +EAGLER_OUT(vec4, v_color4f) uniform mat4 u_matrixTransform; uniform vec3 u_texCoordSize2f_particleSize1f; @@ -40,7 +37,7 @@ uniform vec4 u_color4f; uniform sampler2D u_lightmapTexture; void main() { - v_color4f = u_color4f * p_color4f.bgra * texture(u_lightmapTexture, p_lightMap2f); + v_color4f = u_color4f * p_color4f.bgra * EAGLER_TEXTURE_2D(u_lightmapTexture, p_lightMap2f); vec2 tex2f = a_position2f * 0.5 + 0.5; tex2f.y = 1.0 - tex2f.y; @@ -54,5 +51,5 @@ void main() { pos3f += u_transformParam_1_2_5_f * spos2f.xyy; pos3f.zx += u_transformParam_3_4_f * spos2f; - gl_Position = u_matrixTransform * vec4(pos3f, 1.0); + EAGLER_VERT_POSITION = u_matrixTransform * vec4(pos3f, 1.0); } diff --git a/sources/resources/assets/eagler/glsl/core.fsh b/sources/resources/assets/eagler/glsl/core.fsh index 5a01a799..26e50402 100644 --- a/sources/resources/assets/eagler/glsl/core.fsh +++ b/sources/resources/assets/eagler/glsl/core.fsh @@ -1,7 +1,7 @@ #line 2 /* - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -17,11 +17,11 @@ */ #if defined(COMPILE_ENABLE_TEX_GEN) || defined(COMPILE_ENABLE_FOG) -in vec4 v_position4f; +EAGLER_IN(vec4, v_position4f) #endif #ifdef COMPILE_TEXTURE_ATTRIB -in vec2 v_texture2f; +EAGLER_IN(vec2, v_texture2f) #endif uniform vec4 u_color4f; @@ -32,15 +32,15 @@ uniform vec4 u_colorBlendAdd4f; #endif #ifdef COMPILE_COLOR_ATTRIB -in vec4 v_color4f; +EAGLER_IN(vec4, v_color4f) #endif #ifdef COMPILE_NORMAL_ATTRIB -in vec3 v_normal3f; +EAGLER_IN(vec3, v_normal3f) #endif #ifdef COMPILE_LIGHTMAP_ATTRIB -in vec2 v_lightmap2f; +EAGLER_IN(vec2, v_lightmap2f) #endif #ifdef COMPILE_ENABLE_TEXTURE2D @@ -76,7 +76,7 @@ uniform vec4 u_fogColor4f; #endif #ifdef COMPILE_ENABLE_TEX_GEN -in vec3 v_objectPosition3f; +EAGLER_IN(vec3, v_objectPosition3f) uniform ivec4 u_texGenPlane4i; uniform vec4 u_texGenS4f; uniform vec4 u_texGenT4f; @@ -89,7 +89,7 @@ uniform mat4 u_textureMat4f01; uniform vec2 u_textureAnisotropicFix; #endif -layout(location = 0) out vec4 output4f; +EAGLER_FRAG_OUT() void main() { @@ -98,22 +98,29 @@ void main() { #else vec4 color = u_color4f; #endif - + #ifdef COMPILE_ENABLE_TEX_GEN + vec4 tmpVec4 = vec4(v_objectPosition3f, 1.0); vec4 texGenVector; - - vec4 texGenPosSrc[2]; - texGenPosSrc[0] = vec4(v_objectPosition3f, 1.0); - texGenPosSrc[1] = v_position4f; - - texGenVector.x = dot(texGenPosSrc[u_texGenPlane4i.x], u_texGenS4f); - texGenVector.y = dot(texGenPosSrc[u_texGenPlane4i.y], u_texGenT4f); - texGenVector.z = dot(texGenPosSrc[u_texGenPlane4i.z], u_texGenR4f); - texGenVector.w = dot(texGenPosSrc[u_texGenPlane4i.w], u_texGenQ4f); - + texGenVector.x = dot(u_texGenPlane4i.x == 1 ? v_position4f : tmpVec4, u_texGenS4f); + texGenVector.y = dot(u_texGenPlane4i.y == 1 ? v_position4f : tmpVec4, u_texGenT4f); + texGenVector.z = dot(u_texGenPlane4i.z == 1 ? v_position4f : tmpVec4, u_texGenR4f); + texGenVector.w = dot(u_texGenPlane4i.w == 1 ? v_position4f : tmpVec4, u_texGenQ4f); +#ifdef EAGLER_HAS_GLES_300 + texGenVector.xyz = mat4x3( + u_textureMat4f01[0].xyw, + u_textureMat4f01[1].xyw, + u_textureMat4f01[2].xyw, + u_textureMat4f01[3].xyw + ) * texGenVector; + texGenVector.xy /= texGenVector.z; +#else texGenVector = u_textureMat4f01 * texGenVector; - color *= texture(u_samplerTexture, texGenVector.xy / texGenVector.w); - + texGenVector.xy /= texGenVector.w; +#endif + + color *= EAGLER_TEXTURE_2D(u_samplerTexture, texGenVector.xy); + #ifdef COMPILE_ENABLE_ALPHA_TEST if(color.a < u_alphaTestRef1f) discard; #endif @@ -126,20 +133,20 @@ void main() { // d3d11 doesn't support GL_NEAREST upscaling with anisotropic // filtering enabled, so it needs this stupid fix to 'work' vec2 uv = floor(v_texture2f * u_textureAnisotropicFix) + 0.5; - color *= texture(u_samplerTexture, uv / u_textureAnisotropicFix); + color *= EAGLER_TEXTURE_2D(u_samplerTexture, uv / u_textureAnisotropicFix); #else - color *= texture(u_samplerTexture, v_texture2f); + color *= EAGLER_TEXTURE_2D(u_samplerTexture, v_texture2f); #endif #else - color *= texture(u_samplerTexture, u_textureCoords01); + color *= EAGLER_TEXTURE_2D(u_samplerTexture, u_textureCoords01); #endif #endif #ifdef COMPILE_ENABLE_LIGHTMAP #ifdef COMPILE_LIGHTMAP_ATTRIB - color *= texture(u_samplerLightmap, v_lightmap2f); + color *= EAGLER_TEXTURE_2D(u_samplerLightmap, v_lightmap2f); #else - color *= texture(u_samplerLightmap, u_textureCoords02); + color *= EAGLER_TEXTURE_2D(u_samplerLightmap, u_textureCoords02); #endif #endif @@ -161,9 +168,18 @@ void main() { #endif float diffuse = 0.0; vec4 light; +#ifdef EAGLER_HAS_GLES_300 for(int i = 0; i < u_lightsEnabled1i; ++i) { +#else + for(int i = 0; i < 4; ++i) { +#endif light = u_lightsDirections4fv[i]; diffuse += max(dot(light.xyz, normal), 0.0) * light.w; +#ifndef EAGLER_HAS_GLES_300 + if(i + 1 >= u_lightsEnabled1i) { + break; + } +#endif } color.rgb *= min(u_lightsAmbient3f + vec3(diffuse), 1.0); #endif @@ -179,5 +195,5 @@ void main() { color.rgb = mix(color.rgb, u_fogColor4f.rgb, clamp(f, 0.0, 1.0) * u_fogColor4f.a); #endif - output4f = color; + EAGLER_FRAG_COLOR = color; } diff --git a/sources/resources/assets/eagler/glsl/core.vsh b/sources/resources/assets/eagler/glsl/core.vsh index 77ce5041..f7ebd7e6 100644 --- a/sources/resources/assets/eagler/glsl/core.vsh +++ b/sources/resources/assets/eagler/glsl/core.vsh @@ -1,7 +1,7 @@ #line 2 /* - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -16,39 +16,39 @@ * */ -in vec3 a_position3f; +EAGLER_IN_AUTO(vec3, a_position3f) #if defined(COMPILE_ENABLE_TEX_GEN) || defined(COMPILE_ENABLE_FOG) #define _COMPILE_VARYING_POSITION #endif #ifdef _COMPILE_VARYING_POSITION -out vec4 v_position4f; +EAGLER_OUT(vec4, v_position4f) #endif #ifdef COMPILE_ENABLE_TEX_GEN -out vec3 v_objectPosition3f; +EAGLER_OUT(vec3, v_objectPosition3f) #endif #ifdef COMPILE_TEXTURE_ATTRIB -in vec2 a_texture2f; -out vec2 v_texture2f; +EAGLER_IN_AUTO(vec2, a_texture2f) +EAGLER_OUT(vec2, v_texture2f) uniform mat4 u_textureMat4f01; #endif #ifdef COMPILE_COLOR_ATTRIB -in vec4 a_color4f; -out vec4 v_color4f; +EAGLER_IN_AUTO(vec4, a_color4f) +EAGLER_OUT(vec4, v_color4f) #endif #ifdef COMPILE_NORMAL_ATTRIB -in vec4 a_normal4f; -out vec3 v_normal3f; +EAGLER_IN_AUTO(vec4, a_normal4f) +EAGLER_OUT(vec3, v_normal3f) #endif #ifdef COMPILE_LIGHTMAP_ATTRIB -in vec2 a_lightmap2f; -out vec2 v_lightmap2f; +EAGLER_IN_AUTO(vec2, a_lightmap2f) +EAGLER_OUT(vec2, v_lightmap2f) uniform mat4 u_textureMat4f02; #endif @@ -92,8 +92,8 @@ void main() { #endif #ifdef _COMPILE_VARYING_POSITION - gl_Position = u_projectionMat4f * v_position4f; + EAGLER_VERT_POSITION = u_projectionMat4f * v_position4f; #else - gl_Position = u_modelviewProjMat4f * vec4(a_position3f, 1.0); + EAGLER_VERT_POSITION = u_modelviewProjMat4f * vec4(a_position3f, 1.0); #endif } diff --git a/sources/resources/assets/eagler/glsl/deferred/forward_core.fsh b/sources/resources/assets/eagler/glsl/deferred/forward_core.fsh index 4e6a375c..7f429261 100644 --- a/sources/resources/assets/eagler/glsl/deferred/forward_core.fsh +++ b/sources/resources/assets/eagler/glsl/deferred/forward_core.fsh @@ -226,20 +226,18 @@ void main() { #ifdef COMPILE_ENABLE_TEXTURE2D vec2 texCoords2f; #ifdef COMPILE_ENABLE_TEX_GEN + vec4 tmpVec4 = vec4(v_objectPosition3f, 1.0); vec4 texGenVector; - vec4 texGenPosSrc[2]; - texGenPosSrc[0] = vec4(v_objectPosition3f, 1.0); - texGenPosSrc[1] = v_position4f; - texGenVector.x = dot(texGenPosSrc[u_texGenPlane4i.x], u_texGenS4f); - texGenVector.y = dot(texGenPosSrc[u_texGenPlane4i.y], u_texGenT4f); - texGenVector.z = dot(texGenPosSrc[u_texGenPlane4i.z], u_texGenR4f); - texGenVector.w = dot(texGenPosSrc[u_texGenPlane4i.w], u_texGenQ4f); - texGenVector = vec4(mat4x3( + texGenVector.x = dot(u_texGenPlane4i.x == 1 ? v_position4f : tmpVec4, u_texGenS4f); + texGenVector.y = dot(u_texGenPlane4i.y == 1 ? v_position4f : tmpVec4, u_texGenT4f); + texGenVector.z = dot(u_texGenPlane4i.z == 1 ? v_position4f : tmpVec4, u_texGenR4f); + texGenVector.w = dot(u_texGenPlane4i.w == 1 ? v_position4f : tmpVec4, u_texGenQ4f); + texGenVector.xyz = mat4x3( u_textureMat4f01[0].xyw, u_textureMat4f01[1].xyw, u_textureMat4f01[2].xyw, u_textureMat4f01[3].xyw - ) * texGenVector, 0.0); + ) * texGenVector; texCoords2f = texGenVector.xy / texGenVector.z; #else diff --git a/sources/resources/assets/eagler/glsl/deferred/reproject_control.fsh b/sources/resources/assets/eagler/glsl/deferred/reproject_control.fsh index a7a2c923..a6cd2f25 100644 --- a/sources/resources/assets/eagler/glsl/deferred/reproject_control.fsh +++ b/sources/resources/assets/eagler/glsl/deferred/reproject_control.fsh @@ -99,13 +99,13 @@ void main() { reprojectionReflectionOutput4f = vec4(0.0, 0.0, 0.0, 0.0); reprojectionHitVectorOutput4f = vec4(0.0, 0.0, 0.0, 0.0); #endif - float fragDepth = textureLod(u_gbufferDepthTexture, v_position2f, 0.0).r; + float fragDepth = textureLod(u_gbufferDepthTexture, v_position2f2, 0.0).r; if(fragDepth < 0.000001) { return; } - vec4 fragClipSpacePos4f = vec4(v_position2f, fragDepth, 1.0) * 2.0 - 1.0; + vec4 fragClipSpacePos4f = vec4(v_position2f2, fragDepth, 1.0) * 2.0 - 1.0; vec4 fragPos4f = u_inverseViewProjMatrix4f * fragClipSpacePos4f; fragPos4f.xyz /= fragPos4f.w; fragPos4f.w = 1.0; diff --git a/sources/resources/assets/eagler/glsl/deferred/shader_pack_info.json b/sources/resources/assets/eagler/glsl/deferred/shader_pack_info.json index af5f511d..9cf966a4 100644 --- a/sources/resources/assets/eagler/glsl/deferred/shader_pack_info.json +++ b/sources/resources/assets/eagler/glsl/deferred/shader_pack_info.json @@ -1,7 +1,7 @@ { "name": "§eHigh Performance PBR", "desc": "Pack made from scratch specifically for this client, designed to give what I call the best balance between quality and performance possible in a browser but obviously that's just my opinion", - "vers": "1.2.1", + "vers": "1.2.2", "author": "lax1dude", "api_vers": 1, "features": [ diff --git a/sources/resources/assets/eagler/glsl/deferred/ssao_generate.fsh b/sources/resources/assets/eagler/glsl/deferred/ssao_generate.fsh index f2b310a8..7f0e37e7 100644 --- a/sources/resources/assets/eagler/glsl/deferred/ssao_generate.fsh +++ b/sources/resources/assets/eagler/glsl/deferred/ssao_generate.fsh @@ -70,7 +70,7 @@ void main() { originalViewSpacePos.xyz /= originalViewSpacePos.w; originalViewSpacePos.w = 1.0; - vec4 noiseVec = textureLod(u_noiseConstantTexture, u_randomizerDataMatrix2f * (v_position2f + originalViewSpacePos.xy + normal3f.xz), 0.0); + vec4 noiseVec = textureLod(u_noiseConstantTexture, 13.3725 / fract((u_randomizerDataMatrix2f * (v_position2f * 0.42695346 + originalViewSpacePos.xy * 1.373769945645 + normal3f.xz * 42.69456453)) * 1.123234234), 0.0); noiseVec.xyz *= 2.0; noiseVec.xyz -= 1.0; diff --git a/sources/resources/assets/eagler/glsl/dynamiclights/core_dynamiclights.fsh b/sources/resources/assets/eagler/glsl/dynamiclights/core_dynamiclights.fsh index aa3bc517..c4d30d62 100644 --- a/sources/resources/assets/eagler/glsl/dynamiclights/core_dynamiclights.fsh +++ b/sources/resources/assets/eagler/glsl/dynamiclights/core_dynamiclights.fsh @@ -107,22 +107,23 @@ void main() { #else vec4 color = u_color4f; #endif - + #ifdef COMPILE_ENABLE_TEX_GEN + vec4 tmpVec4 = vec4(v_objectPosition3f, 1.0); vec4 texGenVector; - - vec4 texGenPosSrc[2]; - texGenPosSrc[0] = vec4(v_objectPosition3f, 1.0); - texGenPosSrc[1] = v_position4f; - - texGenVector.x = dot(texGenPosSrc[u_texGenPlane4i.x], u_texGenS4f); - texGenVector.y = dot(texGenPosSrc[u_texGenPlane4i.y], u_texGenT4f); - texGenVector.z = dot(texGenPosSrc[u_texGenPlane4i.z], u_texGenR4f); - texGenVector.w = dot(texGenPosSrc[u_texGenPlane4i.w], u_texGenQ4f); - - texGenVector = u_textureMat4f01 * texGenVector; - color *= texture(u_samplerTexture, texGenVector.xy / texGenVector.w); - + texGenVector.x = dot(u_texGenPlane4i.x == 1 ? v_position4f : tmpVec4, u_texGenS4f); + texGenVector.y = dot(u_texGenPlane4i.y == 1 ? v_position4f : tmpVec4, u_texGenT4f); + texGenVector.z = dot(u_texGenPlane4i.z == 1 ? v_position4f : tmpVec4, u_texGenR4f); + texGenVector.w = dot(u_texGenPlane4i.w == 1 ? v_position4f : tmpVec4, u_texGenQ4f); + texGenVector.xyz = mat4x3( + u_textureMat4f01[0].xyw, + u_textureMat4f01[1].xyw, + u_textureMat4f01[2].xyw, + u_textureMat4f01[3].xyw + ) * texGenVector; + + color *= texture(u_samplerTexture, texGenVector.xy / texGenVector.z); + #ifdef COMPILE_ENABLE_ALPHA_TEST if(color.a < u_alphaTestRef1f) discard; #endif diff --git a/sources/resources/assets/eagler/glsl/gles2_compat.glsl b/sources/resources/assets/eagler/glsl/gles2_compat.glsl new file mode 100644 index 00000000..f0023368 --- /dev/null +++ b/sources/resources/assets/eagler/glsl/gles2_compat.glsl @@ -0,0 +1,98 @@ +#line 2 6969 + +/* + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#ifdef EAGLER_HAS_GLES_300 + +// For GLES 3.00+ (WebGL 2.0) +#ifdef EAGLER_IS_VERTEX_SHADER + +// Vertex Shaders: +#define EAGLER_VSH_LAYOUT_BEGIN() +#define EAGLER_VSH_LAYOUT_END() +#define EAGLER_IN(_loc, _type, _name) layout(location = _loc) in _type _name; +#define EAGLER_IN_AUTO(_type, _name) in _type _name; +#define EAGLER_OUT(_type, _name) out _type _name; +#define EAGLER_VERT_POSITION gl_Position + +#else +#ifdef EAGLER_IS_FRAGMENT_SHADER + +// Fragment Shaders: +#define EAGLER_IN(_type, _name) in _type _name; +#define EAGLER_FRAG_COLOR eagler_FragColor +#define EAGLER_FRAG_DEPTH gl_FragDepth + +#define EAGLER_FRAG_OUT() layout(location = 0) out vec4 EAGLER_FRAG_COLOR; + +#endif +#endif + +// All Shaders: + +#define EAGLER_TEXTURE_2D(tex, coord2f) texture(tex, coord2f) +#define EAGLER_TEXTURE_2D_LOD(_tex, _coord2f, _lod1f) textureLod(_tex, _coord2f, _lod1f) +#define EAGLER_HAS_TEXTURE_2D_LOD + + +#else +#ifdef EAGLER_HAS_GLES_200 + +// For GLES 2.00 (WebGL 1.0) +#ifdef EAGLER_IS_VERTEX_SHADER + +// Vertex Shaders: +#define EAGLER_VSH_LAYOUT_BEGIN() +#define EAGLER_VSH_LAYOUT_END() +#define EAGLER_IN(_loc, _type, _name) attribute _type _name; +#define EAGLER_IN_AUTO(_type, _name) attribute _type _name; +#define EAGLER_OUT(_type, _name) varying _type _name; +#define EAGLER_VERT_POSITION gl_Position + +#else +#ifdef EAGLER_IS_FRAGMENT_SHADER + +// Fragment Shaders: +#define EAGLER_IN(_type, _name) varying _type _name; +#define EAGLER_FRAG_COLOR gl_FragColor +// TODO: Must require EXT_frag_depth to use this on GLES 2.0 (currently not needed) +#define EAGLER_FRAG_DEPTH gl_FragDepth + +#define EAGLER_FRAG_OUT() + +#endif +#endif + +// All Shaders: + +#define EAGLER_TEXTURE_2D(_tex, _coord2f) texture2D(_tex, _coord2f) + +#ifdef EAGLER_HAS_GLES_200_SHADER_TEXTURE_LOD +#define EAGLER_TEXTURE_2D_LOD(_tex, _coord2f, _lod1f) texture2DLodEXT(_tex, _coord2f, _lod1f) +#define EAGLER_HAS_TEXTURE_2D_LOD +#else +// Beware! +#define EAGLER_TEXTURE_2D_LOD(_tex, _coord2f, _lod1f) texture2D(_tex, _coord2f) +#define EAGLER_HAS_TEXTURE_2D_LOD +#endif + +#else +#error Unable to determine API version! (Missing directive EAGLER_HAS_GLES_200 or 300) +#endif +#endif + +#line 1 0 \ No newline at end of file diff --git a/sources/resources/assets/eagler/glsl/hw_fingerprint.fsh b/sources/resources/assets/eagler/glsl/hw_fingerprint.fsh new file mode 100644 index 00000000..0abd43f6 --- /dev/null +++ b/sources/resources/assets/eagler/glsl/hw_fingerprint.fsh @@ -0,0 +1,55 @@ +#line 2 + +/* + * Copyright (c) 2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +EAGLER_IN(vec2, v_position2f) + +EAGLER_FRAG_OUT() + +uniform sampler2D u_inputTexture; +uniform mat4 u_textureMatrix; + +vec2 rand(in vec2 co){ + float f = dot(co, vec2(12.98984576, 78.23378678)); + return fract(vec2(sin(f + 0.32490982), cos(f - 0.69890)) * 43758.54576873); +} + +void main() { + vec4 coords4f = vec4(v_position2f.x * 0.25 - 0.125, v_position2f.y * 0.25 - 0.125, v_position2f.y * 10.0 - 9.0, 1.0); + coords4f = u_textureMatrix * coords4f; + coords4f.xy /= coords4f.w; + EAGLER_FRAG_COLOR = EAGLER_TEXTURE_2D(u_inputTexture, coords4f.xy * 0.5 + 0.5); + EAGLER_FRAG_COLOR.rg += rand(v_position2f * 1.2344574345) * 0.05; + EAGLER_FRAG_COLOR.ba -= rand(v_position2f * 1.2343525225) * 0.05; + EAGLER_FRAG_COLOR.a = fract(sin(dot(coords4f.yz, vec2(12.9898, 78.233))) * 43758.5453); + EAGLER_FRAG_COLOR.a += exp(length(rand(coords4f.xw)) * -69.420); + EAGLER_FRAG_COLOR = pow(EAGLER_FRAG_COLOR, vec4(1.0 / 2.423952)); + EAGLER_FRAG_COLOR = pow(EAGLER_FRAG_COLOR, vec4(5.4523856)); + EAGLER_FRAG_COLOR += 0.00004423 + EAGLER_FRAG_COLOR.a * 0.02; + EAGLER_FRAG_COLOR = sqrt(EAGLER_FRAG_COLOR); + EAGLER_FRAG_COLOR = pow(EAGLER_FRAG_COLOR, vec4(1.0 / 1.9023576)); +#ifdef EAGLER_HAS_GLES_300 + EAGLER_FRAG_COLOR.ra += tanh(fract(EAGLER_FRAG_COLOR.a * 32.324834)) * 0.1012426; +#endif + EAGLER_FRAG_COLOR.b *= 0.934924; + EAGLER_FRAG_COLOR.b += (1.23213 / inversesqrt(EAGLER_FRAG_COLOR.a)) * 0.156365; + EAGLER_FRAG_COLOR.ga += rand(gl_FragCoord.xy) * 0.13423567; + EAGLER_FRAG_COLOR.rb += gl_PointCoord * 0.0124264565; +#ifdef EAGLER_HAS_GLES_300 + EAGLER_FRAG_COLOR *= 0.95234 + asinh(EAGLER_FRAG_COLOR.g * 5.23423) * 0.0254325; +#endif +} diff --git a/sources/resources/assets/eagler/glsl/local.vsh b/sources/resources/assets/eagler/glsl/local.vsh index 4617298e..cd74b1eb 100644 --- a/sources/resources/assets/eagler/glsl/local.vsh +++ b/sources/resources/assets/eagler/glsl/local.vsh @@ -1,7 +1,7 @@ #line 2 /* - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -16,15 +16,13 @@ * */ -precision lowp int; -precision highp float; -precision lowp sampler2D; +EAGLER_VSH_LAYOUT_BEGIN() +EAGLER_IN(0, vec2, a_position2f) +EAGLER_VSH_LAYOUT_END() -layout(location = 0) in vec2 a_position2f; - -out vec2 v_position2f; +EAGLER_OUT(vec2, v_position2f) void main() { v_position2f = a_position2f * 0.5 + 0.5; - gl_Position = vec4(a_position2f, 0.0, 1.0); + EAGLER_VERT_POSITION = vec4(a_position2f, 0.0, 1.0); } diff --git a/sources/resources/assets/eagler/glsl/post_fxaa.fsh b/sources/resources/assets/eagler/glsl/post_fxaa.fsh index 9914f0a4..4878106a 100644 --- a/sources/resources/assets/eagler/glsl/post_fxaa.fsh +++ b/sources/resources/assets/eagler/glsl/post_fxaa.fsh @@ -1,11 +1,13 @@ #line 2 // Remove this line below if you plan to modify this file +#ifndef EAGLER_IS_GLES_200 #define USE_OPTIMIZED +#endif /* - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -52,14 +54,9 @@ * */ -precision lowp int; -precision mediump float; -precision mediump sampler2D; +EAGLER_IN(vec2, v_position2f) - -in vec2 v_position2f; - -layout(location = 0) out vec4 output4f; +EAGLER_FRAG_OUT() uniform sampler2D u_screenTexture; uniform vec2 u_screenSize2f; @@ -110,7 +107,7 @@ uniform vec2 u_screenSize2f; #define FxaaTex sampler2D /*--------------------------------------------------------------------------*/ - #define FxaaTexTop(t, p) textureLod(t, p, 0.0) + #define FxaaTexTop(t, p) EAGLER_TEXTURE_2D_LOD(t, p, 0.0) /*============================================================================ GREEN AS LUMA OPTION SUPPORT FUNCTION @@ -298,7 +295,7 @@ void main(){ rcpFrameOpt.xy = -screenSize05; rcpFrameOpt.zw = screenSize05; - output4f = vec4(FxaaPixelShader(v_position2f + screenSize05, posPos, u_screenTexture, rcpFrameOpt, rcpFrameOpt * 4.0, edgeSharpness, edgeThreshold, edgeThresholdMin).rgb, 1.0); + EAGLER_FRAG_COLOR = vec4(FxaaPixelShader(v_position2f + screenSize05, posPos, u_screenTexture, rcpFrameOpt, rcpFrameOpt * 4.0, edgeSharpness, edgeThreshold, edgeThresholdMin).rgb, 1.0); } #else @@ -306,7 +303,6 @@ void main(){ // Is it faster? Idfk, probably compiles faster at least, what matters it I tried float _616; -vec4 _617; void main() { @@ -317,14 +313,14 @@ void main() mediump vec4 _608; for(;;) { - mediump vec3 _532 = textureLod(u_screenTexture, _611.xy, 0.0).xyz; + mediump vec3 _532 = EAGLER_TEXTURE_2D_LOD(u_screenTexture, _611.xy, 0.0).xyz; mediump float _536 = dot(_532 * _532, vec3(0.2989999949932098388671875, 0.58700001239776611328125, 0.114000000059604644775390625)); - mediump vec3 _540 = textureLod(u_screenTexture, _611.xw, 0.0).xyz; + mediump vec3 _540 = EAGLER_TEXTURE_2D_LOD(u_screenTexture, _611.xw, 0.0).xyz; mediump float _544 = dot(_540 * _540, vec3(0.2989999949932098388671875, 0.58700001239776611328125, 0.114000000059604644775390625)); - mediump vec3 _548 = textureLod(u_screenTexture, _611.zy, 0.0).xyz; - mediump vec3 _556 = textureLod(u_screenTexture, _611.zw, 0.0).xyz; + mediump vec3 _548 = EAGLER_TEXTURE_2D_LOD(u_screenTexture, _611.zy, 0.0).xyz; + mediump vec3 _556 = EAGLER_TEXTURE_2D_LOD(u_screenTexture, _611.zw, 0.0).xyz; mediump float _560 = dot(_556 * _556, vec3(0.2989999949932098388671875, 0.58700001239776611328125, 0.114000000059604644775390625)); - mediump vec4 _390 = textureLod(u_screenTexture, _290, 0.0); + mediump vec4 _390 = EAGLER_TEXTURE_2D_LOD(u_screenTexture, _290, 0.0); mediump vec3 _564 = _390.xyz; mediump float _568 = dot(_564 * _564, vec3(0.2989999949932098388671875, 0.58700001239776611328125, 0.114000000059604644775390625)); mediump float _397 = dot(_548 * _548, vec3(0.2989999949932098388671875, 0.58700001239776611328125, 0.114000000059604644775390625)) + 0.00260416674427688121795654296875; @@ -347,8 +343,8 @@ void main() mediump vec2 _484 = (_612 * 4.0).zw; vec2 _615 = -hp_copy_481; mediump vec2 mp_copy_615 = _615; - mediump vec4 _498 = textureLod(u_screenTexture, mp_copy_614 * _454 + _290, 0.0) + textureLod(u_screenTexture, _449 * _454 + _290, 0.0); - mediump vec4 _505 = ((textureLod(u_screenTexture, mp_copy_615 * _484 + _290, 0.0) + textureLod(u_screenTexture, _481 * _484 + _290, 0.0)) * 0.25) + (_498 * 0.25); + mediump vec4 _498 = EAGLER_TEXTURE_2D_LOD(u_screenTexture, mp_copy_614 * _454 + _290, 0.0) + EAGLER_TEXTURE_2D_LOD(u_screenTexture, _449 * _454 + _290, 0.0); + mediump vec4 _505 = ((EAGLER_TEXTURE_2D_LOD(u_screenTexture, mp_copy_615 * _484 + _290, 0.0) + EAGLER_TEXTURE_2D_LOD(u_screenTexture, _481 * _484 + _290, 0.0)) * 0.25) + (_498 * 0.25); mediump float _576 = dot(_505.xyz * _505.xyz, vec3(0.2989999949932098388671875, 0.58700001239776611328125, 0.114000000059604644775390625)); mediump vec4 _607; if ((_576 < _412) || (_576 > _409)) @@ -367,7 +363,7 @@ void main() _608 = _607; break; } - output4f = vec4(_608.xyz, 1.0); + EAGLER_FRAG_COLOR = vec4(_608.xyz, 1.0); } #endif \ No newline at end of file diff --git a/sources/resources/assets/eagler/glsl/texture_blit.fsh b/sources/resources/assets/eagler/glsl/texture_blit.fsh index 7b3fb59b..debfcc0f 100644 --- a/sources/resources/assets/eagler/glsl/texture_blit.fsh +++ b/sources/resources/assets/eagler/glsl/texture_blit.fsh @@ -1,7 +1,7 @@ #line 2 /* - * Copyright (c) 2023 lax1dude. All Rights Reserved. + * Copyright (c) 2023-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -16,14 +16,10 @@ * */ -precision lowp int; -precision highp float; -precision highp sampler2D; - -in vec2 v_texCoords2f; +EAGLER_IN(vec2, v_texCoords2f) #ifndef COMPILE_BLIT_DEPTH -layout(location = 0) out vec4 output4f; +EAGLER_FRAG_OUT() #endif uniform sampler2D u_inputTexture; @@ -40,8 +36,8 @@ void main() { uv2f = (floor(uv2f * u_pixelAlignmentSizes4f.xy) + u_pixelAlignmentOffset2f) * u_pixelAlignmentSizes4f.zw; #endif #ifndef COMPILE_BLIT_DEPTH - output4f = textureLod(u_inputTexture, uv2f, u_textureLod1f); + EAGLER_FRAG_COLOR = EAGLER_TEXTURE_2D_LOD(u_inputTexture, uv2f, u_textureLod1f); #else - gl_FragDepth = textureLod(u_inputTexture, uv2f, u_textureLod1f).r; + EAGLER_FRAG_DEPTH = EAGLER_TEXTURE_2D_LOD(u_inputTexture, uv2f, u_textureLod1f).r; #endif } diff --git a/sources/resources/assets/eagler/glsl/texture_blit.vsh b/sources/resources/assets/eagler/glsl/texture_blit.vsh index 645e1c4e..d5d18343 100644 --- a/sources/resources/assets/eagler/glsl/texture_blit.vsh +++ b/sources/resources/assets/eagler/glsl/texture_blit.vsh @@ -1,7 +1,7 @@ #line 2 /* - * Copyright (c) 2023 lax1dude. All Rights Reserved. + * Copyright (c) 2023-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -16,13 +16,11 @@ * */ -precision lowp int; -precision lowp float; -precision lowp sampler2D; +EAGLER_VSH_LAYOUT_BEGIN() +EAGLER_IN(0, vec2, a_position2f) +EAGLER_VSH_LAYOUT_END() -layout(location = 0) in vec2 a_position2f; - -out vec2 v_texCoords2f; +EAGLER_OUT(vec2, v_texCoords2f) uniform vec4 u_srcCoords4f; uniform vec4 u_dstCoords4f; @@ -30,5 +28,5 @@ uniform vec4 u_dstCoords4f; void main() { vec2 uv = a_position2f * 0.5 + 0.5; v_texCoords2f = u_srcCoords4f.xy + u_srcCoords4f.zw * uv; - gl_Position = vec4(u_dstCoords4f.xy + u_dstCoords4f.zw * uv, 0.0, 1.0); + EAGLER_VERT_POSITION = vec4(u_dstCoords4f.xy + u_dstCoords4f.zw * uv, 0.0, 1.0); } diff --git a/sources/resources/assets/eagler/glsl/texture_mix.fsh b/sources/resources/assets/eagler/glsl/texture_mix.fsh index 3cef92f8..26cb13fb 100644 --- a/sources/resources/assets/eagler/glsl/texture_mix.fsh +++ b/sources/resources/assets/eagler/glsl/texture_mix.fsh @@ -1,7 +1,7 @@ #line 2 /* - * Copyright (c) 2022-2023 lax1dude. All Rights Reserved. + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -16,13 +16,9 @@ * */ -precision lowp int; -precision highp float; -precision highp sampler2D; +EAGLER_IN(vec2, v_position2f) -in vec2 v_position2f; - -layout(location = 0) out vec4 output4f; +EAGLER_FRAG_OUT() uniform sampler2D u_inputTexture; uniform float u_textureLod1f; @@ -32,6 +28,6 @@ uniform mat3 u_matrixTransform; void main() { vec3 coords = u_matrixTransform * vec3(v_position2f, 1.0); - vec4 color4f = textureLod(u_inputTexture, coords.xy, u_textureLod1f); - output4f = color4f * u_blendFactor4f + u_blendBias4f; + vec4 color4f = EAGLER_TEXTURE_2D_LOD(u_inputTexture, coords.xy, u_textureLod1f); + EAGLER_FRAG_COLOR = color4f * u_blendFactor4f + u_blendBias4f; } diff --git a/sources/resources/assets/eagler/gui/eagler_gui.png b/sources/resources/assets/eagler/gui/eagler_gui.png index 704285831160bc03f8e492dd22684bab0d01690c..5d9d516ddecaca2a7f3026187ce08dd71289b97d 100644 GIT binary patch literal 11256 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 O+SxPbP0CI?M*JV1vMhK2 diff --git a/sources/resources/assets/eagler/gui/notif_bk_large.png b/sources/resources/assets/eagler/gui/notif_bk_large.png new file mode 100644 index 0000000000000000000000000000000000000000..9cab14524960b47d69482185ddc35a94a60b1d84 GIT binary patch literal 5089 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

Su<?$EQBa(|lTqq)dS4L|y#zfRcYA6MM9*U{AD~ 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 z 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

compileClassPathEntries-> Additional compiler class path entries
classPathEntries-> BuildStrategy.setClassPathEntries(List<String>)
entryPointName-> BuildStrategy.setEntryPointName(String)
mainClass-> BuildStrategy.setMainClass(String)