From 49d30eb728c2df5ce4608d969ae604eca2bda99c Mon Sep 17 00:00:00 2001 From: lnkosadmin Date: Fri, 31 Oct 2025 13:53:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=80=E5=88=9D=E7=9A=84=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 + META-INF/mods.toml | 49 + META-INF/neoforge.mods.toml | 49 + README.md | 54 + backup_old_config/build.gradle | 60 + backup_old_config/gradle.properties | 15 + backup_old_config/settings.gradle | 28 + build.gradle | 45 + cursors.piskel | 1 + gradle.properties | 39 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52271 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 164 +++ gradlew.bat | 90 ++ settings.gradle | 10 + src/main/java/net/montoyo/wd/SharedProxy.java | 148 +++ src/main/java/net/montoyo/wd/WebDisplays.java | 420 ++++++ .../montoyo/wd/block/KeyboardBlockLeft.java | 146 +++ .../montoyo/wd/block/KeyboardBlockRight.java | 119 ++ .../net/montoyo/wd/block/PeripheralBlock.java | 160 +++ .../net/montoyo/wd/block/ScreenBlock.java | 353 +++++ .../montoyo/wd/block/WDContainerBlock.java | 22 + .../montoyo/wd/block/item/KeyboardItem.java | 54 + .../net/montoyo/wd/client/ClientProxy.java | 862 ++++++++++++ .../montoyo/wd/client/JSQueryDispatcher.java | 370 ++++++ .../java/net/montoyo/wd/client/WDScheme.java | 215 +++ .../wd/client/audio/WDAudioSource.java | 116 ++ .../montoyo/wd/client/gui/CommandHandler.java | 18 + .../montoyo/wd/client/gui/GuiKeyboard.java | 377 ++++++ .../net/montoyo/wd/client/gui/GuiMinePad.java | 356 +++++ .../wd/client/gui/GuiRedstoneCtrl.java | 74 ++ .../wd/client/gui/GuiScreenConfig.java | 523 ++++++++ .../net/montoyo/wd/client/gui/GuiServer.java | 810 ++++++++++++ .../net/montoyo/wd/client/gui/GuiSetURL2.java | 155 +++ .../montoyo/wd/client/gui/GuiSubscribe.java | 15 + .../montoyo/wd/client/gui/RenderRecipe.java | 205 +++ .../net/montoyo/wd/client/gui/WDScreen.java | 410 ++++++ .../wd/client/gui/camera/KeyboardCamera.java | 271 ++++ .../wd/client/gui/controls/BasicControl.java | 73 ++ .../wd/client/gui/controls/Button.java | 238 ++++ .../wd/client/gui/controls/CheckBox.java | 144 ++ .../wd/client/gui/controls/Container.java | 198 +++ .../wd/client/gui/controls/Control.java | 319 +++++ .../wd/client/gui/controls/ControlGroup.java | 211 +++ .../montoyo/wd/client/gui/controls/Event.java | 14 + .../montoyo/wd/client/gui/controls/Icon.java | 98 ++ .../montoyo/wd/client/gui/controls/Label.java | 104 ++ .../montoyo/wd/client/gui/controls/List.java | 421 ++++++ .../wd/client/gui/controls/TextField.java | 373 ++++++ .../wd/client/gui/controls/UpgradeGroup.java | 154 +++ .../wd/client/gui/controls/YTButton.java | 79 ++ .../wd/client/gui/loading/FillControl.java | 19 + .../wd/client/gui/loading/GuiLoader.java | 85 ++ .../wd/client/gui/loading/JsonAWrapper.java | 61 + .../wd/client/gui/loading/JsonOWrapper.java | 311 +++++ .../wd/client/renderers/IItemRenderer.java | 25 + .../renderers/LaserPointerRenderer.java | 129 ++ .../wd/client/renderers/MinePadRenderer.java | 163 +++ .../wd/client/renderers/ModelMinePad.java | 72 + .../wd/client/renderers/ScreenBaker.java | 236 ++++ .../client/renderers/ScreenModelLoader.java | 64 + .../wd/client/renderers/ScreenRenderer.java | 194 +++ .../net/montoyo/wd/config/ClientConfig.java | 133 ++ .../net/montoyo/wd/config/CommonConfig.java | 127 ++ .../montoyo/wd/config/annoconfg/AnnoCFG.java | 220 ++++ .../wd/config/annoconfg/ConfigEntry.java | 15 + .../annotation/format/CFGSegment.java | 9 + .../annoconfg/annotation/format/Comment.java | 9 + .../annoconfg/annotation/format/Config.java | 12 + .../annoconfg/annotation/format/Name.java | 9 + .../annoconfg/annotation/format/Skip.java | 8 + .../annotation/format/Translation.java | 9 + .../annoconfg/annotation/value/Default.java | 16 + .../annotation/value/DoubleRange.java | 10 + .../annoconfg/annotation/value/IntRange.java | 10 + .../annoconfg/annotation/value/LongRange.java | 10 + .../config/annoconfg/handle/UnsafeHandle.java | 86 ++ .../wd/config/annoconfg/util/EnumType.java | 25 + .../montoyo/wd/controls/ScreenControl.java | 38 + .../wd/controls/ScreenControlRegistry.java | 77 ++ .../wd/controls/ScreenControlType.java | 19 + .../controls/builtin/AutoVolumeControl.java | 50 + .../wd/controls/builtin/ClickControl.java | 62 + .../wd/controls/builtin/JSRequestControl.java | 62 + .../wd/controls/builtin/KeyTypedControl.java | 54 + .../wd/controls/builtin/LaserControl.java | 79 ++ .../ManageRightsAndUpdgradesControl.java | 126 ++ .../builtin/ModifyFriendListControl.java | 56 + .../wd/controls/builtin/OwnerControl.java | 47 + .../controls/builtin/ScreenModifyControl.java | 75 ++ .../wd/controls/builtin/SetURLControl.java | 64 + .../wd/controls/builtin/TurnOffControl.java | 48 + .../java/net/montoyo/wd/core/CCArguments.java | 91 ++ .../montoyo/wd/core/CCPeripheralProvider.java | 33 + .../net/montoyo/wd/core/CraftComponent.java | 37 + .../montoyo/wd/core/DefaultPeripheral.java | 73 ++ .../net/montoyo/wd/core/DefaultUpgrade.java | 50 + .../net/montoyo/wd/core/HasAdvancement.java | 11 + .../net/montoyo/wd/core/IComputerArgs.java | 14 + .../java/net/montoyo/wd/core/IPeripheral.java | 15 + .../montoyo/wd/core/IScreenQueryHandler.java | 17 + .../java/net/montoyo/wd/core/IUpgrade.java | 20 + .../net/montoyo/wd/core/IWDDCapability.java | 11 + .../net/montoyo/wd/core/JSServerRequest.java | 52 + .../wd/core/MissingPermissionException.java | 26 + .../java/net/montoyo/wd/core/OCArguments.java | 43 + .../net/montoyo/wd/core/ScreenRights.java | 25 + .../java/net/montoyo/wd/core/WDCriterion.java | 38 + .../net/montoyo/wd/core/WDDCapability.java | 70 + .../java/net/montoyo/wd/data/GuiData.java | 71 + .../net/montoyo/wd/data/KeyboardData.java | 74 ++ .../net/montoyo/wd/data/RedstoneCtrlData.java | 61 + .../net/montoyo/wd/data/ScreenConfigData.java | 109 ++ .../java/net/montoyo/wd/data/ServerData.java | 52 + .../java/net/montoyo/wd/data/SetURLData.java | 83 ++ .../net/montoyo/wd/data/WDDataComponents.java | 75 ++ .../entity/AbstractInterfaceBlockEntity.java | 413 ++++++ .../entity/AbstractPeripheralBlockEntity.java | 142 ++ .../wd/entity/CCInterfaceBlockEntity.java | 82 ++ .../wd/entity/KeyboardBlockEntity.java | 79 ++ .../wd/entity/OCInterfaceBlockEntity.java | 135 ++ .../wd/entity/RedstoneControlBlockEntity.java | 110 ++ .../wd/entity/RemoteControlBlockEntity.java | 48 + .../montoyo/wd/entity/ScreenBlockEntity.java | 1167 +++++++++++++++++ .../net/montoyo/wd/entity/ScreenData.java | 212 +++ .../montoyo/wd/entity/ServerBlockEntity.java | 53 + .../montoyo/wd/item/ItemCraftComponent.java | 26 + .../net/montoyo/wd/item/ItemLaserPointer.java | 152 +++ .../java/net/montoyo/wd/item/ItemLinker.java | 128 ++ .../net/montoyo/wd/item/ItemMinePad2.java | 118 ++ .../java/net/montoyo/wd/item/ItemMulti.java | 25 + .../montoyo/wd/item/ItemOwnershipThief.java | 111 ++ .../wd/item/ItemScreenConfigurator.java | 63 + .../java/net/montoyo/wd/item/ItemUpgrade.java | 61 + src/main/java/net/montoyo/wd/item/WDItem.java | 23 + .../montoyo/wd/miniserv/AbstractClient.java | 161 +++ .../net/montoyo/wd/miniserv/Constants.java | 28 + .../montoyo/wd/miniserv/KeyParameters.java | 15 + .../montoyo/wd/miniserv/OutgoingPacket.java | 108 ++ .../montoyo/wd/miniserv/PacketHandler.java | 18 + .../net/montoyo/wd/miniserv/PacketID.java | 25 + .../net/montoyo/wd/miniserv/PacketReader.java | 60 + .../net/montoyo/wd/miniserv/PacketWriter.java | 62 + .../net/montoyo/wd/miniserv/SyncPlugin.java | 39 + .../montoyo/wd/miniserv/client/Client.java | 392 ++++++ .../wd/miniserv/client/ClientTask.java | 56 + .../miniserv/client/ClientTaskCheckFile.java | 61 + .../miniserv/client/ClientTaskDeleteFile.java | 40 + .../wd/miniserv/client/ClientTaskGetFile.java | 157 +++ .../client/ClientTaskGetFileList.java | 44 + .../miniserv/client/ClientTaskGetQuota.java | 44 + .../miniserv/client/ClientTaskUploadFile.java | 120 ++ .../wd/miniserv/server/ClientManager.java | 107 ++ .../montoyo/wd/miniserv/server/Server.java | 251 ++++ .../wd/miniserv/server/ServerClient.java | 397 ++++++ .../montoyo/wd/mixins/MouseHandlerMixin.java | 37 + .../net/montoyo/wd/mixins/OverlayMixin.java | 32 + .../java/net/montoyo/wd/net/BufferUtils.java | 82 ++ .../net/montoyo/wd/net/WDNetworkRegistry.java | 128 ++ .../net/client_bound/S2CMessageACResult.java | 48 + .../net/client_bound/S2CMessageAddScreen.java | 142 ++ .../net/client_bound/S2CMessageCloseGui.java | 58 + .../client_bound/S2CMessageJSResponse.java | 82 ++ .../client_bound/S2CMessageMiniservKey.java | 44 + .../net/client_bound/S2CMessageOpenGui.java | 50 + .../client_bound/S2CMessageScreenUpdate.java | 104 ++ .../client_bound/S2CMessageServerInfo.java | 47 + .../net/server_bound/C2SMessageACQuery.java | 61 + .../server_bound/C2SMessageMinepadUrl.java | 67 + .../C2SMessageMiniservConnect.java | 55 + .../server_bound/C2SMessageRedstoneCtrl.java | 74 ++ .../server_bound/C2SMessageScreenCtrl.java | 166 +++ .../montoyo/wd/registry/BlockRegistry.java | 31 + .../net/montoyo/wd/registry/ItemRegistry.java | 76 ++ .../net/montoyo/wd/registry/TileRegistry.java | 34 + .../java/net/montoyo/wd/registry/WDTabs.java | 52 + .../net/montoyo/wd/utilities/DistSafety.java | 16 + .../java/net/montoyo/wd/utilities/Log.java | 36 + .../net/montoyo/wd/utilities/Multiblock.java | 176 +++ .../net/montoyo/wd/utilities/NibbleArray.java | 45 + .../montoyo/wd/utilities/ScreenIterator.java | 64 + .../net/montoyo/wd/utilities/VideoType.java | 137 ++ .../wd/utilities/browser/InWorldQueries.java | 14 + .../wd/utilities/browser/WDBrowser.java | 40 + .../wd/utilities/browser/WDClientBrowser.java | 57 + .../browser/handlers/DisplayHandler.java | 69 + .../utilities/browser/handlers/WDRouter.java | 147 +++ .../browser/handlers/js/FileName.java | 9 + .../browser/handlers/js/JSQueryHandler.java | 20 + .../browser/handlers/js/Scripts.java | 33 + .../js/queries/ElementCenterQuery.java | 89 ++ .../handlers/js/queries/GetSizeQuery.java | 32 + .../montoyo/wd/utilities/data/BlockSide.java | 55 + .../net/montoyo/wd/utilities/data/Bounds.java | 27 + .../montoyo/wd/utilities/data/Rotation.java | 26 + .../wd/utilities/math/MutableAABB.java | 89 ++ .../montoyo/wd/utilities/math/Vector2i.java | 37 + .../montoyo/wd/utilities/math/Vector3f.java | 184 +++ .../montoyo/wd/utilities/math/Vector3i.java | 228 ++++ .../serialization/DontSerialize.java | 15 + .../utilities/serialization/NameUUIDPair.java | 69 + .../wd/utilities/serialization/TypeData.java | 58 + .../wd/utilities/serialization/Util.java | 205 +++ .../resources/META-INF/accesstransformer.cfg | 14 + .../resources/META-INF/neoforge.mods.toml | 49 + .../assets/webdisplays/atlases/blocks.json | 20 + .../webdisplays/blockstates/kb_left.json | 10 + .../webdisplays/blockstates/kb_right.json | 10 + .../assets/webdisplays/blockstates/rctrl.json | 7 + .../webdisplays/blockstates/redctrl.json | 7 + .../webdisplays/blockstates/screen.json | 7 + .../webdisplays/blockstates/server.json | 7 + .../assets/webdisplays/gui/kb_right.json | 38 + .../assets/webdisplays/gui/redstonectrl.json | 72 + .../assets/webdisplays/gui/screencfg.json | 214 +++ .../assets/webdisplays/gui/seturl.json | 53 + .../assets/webdisplays/html/blacklisted.html | 11 + .../assets/webdisplays/html/front.png | Bin 0 -> 1797 bytes .../resources/assets/webdisplays/html/io.html | 167 +++ .../assets/webdisplays/html/jquery.js | 2 + .../assets/webdisplays/html/main.html | 97 ++ .../assets/webdisplays/html/side.png | Bin 0 -> 479 bytes .../assets/webdisplays/html/wdlib.js | 108 ++ .../assets/webdisplays/html/webdisplays.png | Bin 0 -> 7072 bytes .../assets/webdisplays/js/mouse_event.js | 12 + .../assets/webdisplays/js/pointer_lock.js | 47 + .../assets/webdisplays/js/query_element.js | 31 + .../assets/webdisplays/lang/en_us.json | 159 +++ .../assets/webdisplays/lang/fr_fr.json | 156 +++ .../assets/webdisplays/lang/zh_cn.json | 151 +++ .../webdisplays/models/block/ccinterface.json | 8 + .../webdisplays/models/block/kb_left.json | 503 +++++++ .../webdisplays/models/block/kb_right.json | 1 + .../webdisplays/models/block/ocinterface.json | 8 + .../webdisplays/models/block/rctrl.json | 6 + .../webdisplays/models/block/redctrl.json | 6 + .../webdisplays/models/block/screen.json | 3 + .../webdisplays/models/block/screen_item.json | 6 + .../webdisplays/models/block/server.json | 12 + .../models/item/advicon_brokenpad.json | 6 + .../models/item/advicon_pigeon.json | 6 + .../webdisplays/models/item/advicon_wd.json | 6 + .../webdisplays/models/item/ccinterface.json | 8 + .../models/item/craftcomp_backlight.json | 6 + .../models/item/craftcomp_badextcard.json | 6 + .../models/item/craftcomp_batcell.json | 6 + .../models/item/craftcomp_batpack.json | 6 + .../models/item/craftcomp_extcard.json | 6 + .../models/item/craftcomp_laserdiode.json | 6 + .../models/item/craftcomp_peripheral.json | 6 + .../models/item/craftcomp_stonekey.json | 6 + .../models/item/craftcomp_upgrade.json | 6 + .../webdisplays/models/item/keyboard.json | 6 + .../webdisplays/models/item/laserpointer.json | 6 + .../webdisplays/models/item/linker.json | 6 + .../webdisplays/models/item/minepad.json | 1 + .../webdisplays/models/item/ocinterface.json | 8 + .../webdisplays/models/item/ownerthief.json | 6 + .../assets/webdisplays/models/item/rctrl.json | 6 + .../webdisplays/models/item/redctrl.json | 6 + .../webdisplays/models/item/screen.json | 6 + .../webdisplays/models/item/screencfg.json | 6 + .../webdisplays/models/item/server.json | 12 + .../webdisplays/models/item/upgrade_gps.json | 7 + .../models/item/upgrade_lasermouse.json | 7 + .../models/item/upgrade_redinput.json | 7 + .../models/item/upgrade_redoutput.json | 7 + .../resources/assets/webdisplays/sounds.json | 45 + .../assets/webdisplays/sounds/ironic.ogg | Bin 0 -> 59295 bytes .../assets/webdisplays/sounds/keyboard1.ogg | Bin 0 -> 8769 bytes .../assets/webdisplays/sounds/keyboard2.ogg | Bin 0 -> 11637 bytes .../assets/webdisplays/sounds/keyboard3.ogg | Bin 0 -> 11716 bytes .../assets/webdisplays/sounds/keyboard4.ogg | Bin 0 -> 9683 bytes .../assets/webdisplays/sounds/keyboard5.ogg | Bin 0 -> 7567 bytes .../assets/webdisplays/sounds/keyboard6.ogg | Bin 0 -> 9827 bytes .../assets/webdisplays/sounds/keyboard7.ogg | Bin 0 -> 7858 bytes .../assets/webdisplays/sounds/keyboard8.ogg | Bin 0 -> 9751 bytes .../webdisplays/sounds/screencfg_open.ogg | Bin 0 -> 5820 bytes .../assets/webdisplays/sounds/server.ogg | Bin 0 -> 37348 bytes .../assets/webdisplays/sounds/upgrade_add.ogg | Bin 0 -> 27697 bytes .../assets/webdisplays/sounds/upgrade_del.ogg | Bin 0 -> 17200 bytes .../webdisplays/textures/block/cci_front.png | Bin 0 -> 472 bytes .../webdisplays/textures/block/cci_side.png | Bin 0 -> 378 bytes .../webdisplays/textures/block/cci_topbot.png | Bin 0 -> 617 bytes .../webdisplays/textures/block/kb_base.png | Bin 0 -> 164 bytes .../webdisplays/textures/block/kb_key.png | Bin 0 -> 182 bytes .../webdisplays/textures/block/oci_front.png | Bin 0 -> 765 bytes .../webdisplays/textures/block/oci_side.png | Bin 0 -> 676 bytes .../webdisplays/textures/block/oci_topbot.png | Bin 0 -> 645 bytes .../webdisplays/textures/block/rctrl.png | Bin 0 -> 673 bytes .../webdisplays/textures/block/redctrl.png | Bin 0 -> 761 bytes .../webdisplays/textures/block/screen0.png | Bin 0 -> 116 bytes .../webdisplays/textures/block/screen1.png | Bin 0 -> 174 bytes .../webdisplays/textures/block/screen10.png | Bin 0 -> 205 bytes .../webdisplays/textures/block/screen11.png | Bin 0 -> 237 bytes .../webdisplays/textures/block/screen12.png | Bin 0 -> 218 bytes .../webdisplays/textures/block/screen13.png | Bin 0 -> 237 bytes .../webdisplays/textures/block/screen14.png | Bin 0 -> 233 bytes .../webdisplays/textures/block/screen15.png | Bin 0 -> 254 bytes .../webdisplays/textures/block/screen2.png | Bin 0 -> 182 bytes .../webdisplays/textures/block/screen3.png | Bin 0 -> 219 bytes .../webdisplays/textures/block/screen4.png | Bin 0 -> 175 bytes .../webdisplays/textures/block/screen5.png | Bin 0 -> 198 bytes .../webdisplays/textures/block/screen6.png | Bin 0 -> 216 bytes .../webdisplays/textures/block/screen7.png | Bin 0 -> 239 bytes .../webdisplays/textures/block/screen8.png | Bin 0 -> 178 bytes .../webdisplays/textures/block/screen9.png | Bin 0 -> 214 bytes .../webdisplays/textures/block/server.png | Bin 0 -> 520 bytes .../webdisplays/textures/block/server2.png | Bin 0 -> 351 bytes .../webdisplays/textures/gui/checkbox.png | Bin 0 -> 269 bytes .../textures/gui/checkbox_checked.png | Bin 0 -> 775 bytes .../webdisplays/textures/gui/cursors.png | Bin 0 -> 3777 bytes .../assets/webdisplays/textures/gui/edges.png | Bin 0 -> 257 bytes .../webdisplays/textures/gui/server_bg.png | Bin 0 -> 817 bytes .../webdisplays/textures/gui/server_fg.png | Bin 0 -> 1314 bytes .../textures/item/advicon_brokenpad.png | Bin 0 -> 373 bytes .../textures/item/advicon_pigeon.png | Bin 0 -> 322 bytes .../webdisplays/textures/item/advicon_wd.png | Bin 0 -> 299 bytes .../webdisplays/textures/item/backlight.png | Bin 0 -> 619 bytes .../webdisplays/textures/item/badextcard.png | Bin 0 -> 382 bytes .../webdisplays/textures/item/batcell.png | Bin 0 -> 211 bytes .../webdisplays/textures/item/batpack.png | Bin 0 -> 228 bytes .../webdisplays/textures/item/extcard.png | Bin 0 -> 276 bytes .../assets/webdisplays/textures/item/gps.png | Bin 0 -> 246 bytes .../webdisplays/textures/item/input.png | Bin 0 -> 203 bytes .../webdisplays/textures/item/kb_inv.png | Bin 0 -> 251 bytes .../webdisplays/textures/item/laserdiode.png | Bin 0 -> 310 bytes .../textures/item/laserpointer.png | Bin 0 -> 368 bytes .../textures/item/laserpointer2.png | Bin 0 -> 268 bytes .../webdisplays/textures/item/linker.png | Bin 0 -> 634 bytes .../webdisplays/textures/item/minepad.png | Bin 0 -> 315 bytes .../textures/item/model/minepad.png | Bin 0 -> 258 bytes .../textures/item/model/minepad_item.png | Bin 0 -> 290 bytes .../webdisplays/textures/item/output.png | Bin 0 -> 200 bytes .../webdisplays/textures/item/ownerthief.png | Bin 0 -> 256 bytes .../webdisplays/textures/item/peripheral.png | Bin 0 -> 300 bytes .../webdisplays/textures/item/screencfg.png | Bin 0 -> 373 bytes .../webdisplays/textures/item/stonekey.png | Bin 0 -> 225 bytes .../webdisplays/textures/item/upgrade.png | Bin 0 -> 650 bytes .../textures/item/upgrade.png.mcmeta | 9 + .../tags/blocks/mineable/pickaxe.json | 11 + .../advancements/keyboard_cat.json | 14 + .../advancements/link_peripheral.json | 14 + .../webdisplays/advancements/pad_break.json | 26 + .../data/webdisplays/advancements/root.json | 42 + .../data/webdisplays/advancements/screen.json | 36 + .../webdisplays/advancements/upgrade.json | 14 + .../loot_tables/blocks/kb_left.json | 14 + .../loot_tables/blocks/kb_right.json | 14 + .../webdisplays/loot_tables/blocks/rctrl.json | 14 + .../loot_tables/blocks/redctrl.json | 14 + .../loot_tables/blocks/screen.json | 14 + .../loot_tables/blocks/server.json | 14 + .../data/webdisplays/recipes/backlight.json | 20 + .../data/webdisplays/recipes/batcell.json | 24 + .../data/webdisplays/recipes/batpack.json | 15 + .../data/webdisplays/recipes/extcard.json | 31 + .../data/webdisplays/recipes/keyboard.json | 23 + .../data/webdisplays/recipes/laserdiode.json | 27 + .../webdisplays/recipes/laserpointer.json | 25 + .../data/webdisplays/recipes/linker.json | 26 + .../data/webdisplays/recipes/minepad.json | 37 + .../data/webdisplays/recipes/minepad2.json | 22 + .../data/webdisplays/recipes/peripheral.json | 21 + .../data/webdisplays/recipes/rctrl.json | 20 + .../data/webdisplays/recipes/redctrl1.json | 24 + .../data/webdisplays/recipes/redctrl2.json | 20 + .../data/webdisplays/recipes/screen.json | 33 + .../data/webdisplays/recipes/screencfg.json | 22 + .../data/webdisplays/recipes/server.json | 20 + .../data/webdisplays/recipes/stonekey.json | 14 + .../data/webdisplays/recipes/upgrade.json | 22 + .../data/webdisplays/recipes/upgrade_gps.json | 18 + .../webdisplays/recipes/upgrade_laser.json | 19 + .../webdisplays/recipes/upgrade_redin.json | 18 + .../webdisplays/recipes/upgrade_redout.json | 18 + src/main/resources/pack.mcmeta | 6 + src/main/resources/webdisplays.accesswidener | 2 + src/main/resources/webdisplays.mixins.json | 16 + src/test/java/UShortToBytes.java | 24 + 380 files changed, 25153 insertions(+) create mode 100644 LICENSE create mode 100644 META-INF/mods.toml create mode 100644 META-INF/neoforge.mods.toml create mode 100644 README.md create mode 100644 backup_old_config/build.gradle create mode 100644 backup_old_config/gradle.properties create mode 100644 backup_old_config/settings.gradle create mode 100644 build.gradle create mode 100644 cursors.piskel create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/net/montoyo/wd/SharedProxy.java create mode 100644 src/main/java/net/montoyo/wd/WebDisplays.java create mode 100644 src/main/java/net/montoyo/wd/block/KeyboardBlockLeft.java create mode 100644 src/main/java/net/montoyo/wd/block/KeyboardBlockRight.java create mode 100644 src/main/java/net/montoyo/wd/block/PeripheralBlock.java create mode 100644 src/main/java/net/montoyo/wd/block/ScreenBlock.java create mode 100644 src/main/java/net/montoyo/wd/block/WDContainerBlock.java create mode 100644 src/main/java/net/montoyo/wd/block/item/KeyboardItem.java create mode 100644 src/main/java/net/montoyo/wd/client/ClientProxy.java create mode 100644 src/main/java/net/montoyo/wd/client/JSQueryDispatcher.java create mode 100644 src/main/java/net/montoyo/wd/client/WDScheme.java create mode 100644 src/main/java/net/montoyo/wd/client/audio/WDAudioSource.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/CommandHandler.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/GuiKeyboard.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/GuiMinePad.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/GuiRedstoneCtrl.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/GuiScreenConfig.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/GuiServer.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/GuiSetURL2.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/GuiSubscribe.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/RenderRecipe.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/WDScreen.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/camera/KeyboardCamera.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/BasicControl.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/Button.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/CheckBox.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/Container.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/Control.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/ControlGroup.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/Event.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/Icon.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/Label.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/List.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/TextField.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/UpgradeGroup.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/controls/YTButton.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/loading/FillControl.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/loading/GuiLoader.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/loading/JsonAWrapper.java create mode 100644 src/main/java/net/montoyo/wd/client/gui/loading/JsonOWrapper.java create mode 100644 src/main/java/net/montoyo/wd/client/renderers/IItemRenderer.java create mode 100644 src/main/java/net/montoyo/wd/client/renderers/LaserPointerRenderer.java create mode 100644 src/main/java/net/montoyo/wd/client/renderers/MinePadRenderer.java create mode 100644 src/main/java/net/montoyo/wd/client/renderers/ModelMinePad.java create mode 100644 src/main/java/net/montoyo/wd/client/renderers/ScreenBaker.java create mode 100644 src/main/java/net/montoyo/wd/client/renderers/ScreenModelLoader.java create mode 100644 src/main/java/net/montoyo/wd/client/renderers/ScreenRenderer.java create mode 100644 src/main/java/net/montoyo/wd/config/ClientConfig.java create mode 100644 src/main/java/net/montoyo/wd/config/CommonConfig.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/AnnoCFG.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/ConfigEntry.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/annotation/format/CFGSegment.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/annotation/format/Comment.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/annotation/format/Config.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/annotation/format/Name.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/annotation/format/Skip.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/annotation/format/Translation.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/annotation/value/Default.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/annotation/value/DoubleRange.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/annotation/value/IntRange.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/annotation/value/LongRange.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/handle/UnsafeHandle.java create mode 100644 src/main/java/net/montoyo/wd/config/annoconfg/util/EnumType.java create mode 100644 src/main/java/net/montoyo/wd/controls/ScreenControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/ScreenControlRegistry.java create mode 100644 src/main/java/net/montoyo/wd/controls/ScreenControlType.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/AutoVolumeControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/ClickControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/JSRequestControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/KeyTypedControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/LaserControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/ManageRightsAndUpdgradesControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/ModifyFriendListControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/OwnerControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/ScreenModifyControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/SetURLControl.java create mode 100644 src/main/java/net/montoyo/wd/controls/builtin/TurnOffControl.java create mode 100644 src/main/java/net/montoyo/wd/core/CCArguments.java create mode 100644 src/main/java/net/montoyo/wd/core/CCPeripheralProvider.java create mode 100644 src/main/java/net/montoyo/wd/core/CraftComponent.java create mode 100644 src/main/java/net/montoyo/wd/core/DefaultPeripheral.java create mode 100644 src/main/java/net/montoyo/wd/core/DefaultUpgrade.java create mode 100644 src/main/java/net/montoyo/wd/core/HasAdvancement.java create mode 100644 src/main/java/net/montoyo/wd/core/IComputerArgs.java create mode 100644 src/main/java/net/montoyo/wd/core/IPeripheral.java create mode 100644 src/main/java/net/montoyo/wd/core/IScreenQueryHandler.java create mode 100644 src/main/java/net/montoyo/wd/core/IUpgrade.java create mode 100644 src/main/java/net/montoyo/wd/core/IWDDCapability.java create mode 100644 src/main/java/net/montoyo/wd/core/JSServerRequest.java create mode 100644 src/main/java/net/montoyo/wd/core/MissingPermissionException.java create mode 100644 src/main/java/net/montoyo/wd/core/OCArguments.java create mode 100644 src/main/java/net/montoyo/wd/core/ScreenRights.java create mode 100644 src/main/java/net/montoyo/wd/core/WDCriterion.java create mode 100644 src/main/java/net/montoyo/wd/core/WDDCapability.java create mode 100644 src/main/java/net/montoyo/wd/data/GuiData.java create mode 100644 src/main/java/net/montoyo/wd/data/KeyboardData.java create mode 100644 src/main/java/net/montoyo/wd/data/RedstoneCtrlData.java create mode 100644 src/main/java/net/montoyo/wd/data/ScreenConfigData.java create mode 100644 src/main/java/net/montoyo/wd/data/ServerData.java create mode 100644 src/main/java/net/montoyo/wd/data/SetURLData.java create mode 100644 src/main/java/net/montoyo/wd/data/WDDataComponents.java create mode 100644 src/main/java/net/montoyo/wd/entity/AbstractInterfaceBlockEntity.java create mode 100644 src/main/java/net/montoyo/wd/entity/AbstractPeripheralBlockEntity.java create mode 100644 src/main/java/net/montoyo/wd/entity/CCInterfaceBlockEntity.java create mode 100644 src/main/java/net/montoyo/wd/entity/KeyboardBlockEntity.java create mode 100644 src/main/java/net/montoyo/wd/entity/OCInterfaceBlockEntity.java create mode 100644 src/main/java/net/montoyo/wd/entity/RedstoneControlBlockEntity.java create mode 100644 src/main/java/net/montoyo/wd/entity/RemoteControlBlockEntity.java create mode 100644 src/main/java/net/montoyo/wd/entity/ScreenBlockEntity.java create mode 100644 src/main/java/net/montoyo/wd/entity/ScreenData.java create mode 100644 src/main/java/net/montoyo/wd/entity/ServerBlockEntity.java create mode 100644 src/main/java/net/montoyo/wd/item/ItemCraftComponent.java create mode 100644 src/main/java/net/montoyo/wd/item/ItemLaserPointer.java create mode 100644 src/main/java/net/montoyo/wd/item/ItemLinker.java create mode 100644 src/main/java/net/montoyo/wd/item/ItemMinePad2.java create mode 100644 src/main/java/net/montoyo/wd/item/ItemMulti.java create mode 100644 src/main/java/net/montoyo/wd/item/ItemOwnershipThief.java create mode 100644 src/main/java/net/montoyo/wd/item/ItemScreenConfigurator.java create mode 100644 src/main/java/net/montoyo/wd/item/ItemUpgrade.java create mode 100644 src/main/java/net/montoyo/wd/item/WDItem.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/AbstractClient.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/Constants.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/KeyParameters.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/OutgoingPacket.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/PacketHandler.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/PacketID.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/PacketReader.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/PacketWriter.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/SyncPlugin.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/Client.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/ClientTask.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/ClientTaskCheckFile.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/ClientTaskDeleteFile.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/ClientTaskGetFile.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/ClientTaskGetFileList.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/ClientTaskGetQuota.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/client/ClientTaskUploadFile.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/server/ClientManager.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/server/Server.java create mode 100644 src/main/java/net/montoyo/wd/miniserv/server/ServerClient.java create mode 100644 src/main/java/net/montoyo/wd/mixins/MouseHandlerMixin.java create mode 100644 src/main/java/net/montoyo/wd/mixins/OverlayMixin.java create mode 100644 src/main/java/net/montoyo/wd/net/BufferUtils.java create mode 100644 src/main/java/net/montoyo/wd/net/WDNetworkRegistry.java create mode 100644 src/main/java/net/montoyo/wd/net/client_bound/S2CMessageACResult.java create mode 100644 src/main/java/net/montoyo/wd/net/client_bound/S2CMessageAddScreen.java create mode 100644 src/main/java/net/montoyo/wd/net/client_bound/S2CMessageCloseGui.java create mode 100644 src/main/java/net/montoyo/wd/net/client_bound/S2CMessageJSResponse.java create mode 100644 src/main/java/net/montoyo/wd/net/client_bound/S2CMessageMiniservKey.java create mode 100644 src/main/java/net/montoyo/wd/net/client_bound/S2CMessageOpenGui.java create mode 100644 src/main/java/net/montoyo/wd/net/client_bound/S2CMessageScreenUpdate.java create mode 100644 src/main/java/net/montoyo/wd/net/client_bound/S2CMessageServerInfo.java create mode 100644 src/main/java/net/montoyo/wd/net/server_bound/C2SMessageACQuery.java create mode 100644 src/main/java/net/montoyo/wd/net/server_bound/C2SMessageMinepadUrl.java create mode 100644 src/main/java/net/montoyo/wd/net/server_bound/C2SMessageMiniservConnect.java create mode 100644 src/main/java/net/montoyo/wd/net/server_bound/C2SMessageRedstoneCtrl.java create mode 100644 src/main/java/net/montoyo/wd/net/server_bound/C2SMessageScreenCtrl.java create mode 100644 src/main/java/net/montoyo/wd/registry/BlockRegistry.java create mode 100644 src/main/java/net/montoyo/wd/registry/ItemRegistry.java create mode 100644 src/main/java/net/montoyo/wd/registry/TileRegistry.java create mode 100644 src/main/java/net/montoyo/wd/registry/WDTabs.java create mode 100644 src/main/java/net/montoyo/wd/utilities/DistSafety.java create mode 100644 src/main/java/net/montoyo/wd/utilities/Log.java create mode 100644 src/main/java/net/montoyo/wd/utilities/Multiblock.java create mode 100644 src/main/java/net/montoyo/wd/utilities/NibbleArray.java create mode 100644 src/main/java/net/montoyo/wd/utilities/ScreenIterator.java create mode 100644 src/main/java/net/montoyo/wd/utilities/VideoType.java create mode 100644 src/main/java/net/montoyo/wd/utilities/browser/InWorldQueries.java create mode 100644 src/main/java/net/montoyo/wd/utilities/browser/WDBrowser.java create mode 100644 src/main/java/net/montoyo/wd/utilities/browser/WDClientBrowser.java create mode 100644 src/main/java/net/montoyo/wd/utilities/browser/handlers/DisplayHandler.java create mode 100644 src/main/java/net/montoyo/wd/utilities/browser/handlers/WDRouter.java create mode 100644 src/main/java/net/montoyo/wd/utilities/browser/handlers/js/FileName.java create mode 100644 src/main/java/net/montoyo/wd/utilities/browser/handlers/js/JSQueryHandler.java create mode 100644 src/main/java/net/montoyo/wd/utilities/browser/handlers/js/Scripts.java create mode 100644 src/main/java/net/montoyo/wd/utilities/browser/handlers/js/queries/ElementCenterQuery.java create mode 100644 src/main/java/net/montoyo/wd/utilities/browser/handlers/js/queries/GetSizeQuery.java create mode 100644 src/main/java/net/montoyo/wd/utilities/data/BlockSide.java create mode 100644 src/main/java/net/montoyo/wd/utilities/data/Bounds.java create mode 100644 src/main/java/net/montoyo/wd/utilities/data/Rotation.java create mode 100644 src/main/java/net/montoyo/wd/utilities/math/MutableAABB.java create mode 100644 src/main/java/net/montoyo/wd/utilities/math/Vector2i.java create mode 100644 src/main/java/net/montoyo/wd/utilities/math/Vector3f.java create mode 100644 src/main/java/net/montoyo/wd/utilities/math/Vector3i.java create mode 100644 src/main/java/net/montoyo/wd/utilities/serialization/DontSerialize.java create mode 100644 src/main/java/net/montoyo/wd/utilities/serialization/NameUUIDPair.java create mode 100644 src/main/java/net/montoyo/wd/utilities/serialization/TypeData.java create mode 100644 src/main/java/net/montoyo/wd/utilities/serialization/Util.java create mode 100644 src/main/resources/META-INF/accesstransformer.cfg create mode 100644 src/main/resources/META-INF/neoforge.mods.toml create mode 100644 src/main/resources/assets/webdisplays/atlases/blocks.json create mode 100644 src/main/resources/assets/webdisplays/blockstates/kb_left.json create mode 100644 src/main/resources/assets/webdisplays/blockstates/kb_right.json create mode 100644 src/main/resources/assets/webdisplays/blockstates/rctrl.json create mode 100644 src/main/resources/assets/webdisplays/blockstates/redctrl.json create mode 100644 src/main/resources/assets/webdisplays/blockstates/screen.json create mode 100644 src/main/resources/assets/webdisplays/blockstates/server.json create mode 100644 src/main/resources/assets/webdisplays/gui/kb_right.json create mode 100644 src/main/resources/assets/webdisplays/gui/redstonectrl.json create mode 100644 src/main/resources/assets/webdisplays/gui/screencfg.json create mode 100644 src/main/resources/assets/webdisplays/gui/seturl.json create mode 100644 src/main/resources/assets/webdisplays/html/blacklisted.html create mode 100644 src/main/resources/assets/webdisplays/html/front.png create mode 100644 src/main/resources/assets/webdisplays/html/io.html create mode 100644 src/main/resources/assets/webdisplays/html/jquery.js create mode 100644 src/main/resources/assets/webdisplays/html/main.html create mode 100644 src/main/resources/assets/webdisplays/html/side.png create mode 100644 src/main/resources/assets/webdisplays/html/wdlib.js create mode 100644 src/main/resources/assets/webdisplays/html/webdisplays.png create mode 100644 src/main/resources/assets/webdisplays/js/mouse_event.js create mode 100644 src/main/resources/assets/webdisplays/js/pointer_lock.js create mode 100644 src/main/resources/assets/webdisplays/js/query_element.js create mode 100644 src/main/resources/assets/webdisplays/lang/en_us.json create mode 100644 src/main/resources/assets/webdisplays/lang/fr_fr.json create mode 100644 src/main/resources/assets/webdisplays/lang/zh_cn.json create mode 100644 src/main/resources/assets/webdisplays/models/block/ccinterface.json create mode 100644 src/main/resources/assets/webdisplays/models/block/kb_left.json create mode 100644 src/main/resources/assets/webdisplays/models/block/kb_right.json create mode 100644 src/main/resources/assets/webdisplays/models/block/ocinterface.json create mode 100644 src/main/resources/assets/webdisplays/models/block/rctrl.json create mode 100644 src/main/resources/assets/webdisplays/models/block/redctrl.json create mode 100644 src/main/resources/assets/webdisplays/models/block/screen.json create mode 100644 src/main/resources/assets/webdisplays/models/block/screen_item.json create mode 100644 src/main/resources/assets/webdisplays/models/block/server.json create mode 100644 src/main/resources/assets/webdisplays/models/item/advicon_brokenpad.json create mode 100644 src/main/resources/assets/webdisplays/models/item/advicon_pigeon.json create mode 100644 src/main/resources/assets/webdisplays/models/item/advicon_wd.json create mode 100644 src/main/resources/assets/webdisplays/models/item/ccinterface.json create mode 100644 src/main/resources/assets/webdisplays/models/item/craftcomp_backlight.json create mode 100644 src/main/resources/assets/webdisplays/models/item/craftcomp_badextcard.json create mode 100644 src/main/resources/assets/webdisplays/models/item/craftcomp_batcell.json create mode 100644 src/main/resources/assets/webdisplays/models/item/craftcomp_batpack.json create mode 100644 src/main/resources/assets/webdisplays/models/item/craftcomp_extcard.json create mode 100644 src/main/resources/assets/webdisplays/models/item/craftcomp_laserdiode.json create mode 100644 src/main/resources/assets/webdisplays/models/item/craftcomp_peripheral.json create mode 100644 src/main/resources/assets/webdisplays/models/item/craftcomp_stonekey.json create mode 100644 src/main/resources/assets/webdisplays/models/item/craftcomp_upgrade.json create mode 100644 src/main/resources/assets/webdisplays/models/item/keyboard.json create mode 100644 src/main/resources/assets/webdisplays/models/item/laserpointer.json create mode 100644 src/main/resources/assets/webdisplays/models/item/linker.json create mode 100644 src/main/resources/assets/webdisplays/models/item/minepad.json create mode 100644 src/main/resources/assets/webdisplays/models/item/ocinterface.json create mode 100644 src/main/resources/assets/webdisplays/models/item/ownerthief.json create mode 100644 src/main/resources/assets/webdisplays/models/item/rctrl.json create mode 100644 src/main/resources/assets/webdisplays/models/item/redctrl.json create mode 100644 src/main/resources/assets/webdisplays/models/item/screen.json create mode 100644 src/main/resources/assets/webdisplays/models/item/screencfg.json create mode 100644 src/main/resources/assets/webdisplays/models/item/server.json create mode 100644 src/main/resources/assets/webdisplays/models/item/upgrade_gps.json create mode 100644 src/main/resources/assets/webdisplays/models/item/upgrade_lasermouse.json create mode 100644 src/main/resources/assets/webdisplays/models/item/upgrade_redinput.json create mode 100644 src/main/resources/assets/webdisplays/models/item/upgrade_redoutput.json create mode 100644 src/main/resources/assets/webdisplays/sounds.json create mode 100644 src/main/resources/assets/webdisplays/sounds/ironic.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/keyboard1.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/keyboard2.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/keyboard3.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/keyboard4.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/keyboard5.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/keyboard6.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/keyboard7.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/keyboard8.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/screencfg_open.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/server.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/upgrade_add.ogg create mode 100644 src/main/resources/assets/webdisplays/sounds/upgrade_del.ogg create mode 100644 src/main/resources/assets/webdisplays/textures/block/cci_front.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/cci_side.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/cci_topbot.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/kb_base.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/kb_key.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/oci_front.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/oci_side.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/oci_topbot.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/rctrl.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/redctrl.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen0.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen1.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen10.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen11.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen12.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen13.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen14.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen15.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen2.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen3.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen4.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen5.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen6.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen7.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen8.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/screen9.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/server.png create mode 100644 src/main/resources/assets/webdisplays/textures/block/server2.png create mode 100644 src/main/resources/assets/webdisplays/textures/gui/checkbox.png create mode 100644 src/main/resources/assets/webdisplays/textures/gui/checkbox_checked.png create mode 100644 src/main/resources/assets/webdisplays/textures/gui/cursors.png create mode 100644 src/main/resources/assets/webdisplays/textures/gui/edges.png create mode 100644 src/main/resources/assets/webdisplays/textures/gui/server_bg.png create mode 100644 src/main/resources/assets/webdisplays/textures/gui/server_fg.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/advicon_brokenpad.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/advicon_pigeon.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/advicon_wd.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/backlight.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/badextcard.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/batcell.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/batpack.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/extcard.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/gps.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/input.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/kb_inv.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/laserdiode.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/laserpointer.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/laserpointer2.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/linker.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/minepad.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/model/minepad.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/model/minepad_item.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/output.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/ownerthief.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/peripheral.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/screencfg.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/stonekey.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/upgrade.png create mode 100644 src/main/resources/assets/webdisplays/textures/item/upgrade.png.mcmeta create mode 100644 src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json create mode 100644 src/main/resources/data/webdisplays/advancements/keyboard_cat.json create mode 100644 src/main/resources/data/webdisplays/advancements/link_peripheral.json create mode 100644 src/main/resources/data/webdisplays/advancements/pad_break.json create mode 100644 src/main/resources/data/webdisplays/advancements/root.json create mode 100644 src/main/resources/data/webdisplays/advancements/screen.json create mode 100644 src/main/resources/data/webdisplays/advancements/upgrade.json create mode 100644 src/main/resources/data/webdisplays/loot_tables/blocks/kb_left.json create mode 100644 src/main/resources/data/webdisplays/loot_tables/blocks/kb_right.json create mode 100644 src/main/resources/data/webdisplays/loot_tables/blocks/rctrl.json create mode 100644 src/main/resources/data/webdisplays/loot_tables/blocks/redctrl.json create mode 100644 src/main/resources/data/webdisplays/loot_tables/blocks/screen.json create mode 100644 src/main/resources/data/webdisplays/loot_tables/blocks/server.json create mode 100644 src/main/resources/data/webdisplays/recipes/backlight.json create mode 100644 src/main/resources/data/webdisplays/recipes/batcell.json create mode 100644 src/main/resources/data/webdisplays/recipes/batpack.json create mode 100644 src/main/resources/data/webdisplays/recipes/extcard.json create mode 100644 src/main/resources/data/webdisplays/recipes/keyboard.json create mode 100644 src/main/resources/data/webdisplays/recipes/laserdiode.json create mode 100644 src/main/resources/data/webdisplays/recipes/laserpointer.json create mode 100644 src/main/resources/data/webdisplays/recipes/linker.json create mode 100644 src/main/resources/data/webdisplays/recipes/minepad.json create mode 100644 src/main/resources/data/webdisplays/recipes/minepad2.json create mode 100644 src/main/resources/data/webdisplays/recipes/peripheral.json create mode 100644 src/main/resources/data/webdisplays/recipes/rctrl.json create mode 100644 src/main/resources/data/webdisplays/recipes/redctrl1.json create mode 100644 src/main/resources/data/webdisplays/recipes/redctrl2.json create mode 100644 src/main/resources/data/webdisplays/recipes/screen.json create mode 100644 src/main/resources/data/webdisplays/recipes/screencfg.json create mode 100644 src/main/resources/data/webdisplays/recipes/server.json create mode 100644 src/main/resources/data/webdisplays/recipes/stonekey.json create mode 100644 src/main/resources/data/webdisplays/recipes/upgrade.json create mode 100644 src/main/resources/data/webdisplays/recipes/upgrade_gps.json create mode 100644 src/main/resources/data/webdisplays/recipes/upgrade_laser.json create mode 100644 src/main/resources/data/webdisplays/recipes/upgrade_redin.json create mode 100644 src/main/resources/data/webdisplays/recipes/upgrade_redout.json create mode 100644 src/main/resources/pack.mcmeta create mode 100644 src/main/resources/webdisplays.accesswidener create mode 100644 src/main/resources/webdisplays.mixins.json create mode 100644 src/test/java/UShortToBytes.java diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..49e4784 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 CinemaMod Group + +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. diff --git a/META-INF/mods.toml b/META-INF/mods.toml new file mode 100644 index 0000000..6b416e7 --- /dev/null +++ b/META-INF/mods.toml @@ -0,0 +1,49 @@ +modLoader="javafml" #mandatory + +# Use NeoForge's FML loader range for 1.21.1 +loaderVersion="[1,)" #mandatory + +license="MIT" + +issueTrackerURL="" #optional + +[[mods]] #mandatory + +modId="webdisplays" #mandatory + +# Align mod version with 1.21.1 build +version="2.0.1-1.21.1" #mandatory + +displayName="WebDisplays" #mandatory + +displayURL="https://github.com/CinemaMod/webdisplays" #optional + +logoFile= "" #optional + +credits="" #optional + +authors="GiantLuigi4, ds58, Mysticpasta1, montoyo, WaterPicker" #optional + +description=''' +''' + +[[dependencies.webdisplays]] #optional +modId="neoforge" #mandatory +mandatory=true #mandatory +versionRange="[21.1.0,)" #mandatory +ordering="NONE" +side="BOTH" + +[[dependencies.webdisplays]] +modId="minecraft" +mandatory=true +versionRange="[1.21.1]" +ordering="NONE" +side="BOTH" + +[[dependencies.webdisplays]] +modId="mcef" +mandatory=true +versionRange="[2.1.6-1.21.1, )" +ordering="NONE" +side="BOTH" diff --git a/META-INF/neoforge.mods.toml b/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..6b416e7 --- /dev/null +++ b/META-INF/neoforge.mods.toml @@ -0,0 +1,49 @@ +modLoader="javafml" #mandatory + +# Use NeoForge's FML loader range for 1.21.1 +loaderVersion="[1,)" #mandatory + +license="MIT" + +issueTrackerURL="" #optional + +[[mods]] #mandatory + +modId="webdisplays" #mandatory + +# Align mod version with 1.21.1 build +version="2.0.1-1.21.1" #mandatory + +displayName="WebDisplays" #mandatory + +displayURL="https://github.com/CinemaMod/webdisplays" #optional + +logoFile= "" #optional + +credits="" #optional + +authors="GiantLuigi4, ds58, Mysticpasta1, montoyo, WaterPicker" #optional + +description=''' +''' + +[[dependencies.webdisplays]] #optional +modId="neoforge" #mandatory +mandatory=true #mandatory +versionRange="[21.1.0,)" #mandatory +ordering="NONE" +side="BOTH" + +[[dependencies.webdisplays]] +modId="minecraft" +mandatory=true +versionRange="[1.21.1]" +ordering="NONE" +side="BOTH" + +[[dependencies.webdisplays]] +modId="mcef" +mandatory=true +versionRange="[2.1.6-1.21.1, )" +ordering="NONE" +side="BOTH" diff --git a/README.md b/README.md new file mode 100644 index 0000000..5fa5396 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# WebDisplays(NeoForge 1.21.1 适配版) + +本仓库来源于原始项目并在本地克隆后进行适配与修复: +- 上游仓库:`https://github.com/CinemaMod/webdisplays.git` +- 本分支目标:适配 Minecraft 1.21.1(NeoForge)并在可用范围内修复关键问题 +- 大量代码改动由多位 AI 大模型辅助完成(自动化迁移、代码生成、问题定位与修复) + +## 当前状态 +- 完成基础功能适配(屏幕方块、模型加载、基础网络消息、MinePad 使用流程等) +- 修复 MinePad 初次使用时 URL 为空导致的崩溃(自定义负载编码 NPE) +- 调整屏幕模型贴图加载逻辑(通过 `assets/webdisplays/atlases/blocks.json` 注册 `screen0..screen15` 贴图) +- 默认主页为 `https://git.lnkos.cn`(`CommonConfig.Browser.homepage`) +- 已验证可构建并产出 JAR(Windows,JDK 21,NeoGradle) + +## 已知问题(进行中) +- 外置键盘(左/右部件)在某些朝向或交互下存在异常 +- 屏幕边框/部分贴图在某些资源包或拼图场景下异常(已加入图集声明,但仍需进一步兼容性验证) +- 代码中存在一定数量的弃用/迁移警告(不影响构建,后续逐步清理) + +## 构建环境与依赖 +- JDK:`21` +- 构建:`Gradle Wrapper`(已内置) +- Mod 平台:`NeoForge 1.21.1` +- 建议依赖:`MCEF (com.cinemamod:mcef-neoforge)` 对应 1.21.1 版本 + +## 本地构建 +在仓库根目录执行: +- Windows:`./gradlew.bat build -x test` +- 其他平台:`./gradlew build -x test` +构建成功后,JAR 位于 `build/libs/`。 + +## 安装与试用 +- 将生成的 `webdisplays-*.jar` 放入 `mods/` +- 确保安装对应版本的 `NeoForge` 与 `MCEF` +- 启动 1.21.1 客户端,创建世界,放置屏幕方块进行验证 + +## 主要改动摘要 +- 屏幕自定义模型:`ScreenModelLoader`/`ScreenBaker` + - 通过 `atlases/blocks.json` 将 `screen0..screen15` 注册至方块图集 + - 自定义烘焙模型以实现屏幕边框/连接效果 +- 网络与数据修复: + - MinePad URL 为空值编码导致崩溃修复(`C2SMessageMinepadUrl` 编码前对 `null` 处理) + - GUI 初始化对 `null` URL 防御式赋值,避免界面 NPE + +## 贡献与致谢 +- 原始项目与创意来源归上游仓库及其作者所有 +- 本地适配工作由多位 AI 大模型参与完成,辅助完成迁移、修复与文档编写 +- 欢迎提交 Issue/PR,共同完善 1.21.1 的适配与修复 + +## 许可证 +- 许可证遵循上游仓库约定(详见本仓库 `LICENSE`) + +--- +说明:本项目仍在持续迭代阶段,优先确保基础功能在 1.21.1 可用,其它外围与兼容性问题会逐步修复与完善。 \ No newline at end of file diff --git a/backup_old_config/build.gradle b/backup_old_config/build.gradle new file mode 100644 index 0000000..33d1a82 --- /dev/null +++ b/backup_old_config/build.gradle @@ -0,0 +1,60 @@ +plugins { + id 'net.neoforged.gradle.userdev' version '7.0.+' + id 'org.spongepowered.mixin' version '0.7.+' + id 'maven-publish' +} + +var mod_version = project.mod_version + +archivesBaseName = project.archives_base_name +version = mod_version +group = maven_group + +java.toolchain.languageVersion = JavaLanguageVersion.of(java_version as int) + +sourceSets.main.resources.srcDirs += 'src/generated/resources' +mixin.add sourceSets.main, "webdisplays.refmap.json" + +repositories { + maven { + name = "cursemaven" + url = "https://www.cursemaven.com" + } + maven { + name = 'mcef-releases' + url = 'https://mcef-download.cinemamod.com/repositories/releases/' + } + maven { + name = 'neoforged' + url = 'https://maven.neoforged.net/releases' + } +} + +dependencies { + implementation 'net.neoforged:neoforge:21.1.203' + annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' + + // useful for debugging performance problems + implementation "curse.maven:spark-361579:4381167" + // here because we need to manually open the VR keyboard + compileOnly "curse.maven:vivecraft-667903:4794431" + + implementation("com.cinemamod:mcef-neoforge:2.1.6-1.21.1") { + transitive = false + } +} + +jar { + manifest { + attributes([ + "Specification-Title": "WebDisplays", + "Specification-Vendor": "CinemaMod Group", + "Specification-Version": "1", + "Implementation-Title": project.name, + "Implementation-Version": project.version, + "Implementation-Vendor": "CinemaMod Group", + "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), + "MixinConfigs": "webdisplays.mixins.json" + ]) + } +} diff --git a/backup_old_config/gradle.properties b/backup_old_config/gradle.properties new file mode 100644 index 0000000..ebbb0b5 --- /dev/null +++ b/backup_old_config/gradle.properties @@ -0,0 +1,15 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs = -Xmx8G +mod_version = 2.0.2-1.21.1 +maven_group = com.cinemamod +archives_base_name = webdisplays + +# NeoForge 1.21.1 configuration +minecraft_version = 1.21.1 +neoforge_version = 21.1.203 +java_version = 21 + +systemProp.http.proxyHost=127.0.0.1 +systemProp.http.proxyPort=20803 +systemProp.https.proxyHost=127.0.0.1 +systemProp.https.proxyPort=20803 \ No newline at end of file diff --git a/backup_old_config/settings.gradle b/backup_old_config/settings.gradle new file mode 100644 index 0000000..685a894 --- /dev/null +++ b/backup_old_config/settings.gradle @@ -0,0 +1,28 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'NeoForged' + url = 'https://maven.neoforged.net/releases' + } + maven { url = 'https://maven.parchmentmc.org' } // Add this line + } +// resolutionStrategy { +// eachPlugin { +// switch (requested.id.toString()) { +// case "net.minecraftforge.gradle": { +// useModule("${requested.id}:ForgeGradle:${requested.version}") +// break +// } +// case "org.spongepowered.mixin": { +// useModule("org.spongepowered:mixingradle:${requested.version}") +// break; +// } +// } +// } +// } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0' +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5d14b4f --- /dev/null +++ b/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'net.neoforged.gradle.userdev' version '7.0.192' +} + +version = mod_version +group = mod_group_id + +repositories { + mavenCentral() + maven { url = 'https://maven.neoforged.net/releases' } + maven { + name = "cursemaven" + url = "https://www.cursemaven.com" + } + maven { + name = 'mcef-releases' + url = 'https://mcef-download.cinemamod.com/repositories/releases/' + } +} + +base { + archivesName = mod_id +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +dependencies { + implementation "net.neoforged:neoforge:${neo_version}" + + // Project dependencies + implementation "curse.maven:spark-361579:4381167" + compileOnly "curse.maven:vivecraft-667903:4794431" + + implementation("com.cinemamod:mcef-neoforge:2.1.6-1.21.1") { + transitive = false + } +} + +// Improve compiler diagnostics to speed up fixing API changes +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.compilerArgs += ['-Xdiags:verbose', '-Xlint:deprecation', '-Xlint:unchecked'] +} diff --git a/cursors.piskel b/cursors.piskel new file mode 100644 index 0000000..674606d --- /dev/null +++ b/cursors.piskel @@ -0,0 +1 @@ +{"modelVersion":2,"piskel":{"name":"image","description":"","fps":10,"height":256,"width":256,"layers":["{\"name\":\"Layer 1\",\"opacity\":0.5,\"frameCount\":1,\"chunks\":[{\"layout\":[[0]],\"base64PNG\":\"data:image/png;base64,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\"}]}","{\"name\":\"Layer 3\",\"opacity\":0.5,\"frameCount\":1,\"chunks\":[{\"layout\":[[0]],\"base64PNG\":\"data:image/png;base64,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\"}]}","{\"name\":\"Layer 2\",\"opacity\":0,\"frameCount\":1,\"chunks\":[{\"layout\":[[0]],\"base64PNG\":\"data:image/png;base64,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\"}]}"],"hiddenFrames":[""]}} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f7f1ec3 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,39 @@ +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +org.gradle.jvmargs=-Xmx4G +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true + +#read more on this at https://github.com/neoforged/NeoGradle/blob/NG_7.0/README.md#apply-parchment-mappings +# you can also find the latest versions at: https://parchmentmc.org/docs/getting-started +neogradle.subsystems.parchment.minecraftVersion=1.21.1 +neogradle.subsystems.parchment.mappingsVersion=2024.11.17 + +# Environment Properties +# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge +# The Minecraft version must agree with the Neo version to get a valid artifact +minecraft_version=1.21.1 +# The Minecraft version range can use any release version of Minecraft as bounds. +minecraft_version_range=[1.21.1] +# The Neo version must agree with the Minecraft version to get a valid artifact +neo_version=21.1.213 +# The loader version range can only use the major version of FML as bounds +loader_version_range=[1,) + +## Mod Properties + +# The unique mod identifier for the mod. Must be lowercase in English locale. +mod_id=webdisplays +# The human-readable display name for the mod. +mod_name=WebDisplays +# The license of the mod. +mod_license=LGPL-3.0 +# The mod version. +mod_version=1.1 +# The group ID for the mod. +mod_group_id=net.montoyo.wd +# The authors of the mod. +mod_authors=montoyo, CinemaMod Group +# The description of the mod. +mod_description=A Minecraft mod that adds web displays to the game using MCEF (Minecraft Chromium Embedded Framework). \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..30d399d8d2bf522ff5de94bf434a7cc43a9a74b5 GIT binary patch literal 52271 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 p \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3a3e5b9 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = 'https://maven.neoforged.net/releases' } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} diff --git a/src/main/java/net/montoyo/wd/SharedProxy.java b/src/main/java/net/montoyo/wd/SharedProxy.java new file mode 100644 index 0000000..cde737c --- /dev/null +++ b/src/main/java/net/montoyo/wd/SharedProxy.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd; + +import com.cinemamod.mcef.MCEF; +import com.mojang.authlib.GameProfile; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import net.neoforged.neoforge.server.ServerLifecycleHooks; +import net.montoyo.wd.core.HasAdvancement; +import net.montoyo.wd.core.JSServerRequest; +import net.montoyo.wd.data.GuiData; +import net.montoyo.wd.entity.ScreenBlockEntity; +import net.montoyo.wd.entity.ScreenData; +import net.montoyo.wd.utilities.*; +import net.montoyo.wd.utilities.math.Vector2i; +import net.montoyo.wd.utilities.math.Vector3i; +import net.montoyo.wd.utilities.data.BlockSide; +import net.montoyo.wd.utilities.data.Rotation; +import net.montoyo.wd.utilities.serialization.NameUUIDPair; +import org.joml.Vector3d; + +import javax.annotation.Nonnull; +import java.util.UUID; + +public class SharedProxy { + public void preInit() { + } + + public void init() { + MCEF.scheduleForInit((cef) -> onCefInit()); + } + + public void postInit() { + } + + public void onCefInit() { + } + + @Deprecated(forRemoval = true) + public Level getWorld(ResourceKey dim) { + return getServer().getLevel(dim); + } + + public BlockGetter getWorld(IPayloadContext context) { + if (context.player() != null) return context.player().level(); + return null; + } + + public void enqueue(Runnable r) { + ServerLifecycleHooks.getCurrentServer().addTickable(r); + } + + public void displayGui(GuiData data) { + Log.error("Called SharedProxy.displayGui() on server side..."); + } + + public void trackScreen(ScreenBlockEntity tes, boolean track) { + } + + public void onAutocompleteResult(NameUUIDPair pairs[]) { + } + + public GameProfile[] getOnlineGameProfiles() { + return ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayers().stream().map(Player::getGameProfile).toArray(GameProfile[]::new); + } + + public void screenUpdateResolutionInGui(Vector3i pos, BlockSide side, Vector2i res) { + } + + public void screenUpdateRotationInGui(Vector3i pos, BlockSide side, Rotation rot) { + } + + public void screenUpdateAutoVolumeInGui(Vector3i pos, BlockSide side, boolean av) { + } + + public void displaySetPadURLGui(ItemStack is, String padURL) { + Log.error("Called SharedProxy.displaySetPadURLGui() on server side..."); + } + + public void openMinePadGui(UUID padId) { + Log.error("Called SharedProxy.openMinePadGui() on server side..."); + } + + public void handleJSResponseSuccess(int reqId, JSServerRequest type, byte[] data) { + Log.error("Called SharedProxy.handleJSResponseSuccess() on server side..."); + } + + public void handleJSResponseError(int reqId, JSServerRequest type, int errCode, String err) { + Log.error("Called SharedProxy.handleJSResponseError() on server side..."); + } + + @Nonnull + public HasAdvancement hasClientPlayerAdvancement(@Nonnull ResourceLocation rl) { + return HasAdvancement.DONT_KNOW; + } + + public MinecraftServer getServer() { + return ServerLifecycleHooks.getCurrentServer(); + } + + public void setMiniservClientPort(int port) { + } + + public void startMiniservClient() { + } + + public boolean isMiniservDisabled() { + return false; + } + + public void closeGui(BlockPos bp, BlockSide bs) { + } + + public void renderRecipes() { + } + + public boolean isShiftDown() { + return false; + } + + public double distanceTo(ScreenBlockEntity tes, Vec3 position) { + double dist = Double.POSITIVE_INFINITY; + for (int i = 0; i < tes.screenCount(); i++) { + ScreenData scrn = tes.getScreen(i); + + Vector3d pos = new Vector3d( + scrn.side.right.x * scrn.size.x / 2d + scrn.size.y * scrn.side.up.x / 2d, + scrn.side.right.y * scrn.size.x / 2d + scrn.size.y * scrn.side.up.y / 2d, + scrn.side.right.z * scrn.size.x / 2d + scrn.size.y * scrn.side.up.z / 2d + ).add(tes.getBlockPos().getX(), tes.getBlockPos().getY(), tes.getBlockPos().getZ()); + + double dist2 = position.distanceToSqr(pos.x, pos.y, pos.z); + dist = Math.min(dist, dist2); + } + return dist; + } +} diff --git a/src/main/java/net/montoyo/wd/WebDisplays.java b/src/main/java/net/montoyo/wd/WebDisplays.java new file mode 100644 index 0000000..ea717c8 --- /dev/null +++ b/src/main/java/net/montoyo/wd/WebDisplays.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd; + +import com.google.gson.Gson; +import net.minecraft.ChatFormatting; +import net.minecraft.advancements.Advancement; +import net.minecraft.advancements.AdvancementHolder; +import net.minecraft.advancements.CriteriaTriggers; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModList; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.ModLoadingContext; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.ServerChatEvent; +import net.neoforged.neoforge.event.entity.item.ItemTossEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.level.LevelEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import net.neoforged.neoforge.registries.DeferredRegister; +import net.minecraft.core.registries.Registries; +import net.montoyo.wd.client.ClientProxy; +import net.montoyo.wd.client.gui.camera.KeyboardCamera; +import net.montoyo.wd.config.ClientConfig; +import net.montoyo.wd.config.CommonConfig; +import net.montoyo.wd.controls.ScreenControlRegistry; +import net.montoyo.wd.core.*; +import net.montoyo.wd.miniserv.server.Server; +import net.montoyo.wd.net.WDNetworkRegistry; +import net.montoyo.wd.net.client_bound.S2CMessageServerInfo; +import net.montoyo.wd.registry.BlockRegistry; +import net.montoyo.wd.registry.ItemRegistry; +import net.montoyo.wd.registry.TileRegistry; +import net.montoyo.wd.registry.WDTabs; +import net.montoyo.wd.utilities.DistSafety; +import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.utilities.serialization.Util; +import net.montoyo.wd.data.WDDataComponents; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Objects; +import java.util.UUID; + +@Mod("webdisplays") +public class WebDisplays { + public static final String MOD_ID = "webdisplays"; + public static WebDisplays INSTANCE; + + public static SharedProxy PROXY = null; + + public static final ResourceLocation ADV_PAD_BREAK = ResourceLocation.fromNamespaceAndPath("webdisplays", "pad_break"); + public static final String BLACKLIST_URL = "mod://webdisplays/blacklisted.html"; + public static final Gson GSON = new Gson(); + + //Sounds + public SoundEvent soundTyping; + public SoundEvent soundUpgradeAdd; + public SoundEvent soundUpgradeDel; + public SoundEvent soundScreenCfg; + public SoundEvent soundServer; + public SoundEvent soundIronic; + + //Criterions + public WDCriterion criterionPadBreak; + public WDCriterion criterionUpgradeScreen; + public WDCriterion criterionLinkPeripheral; + public WDCriterion criterionKeyboardCat; + + //Config + public static final double PAD_RATIO = 59.0 / 30.0; + public double padResX; + public double padResY; + private int lastPadId = 0; + public double unloadDistance2; + public double loadDistance2; + public int miniservPort; + public long miniservQuota; + public float ytVolume; + public float avDist100; + public float avDist0; + + // mod detection + private boolean hasOC; + private boolean hasCC; + + public WebDisplays() { + INSTANCE = this; + if(FMLEnvironment.dist.isClient()) { + PROXY = DistSafety.createProxy(); + } else { + PROXY = new SharedProxy(); + } + + if (FMLEnvironment.dist.isClient()) { + // proxies are annoying, so from now on, I'mma be just registering stuff in here + ModLoadingContext.get().getActiveContainer().getEventBus().addListener(ClientProxy::onKeybindRegistry); + ModLoadingContext.get().getActiveContainer().getEventBus().addListener(ClientProxy::onClientSetup); + ModLoadingContext.get().getActiveContainer().getEventBus().addListener(ClientProxy::onModelRegistryEvent); + // ClientProxy registers instance event handlers in preInit(); + // RenderHighlightEvent.Block is handled via ClientProxy#onDrawSelection (instance) + NeoForge.EVENT_BUS.addListener(KeyboardCamera::updateCamera); + NeoForge.EVENT_BUS.addListener(KeyboardCamera::gameTick); + ClientConfig.init(); + } + + CommonConfig.init(); + + //Criterions + criterionPadBreak = new WDCriterion(); + criterionUpgradeScreen = new WDCriterion(); + criterionLinkPeripheral = new WDCriterion(); + criterionKeyboardCat = new WDCriterion(); + + IEventBus bus = ModLoadingContext.get().getActiveContainer().getEventBus(); + bus.addListener(WDNetworkRegistry::register); + // Register custom advancement triggers at the correct time + bus.addListener(this::onRegisterTriggers); + WDNetworkRegistry.init(); + SOUNDS.register(bus); + onRegisterSounds(); + WDTabs.init(bus); + BlockRegistry.init(bus); + ItemRegistry.init(bus); + TileRegistry.init(bus); + WDDataComponents.DATA_COMPONENTS.register(bus); + WDDCapability.ATTACHMENT_TYPES.register(bus); + + PROXY.preInit(); + + NeoForge.EVENT_BUS.register(this); + + //Other things + PROXY.init(); + + PROXY.postInit(); + hasOC = ModList.get().isLoaded("opencomputers"); + hasCC = ModList.get().isLoaded("computercraft"); + + /* if(hasCC) { + try { + //We have to do this because the "register" method might be stripped out if CC isn't loaded + CCPeripheralProvider.class.getMethod("register").invoke(null); + } catch(Throwable t) { + Log.error("ComputerCraft was found, but WebDisplays wasn't able to register its CC Interface Peripheral"); + t.printStackTrace(); + } + } */ + + if (!FMLEnvironment.production) { + ScreenControlRegistry.init(); + } + } + + // Register custom advancement triggers via registry event to avoid freeze + private void onRegisterTriggers(final net.neoforged.neoforge.registries.RegisterEvent event) { + event.register(Registries.TRIGGER_TYPE, + ResourceLocation.fromNamespaceAndPath(MOD_ID, "pad_break"), + () -> criterionPadBreak); + event.register(Registries.TRIGGER_TYPE, + ResourceLocation.fromNamespaceAndPath(MOD_ID, "upgrade_screen"), + () -> criterionUpgradeScreen); + event.register(Registries.TRIGGER_TYPE, + ResourceLocation.fromNamespaceAndPath(MOD_ID, "link_peripheral"), + () -> criterionLinkPeripheral); + event.register(Registries.TRIGGER_TYPE, + ResourceLocation.fromNamespaceAndPath(MOD_ID, "keyboard_cat"), + () -> criterionKeyboardCat); + } + + public void onRegisterSounds() { + soundTyping = registerSound("keyboard_type"); + soundUpgradeAdd = registerSound("upgrade_add"); + soundUpgradeDel = registerSound("upgrade_del"); + soundScreenCfg = registerSound("screencfg_open"); + soundServer = registerSound("server"); + soundIronic = registerSound("ironic"); + } + + ArrayList> serverStartedDimensions = new ArrayList<>(); + + @SubscribeEvent + public void onWorldLoad(LevelEvent.Load ev) { + if (ev.getLevel() instanceof Level level) { + if (ev.getLevel().isClientSide() || level.dimension() != Level.OVERWORLD) + return; + + Path worldDir = Objects.requireNonNull(ev.getLevel().getServer()).getServerDirectory(); + Path f = worldDir.resolve("wd_next.txt"); + + if (Files.exists(f)) { + try { + BufferedReader br = Files.newBufferedReader(f); + String idx = br.readLine(); + Util.silentClose(br); + + if (idx == null) + throw new RuntimeException("Seems like the file is empty (1)"); + + idx = idx.trim(); + if (idx.isEmpty()) + throw new RuntimeException("Seems like the file is empty (2)"); + + lastPadId = Integer.parseInt(idx); //This will throw NumberFormatException if it goes wrong + } catch (Throwable t) { + Log.warningEx("Could not read last minePad ID from %s. I'm afraid this might break all minePads.", t, f.toAbsolutePath().toString()); + } + } + + if (miniservPort != 0) { + Server sv = Server.getInstance(); + + if(!serverStartedDimensions.contains(level.dimension())) { + sv.setPort(miniservPort); + sv.setDirectory(worldDir.resolve("wd_filehost").toFile()); + sv.start(); + serverStartedDimensions.add(level.dimension()); + } + } + } + } + + @SubscribeEvent + public void onWorldLeave(LevelEvent.Unload ev) throws IOException { + if(ev.getLevel() instanceof Level level) { + if (ev.getLevel().isClientSide() || level.dimension() != Level.OVERWORLD) + return; + Server sw = Server.getInstance(); + sw.stopServer(); + serverStartedDimensions.remove(level.dimension()); + } + } + + @SubscribeEvent + public void onWorldSave(LevelEvent.Save ev) { + if(ev.getLevel() instanceof Level level) { + if (ev.getLevel().isClientSide() || level.dimension() != Level.OVERWORLD) + return; + Path f = Objects.requireNonNull(ev.getLevel().getServer()).getServerDirectory().resolve("wd_next.txt"); + + try { + BufferedWriter bw = Files.newBufferedWriter(f); + bw.write("" + lastPadId + "\n"); + Util.silentClose(bw); + } catch (Throwable t) { + Log.warningEx("Could not save last minePad ID (%d) to %s. I'm afraid this might break all minePads.", t, lastPadId, f.toAbsolutePath().toString()); + } + } + } + + @SubscribeEvent + public void onToss(ItemTossEvent ev) { + if(!ev.getEntity().level().isClientSide) { + ItemStack is = ev.getEntity().getItem(); + + if(is.getItem() == ItemRegistry.MINEPAD.get()) { + UUID thrower = ev.getPlayer().getGameProfile().getId(); + is.set(WDDataComponents.THROWER_MSB.get(), thrower.getMostSignificantBits()); + is.set(WDDataComponents.THROWER_LSB.get(), thrower.getLeastSignificantBits()); + is.set(WDDataComponents.THROW_HEIGHT.get(), (float) (ev.getPlayer().getY() + ev.getPlayer().getEyeHeight())); + } + } + } + + @SubscribeEvent + public void onPlayerCraft(PlayerEvent.ItemCraftedEvent ev) { + if(CommonConfig.hardRecipes && ItemRegistry.isCompCraftItem(ev.getCrafting().getItem()) && (CraftComponent.EXTCARD.makeItemStack().is(ev.getCrafting().getItem()))) { + if((ev.getEntity() instanceof ServerPlayer && !hasPlayerAdvancement((ServerPlayer) ev.getEntity(), ADV_PAD_BREAK)) || PROXY.hasClientPlayerAdvancement(ADV_PAD_BREAK) != HasAdvancement.YES) { + ev.getCrafting().setDamageValue(CraftComponent.BADEXTCARD.ordinal()); + + if(!ev.getEntity().level().isClientSide) + ev.getEntity().level().playSound(null, ev.getEntity().getX(), ev.getEntity().getY(), ev.getEntity().getZ(), SoundEvents.ITEM_BREAK, SoundSource.MASTER, 1.0f, 1.0f); + } + } + } + + @SubscribeEvent + public void onServerStop(ServerStoppingEvent ev) throws IOException { + Server.getInstance().stopServer(); + } + + @SubscribeEvent + public void onLogIn(PlayerEvent.PlayerLoggedInEvent ev) { + if (!CommonConfig.joinMessage) { + return; + } + + if(!ev.getEntity().level().isClientSide && ev.getEntity() instanceof ServerPlayer) { + IWDDCapability cap = ev.getEntity().getData(WDDCapability.WDD_CAPABILITY.get()); + + if(cap.isFirstRun()) { + Util.toast(ev.getEntity(), ChatFormatting.LIGHT_PURPLE, "welcome1"); + Util.toast(ev.getEntity(), ChatFormatting.LIGHT_PURPLE, "welcome2"); + Util.toast(ev.getEntity(), ChatFormatting.LIGHT_PURPLE, "welcome3"); + + cap.clearFirstRun(); + } + + S2CMessageServerInfo message = new S2CMessageServerInfo(miniservPort); + WDNetworkRegistry.INSTANCE.send((ServerPlayer) ev.getEntity(), message); + } + } + + @SubscribeEvent + public void onLogOut(PlayerEvent.PlayerLoggedOutEvent ev) { + if(!ev.getEntity().level().isClientSide) + Server.getInstance().getClientManager().revokeClientKey(ev.getEntity().getGameProfile().getId()); + } + + + @SubscribeEvent + public void onPlayerClone(PlayerEvent.Clone ev) { + IWDDCapability src = ev.getOriginal().getData(WDDCapability.WDD_CAPABILITY.get()); + IWDDCapability dst = ev.getEntity().getData(WDDCapability.WDD_CAPABILITY.get()); + + if(src == null) { + Log.error("src is null"); + return; + } + + if(dst == null) { + Log.error("dst is null"); + return; + } + + src.cloneTo(dst); + } + + @SubscribeEvent + public void onServerChat(ServerChatEvent ev) { + String msg = ev.getMessage().getString().replaceAll("\\s+", " ").toLowerCase(); + StringBuilder sb = new StringBuilder(msg.length()); + for(int i = 0; i < msg.length(); i++) { + char chr = msg.charAt(i); + + if(chr != '.' && chr != ',' && chr != ';' && chr != '!' && chr != '?' && chr != ':' && chr != '\'' && chr != '\"' && chr != '`') + sb.append(chr); + } + + if(sb.toString().equals("ironic he could save others from death but not himself")) { + Player ply = ev.getPlayer(); + ply.level().playSound(null, ply.getX(), ply.getY(), ply.getZ(), soundIronic, SoundSource.PLAYERS, 1.0f, 1.0f); + } + } + + // TODO: Update ClientChatEvent for NeoForge 1.21.1 + // @SubscribeEvent + // public void onClientChat(ClientChatEvent ev) { + // if(ev.getMessage().equals("!WD render recipes")) + // PROXY.renderRecipes(); + // } + + private boolean hasPlayerAdvancement(ServerPlayer ply, ResourceLocation rl) { + MinecraftServer server = PROXY.getServer(); + if(server == null) + return false; + + AdvancementHolder adv = server.getAdvancements().get(rl); + return adv != null && ply.getAdvancements().getOrStartProgress(adv).isDone(); + } + + public static int getNextAvailablePadID() { + return new WebDisplays().lastPadId++; + } + + public static DeferredRegister SOUNDS = DeferredRegister.create(Registries.SOUND_EVENT, "webdisplays"); + + private static SoundEvent registerSound(String resName) { + ResourceLocation resLoc = ResourceLocation.fromNamespaceAndPath("webdisplays", resName); + SoundEvent ret = SoundEvent.createVariableRangeEvent(resLoc); + + SOUNDS.register(resName, () -> ret); + return ret; + } + + // Removed unused helper for older advancement API registration + + // public static boolean isOpenComputersAvailable() { + // return INSTANCE.hasOC; + // } + + // public static boolean isComputerCraftAvailable() { + // return INSTANCE.hasCC; + // } + + public static boolean isSiteBlacklisted(String url) { + try { + URL url2 = new URL(Util.addProtocol(url)); + for (String str : CommonConfig.Browser.blacklist) + if (str.equalsIgnoreCase(url2.getHost())) return true; + return false; + } catch(MalformedURLException ex) { + return false; + } + } + + public static String applyBlacklist(String url) { + return isSiteBlacklisted(url) ? BLACKLIST_URL : url; + } +} + diff --git a/src/main/java/net/montoyo/wd/block/KeyboardBlockLeft.java b/src/main/java/net/montoyo/wd/block/KeyboardBlockLeft.java new file mode 100644 index 0000000..5adba51 --- /dev/null +++ b/src/main/java/net/montoyo/wd/block/KeyboardBlockLeft.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd.block; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.neoforged.neoforge.network.PacketDistributor; +import net.montoyo.wd.core.DefaultPeripheral; +import net.montoyo.wd.entity.KeyboardBlockEntity; +import net.montoyo.wd.item.ItemLinker; +import net.montoyo.wd.net.WDNetworkRegistry; +import net.montoyo.wd.net.client_bound.S2CMessageCloseGui; +import org.jetbrains.annotations.NotNull; + +public class KeyboardBlockLeft extends PeripheralBlock { + public static final EnumProperty TYPE = EnumProperty.create("type", DefaultPeripheral.class); + public static final DirectionProperty FACING = DirectionProperty.create("facing", Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST); +// public static final DirectionProperty HALF = DirectionProperty.create("facing", Direction.EAST, Direction.WEST); + + public static final VoxelShape[] KEYBOARD_AABBS = new VoxelShape[]{ + Shapes.box(0.0, 0.0, 3.0 / 16, 1.0, 1.0 / 16.0, 1.0), + Shapes.box(0.0, 0.0, 0.0, 1.0, 1.0 / 16.0, 13 / 16.0), + Shapes.box(3.0 / 16, 0.0, 0.0, 1.0, 1.0 / 16.0, 1.0), + Shapes.box(0.0, 0.0, 0.0, 13 / 16.0, 1.0 / 16.0, 1.0), + }; + + private static final Property[] properties = new Property[] {TYPE, FACING}; + + public KeyboardBlockLeft() { + super(DefaultPeripheral.KEYBOARD); + } + + // TODO: make non static (for extensibility purposes) + public static KeyboardBlockEntity getTileEntity(BlockState state, Level world, BlockPos pos) { + if (state.getBlock() instanceof KeyboardBlockLeft) { + BlockEntity te = world.getBlockEntity(pos); // TODO: check? + if (te instanceof KeyboardBlockEntity) + return (KeyboardBlockEntity) te; + } + + BlockPos relative = pos.relative(KeyboardBlockLeft.mapDirection(state.getValue(FACING).getOpposite())); + BlockState ns = world.getBlockState(relative); + + if (ns.getBlock() instanceof PeripheralBlock) { + BlockEntity te = world.getBlockEntity(relative); // TODO: check? + if (te instanceof KeyboardBlockEntity) + return (KeyboardBlockEntity) te; + } + + return null; + } + + public static Direction mapDirection(Direction facing) { + return switch (facing) { + case NORTH -> Direction.EAST; + case EAST -> Direction.SOUTH; + case SOUTH -> Direction.WEST; + case WEST -> Direction.NORTH; + default -> facing; + }; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(properties); + } + + @Override + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + double rpos = (entity.getY() - ((double) pos.getY())) * 16.0; + if (!world.isClientSide && rpos >= 1.0 && rpos <= 2.0 && Math.random() < 0.25) { + KeyboardBlockEntity tek = KeyboardBlockLeft.getTileEntity(state, world, pos); + + if (tek != null) + tek.simulateCat(entity); + } + } + + @Override + protected @NotNull InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { + // Note: useWithoutItem doesn't have InteractionHand parameter, so we use MAIN_HAND as default + InteractionHand hand = InteractionHand.MAIN_HAND; + + if (player.getItemInHand(hand).getItem() instanceof ItemLinker) + return InteractionResult.PASS; + + KeyboardBlockEntity tek = KeyboardBlockLeft.getTileEntity(state, level, pos); + if (tek != null) + return tek.onRightClick(player, hand); + + return InteractionResult.PASS; + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return KEYBOARD_AABBS[state.getValue(FACING).ordinal() - 2]; + } + + @Override + public VoxelShape getOcclusionShape(BlockState arg, BlockGetter arg2, BlockPos arg3) { + return Shapes.empty(); + } + + private static void removeRightPiece(BlockState state, Level world, BlockPos pos) { + BlockPos relative = pos.relative(KeyboardBlockLeft.mapDirection(state.getValue(FACING))); + + BlockState ns = world.getBlockState(relative); + if (ns.getBlock() instanceof KeyboardBlockRight) + world.setBlock(relative, Blocks.AIR.defaultBlockState(), 3); + } + + public static void remove(BlockState state, Level world, BlockPos pos, boolean setState, boolean drop) { + removeRightPiece(state, world, pos); + if (setState) + world.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); + // TODO: Rewrite for new networking system + // WDNetworkRegistry.INSTANCE.send(PacketDistributor.NEAR.with(() -> point(world, pos)), new S2CMessageCloseGui(pos)); + } + + @Override + public void onRemove(BlockState arg, Level arg2, BlockPos arg3, BlockState arg4, boolean bl) { + if (!arg2.isClientSide) + remove(arg, arg2, arg3, false, false); + super.onRemove(arg, arg2, arg3, arg4, bl); + } +} diff --git a/src/main/java/net/montoyo/wd/block/KeyboardBlockRight.java b/src/main/java/net/montoyo/wd/block/KeyboardBlockRight.java new file mode 100644 index 0000000..b5bc5ff --- /dev/null +++ b/src/main/java/net/montoyo/wd/block/KeyboardBlockRight.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.block; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.neoforged.neoforge.network.PacketDistributor; +import net.montoyo.wd.core.IPeripheral; +import net.montoyo.wd.entity.KeyboardBlockEntity; +import net.montoyo.wd.item.ItemLinker; +import net.montoyo.wd.net.WDNetworkRegistry; +import net.montoyo.wd.net.client_bound.S2CMessageCloseGui; +import net.montoyo.wd.utilities.data.BlockSide; +import net.montoyo.wd.utilities.math.Vector3i; +import org.jetbrains.annotations.NotNull; + +import static net.montoyo.wd.block.KeyboardBlockLeft.KEYBOARD_AABBS; + +// TODO: merge into KeyboardLeft +public class KeyboardBlockRight extends Block implements IPeripheral { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + + public KeyboardBlockRight() { + super(Properties.ofFullCopy(Blocks.STONE) + .strength(1.5f, 10.f)); + } + + private static void removeLeftPiece(BlockState state, Level world, BlockPos pos) { + BlockPos relative = pos.relative(KeyboardBlockLeft.mapDirection(state.getValue(FACING).getOpposite())); + + BlockState ns = world.getBlockState(relative); + if (ns.getBlock() instanceof KeyboardBlockLeft) + world.setBlock(relative, Blocks.AIR.defaultBlockState(), 3); + } + + public static void remove(BlockState state, Level world, BlockPos pos, boolean setState, boolean drop) { + removeLeftPiece(state, world, pos); + if (setState) + world.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); + // TODO: Rewrite for new networking system + // WDNetworkRegistry.INSTANCE.send(PacketDistributor.NEAR.with(() -> point(world, pos)), new S2CMessageCloseGui(pos)); + } + + @Override + public void onRemove(BlockState arg, Level arg2, BlockPos arg3, BlockState arg4, boolean bl) { + if (!arg2.isClientSide) + remove(arg, arg2, arg3, false, false); + super.onRemove(arg, arg2, arg3, arg4, bl); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } + + @Override + public boolean isCollisionShapeFullBlock(BlockState state, BlockGetter level, BlockPos pos) { + return false; + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return KEYBOARD_AABBS[state.getValue(FACING).ordinal() - 2]; + } + + @Override + public boolean connect(Level world, BlockPos pos, BlockState state, Vector3i scrPos, BlockSide scrSide) { + KeyboardBlockEntity keyboard = KeyboardBlockLeft.getTileEntity(state, world, pos); + return keyboard != null && keyboard.connect(world, pos, state, scrPos, scrSide); + } + + @Override + public void entityInside(BlockState state, Level world, BlockPos pos, Entity entity) { + double rpos = (entity.getY() - ((double) pos.getY())) * 16.0; + if (!world.isClientSide && rpos >= 1.0 && rpos <= 2.0 && Math.random() < 0.25) { + KeyboardBlockEntity tek = KeyboardBlockLeft.getTileEntity(state, world, pos); + + if (tek != null) + tek.simulateCat(entity); + } + } + + @Override + protected @NotNull InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { + // Note: useWithoutItem doesn't have InteractionHand parameter, so we use MAIN_HAND as default + InteractionHand hand = InteractionHand.MAIN_HAND; + + if (player.getItemInHand(hand).getItem() instanceof ItemLinker) + return InteractionResult.PASS; + + KeyboardBlockEntity tek = KeyboardBlockLeft.getTileEntity(state, level, pos); + if (tek != null) + return tek.onRightClick(player, hand); + + return InteractionResult.PASS; + } + + @Override + public VoxelShape getOcclusionShape(BlockState arg, BlockGetter arg2, BlockPos arg3) { + return Shapes.empty(); + } +} diff --git a/src/main/java/net/montoyo/wd/block/PeripheralBlock.java b/src/main/java/net/montoyo/wd/block/PeripheralBlock.java new file mode 100644 index 0000000..a32faae --- /dev/null +++ b/src/main/java/net/montoyo/wd/block/PeripheralBlock.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd.block; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.PushReaction; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import net.neoforged.neoforge.network.PacketDistributor; +import net.montoyo.wd.core.DefaultPeripheral; +import net.montoyo.wd.entity.AbstractInterfaceBlockEntity; +import net.montoyo.wd.entity.AbstractPeripheralBlockEntity; +import net.montoyo.wd.entity.ServerBlockEntity; +import net.montoyo.wd.item.ItemLinker; +import net.montoyo.wd.net.WDNetworkRegistry; +import net.montoyo.wd.net.client_bound.S2CMessageCloseGui; +import net.montoyo.wd.utilities.Log; +import org.jetbrains.annotations.Nullable; + +public class PeripheralBlock extends WDContainerBlock { + public static final MapCodec CODEC = simpleCodec(properties -> new PeripheralBlock(DefaultPeripheral.KEYBOARD)); + DefaultPeripheral type; + + public PeripheralBlock(DefaultPeripheral type) { + super(BlockBehaviour.Properties.ofFullCopy(Blocks.STONE).strength(1.5f, 10.f)); + this.type = type; + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + BlockEntityType.BlockEntitySupplier cls = type.getTEClass(); + if (cls == null) + return null; + + try { + return cls.create(pos, state); + } catch (Throwable t) { + Log.errorEx("Couldn't instantiate peripheral TileEntity:", t); + } + + return null; + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { + if (player.isShiftKeyDown()) + return InteractionResult.FAIL; + + // Note: useWithoutItem doesn't have InteractionHand parameter, so we use MAIN_HAND as default + InteractionHand hand = InteractionHand.MAIN_HAND; + + if (player.getItemInHand(hand).getItem() instanceof ItemLinker) + return InteractionResult.FAIL; + + BlockEntity te = level.getBlockEntity(pos); + + if (te instanceof AbstractPeripheralBlockEntity) + return ((AbstractPeripheralBlockEntity) te).onRightClick(player, hand); + else if (te instanceof ServerBlockEntity) { + ((ServerBlockEntity) te).onPlayerRightClick(player); + return InteractionResult.SUCCESS; + } else + return InteractionResult.FAIL; + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return Shapes.block(); + } + + @Override + public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) { + if (world.isClientSide) + return; + + if (placer instanceof Player) { + BlockEntity te = world.getBlockEntity(pos); + + if (te instanceof ServerBlockEntity) + ((ServerBlockEntity) te).setOwner((Player) placer); + else if (te instanceof AbstractInterfaceBlockEntity) + ((AbstractInterfaceBlockEntity) te).setOwner((Player) placer); + } + } + + @Override + public PushReaction getPistonPushReaction(BlockState state) { + return PushReaction.IGNORE; + } + + @Override + public void neighborChanged(BlockState state, Level world, BlockPos pos, Block neighborType, BlockPos neighbor, boolean isMoving) { + BlockEntity te = world.getBlockEntity(pos); + if (te instanceof AbstractPeripheralBlockEntity) + ((AbstractPeripheralBlockEntity) te).onNeighborChange(neighborType, neighbor); + } + + @Override + public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack tool) { + if (!world.isClientSide) { + // TODO: Rewrite for new networking system + // WDNetworkRegistry.INSTANCE.send(PacketDistributor.NEAR.with(() -> point(world, pos)), new S2CMessageCloseGui(pos)); + } + super.playerDestroy(world, player, pos, state, blockEntity, tool); + } + + @Override + public void onBlockExploded(BlockState state, Level level, BlockPos pos, Explosion explosion) { + playerDestroy(level, null, pos, level.getBlockState(pos), null, null); + } + + // Helper method to send packets to players near a position + public static void sendToPlayersNear(Level world, BlockPos bp, CustomPacketPayload payload) { + if (world instanceof ServerLevel serverLevel) { + PacketDistributor.sendToPlayersNear(serverLevel, null, bp.getX(), bp.getY(), bp.getZ(), 64.0, payload); + } + } + + public static void sendToPlayersNear(Player exclude, Level world, BlockPos bp, CustomPacketPayload payload) { + if (world instanceof ServerLevel serverLevel) { + PacketDistributor.sendToPlayersNear(serverLevel, (ServerPlayer) exclude, bp.getX(), bp.getY(), bp.getZ(), 64.0, payload); + } + } + +} diff --git a/src/main/java/net/montoyo/wd/block/ScreenBlock.java b/src/main/java/net/montoyo/wd/block/ScreenBlock.java new file mode 100644 index 0000000..3caaa8a --- /dev/null +++ b/src/main/java/net/montoyo/wd/block/ScreenBlock.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.block; + +import com.mojang.serialization.MapCodec; +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.PushReaction; +import net.minecraft.world.phys.BlockHitResult; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.config.CommonConfig; +import net.montoyo.wd.core.DefaultUpgrade; +import net.montoyo.wd.core.IUpgrade; +import net.montoyo.wd.core.ScreenRights; +import net.montoyo.wd.data.SetURLData; +import net.montoyo.wd.entity.ScreenData; +import net.montoyo.wd.entity.ScreenBlockEntity; +import net.montoyo.wd.item.ItemLaserPointer; +import net.montoyo.wd.utilities.*; +import net.montoyo.wd.utilities.math.Vector2i; +import net.montoyo.wd.utilities.math.Vector3f; +import net.montoyo.wd.utilities.math.Vector3i; +import net.montoyo.wd.utilities.data.BlockSide; +import net.montoyo.wd.utilities.serialization.Util; +import org.jetbrains.annotations.NotNull; + +public class ScreenBlock extends BaseEntityBlock { + public static final MapCodec CODEC = simpleCodec(ScreenBlock::new); + public static final BooleanProperty hasTE = BooleanProperty.create("haste"); + public static final BooleanProperty emitting = BooleanProperty.create("emitting"); + private static final Property[] properties = new Property[]{hasTE, emitting}; + + public ScreenBlock(Properties properties) { + super(properties.strength(1.5f, 10.f)); + this.registerDefaultState(this.defaultBlockState().setValue(hasTE, false).setValue(emitting, false)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + public void onRemove(BlockState p_60515_, Level p_60516_, BlockPos p_60517_, BlockState p_60518_, boolean p_60519_) { + // TODO: make this also get called on client? + if (p_60518_.getBlock() == p_60515_.getBlock()) return; + + for (BlockSide value : BlockSide.values()) { + Vector3i vec = new Vector3i(p_60517_.getX(), p_60517_.getY(), p_60517_.getZ()); + Multiblock.findOrigin(p_60516_, vec, value, null); + BlockPos bp = new BlockPos(vec.x, vec.y, vec.z); + if (!bp.equals(p_60517_)) { + p_60516_.removeBlockEntity(bp); + p_60516_.setBlock( + bp, p_60516_.getBlockState(bp).setValue(hasTE, false), + 11 + ); + } + } + + super.onRemove(p_60515_, p_60516_, p_60517_, p_60518_, p_60519_); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { + // Note: useWithoutItem doesn't have InteractionHand parameter, so we use MAIN_HAND as default + InteractionHand hand = InteractionHand.MAIN_HAND; + + ItemStack heldItem = player.getItemInHand(hand); + boolean isUpgrade = false; + if (heldItem.isEmpty()) + heldItem = null; //Easier to work with + else if (!(isUpgrade = heldItem.getItem() instanceof IUpgrade)) + return InteractionResult.FAIL; + else if (heldItem.getItem() instanceof ItemLaserPointer) + return InteractionResult.FAIL; // laser pointer already handles stuff + + // handling the off hand leads to double clicking + if (!isUpgrade && hand == InteractionHand.OFF_HAND) + return InteractionResult.FAIL; + + if (level.isClientSide) + return InteractionResult.FAIL; + + boolean sneaking = player.isShiftKeyDown(); + Vector3i screenPos = new Vector3i(pos); + + BlockSide side = BlockSide.values()[hitResult.getDirection().ordinal()]; + + Multiblock.findOrigin(level, screenPos, side, null); + ScreenBlockEntity te = (ScreenBlockEntity) level.getBlockEntity(screenPos.toBlock()); + + if (te != null && te.getScreen(side) != null) { + ScreenData scr = te.getScreen(side); + + if (sneaking) { //Right Click + if ((scr.rightsFor(player) & ScreenRights.CHANGE_URL) == 0) + Util.toast(player, "restrictions"); + else + (new SetURLData(screenPos, scr.side, scr.url)).sendTo((ServerPlayer) player); + + return InteractionResult.SUCCESS; + } else if (heldItem != null) { + if (!te.hasUpgrade(side, heldItem)) { + if ((scr.rightsFor(player) & ScreenRights.MANAGE_UPGRADES) == 0) { + Util.toast(player, "restrictions"); + return InteractionResult.CONSUME; + } + + if (te.addUpgrade(side, heldItem, player, false)) { + if (!player.isCreative()) + heldItem.shrink(1); + + Util.toast(player, ChatFormatting.AQUA, "upgradeOk"); + if (player instanceof ServerPlayer) + WebDisplays.INSTANCE.criterionUpgradeScreen.trigger((ServerPlayer) player); + } else + Util.toast(player, "upgradeError"); + + return InteractionResult.CONSUME; + } + } else { + if ((scr.rightsFor(player) & ScreenRights.INTERACT) == 0) { + Util.toast(player, "restrictions"); + return InteractionResult.CONSUME; + } + + Vector2i tmp = new Vector2i(); + + float hitX = ((float) hitResult.getLocation().x) - (float) te.getBlockPos().getX(); + float hitY = ((float) hitResult.getLocation().y) - (float) te.getBlockPos().getY(); + float hitZ = ((float) hitResult.getLocation().z) - (float) te.getBlockPos().getZ(); + + if (hit2pixels(side, hitResult.getBlockPos(), new Vector3i(hitResult.getBlockPos()), scr, hitX, hitY, hitZ, tmp)) + te.click(side, tmp); + return InteractionResult.CONSUME; + } + } +// else if(sneaking) { +// Util.toast(player, "turnOn"); +// return InteractionResult.SUCCESS; +// } + + Vector2i size = Multiblock.measure(level, screenPos, side); + if (size.x < 2 && size.y < 2) { + Util.toast(player, "tooSmall"); + return InteractionResult.SUCCESS; + } + + if (size.x > CommonConfig.Screen.maxScreenSizeX || size.y > CommonConfig.Screen.maxScreenSizeY) { + Util.toast(player, "tooBig", CommonConfig.Screen.maxScreenSizeX, CommonConfig.Screen.maxScreenSizeY); + return InteractionResult.SUCCESS; + } + + Vector3i err = Multiblock.check(level, screenPos, size, side); + if (err != null) { + Util.toast(player, "invalid", err.toString()); + return InteractionResult.SUCCESS; + } + + boolean created = false; + Log.info("Player %s (UUID %s) created a screen at %s of size %dx%d", player.getName(), player.getGameProfile().getId().toString(), screenPos.toString(), size.x, size.y); + + if (te == null) { + BlockPos bp = screenPos.toBlock(); + level.setBlockAndUpdate(bp, level.getBlockState(bp).setValue(hasTE, true)); + te = (ScreenBlockEntity) level.getBlockEntity(bp); + created = true; + } + + te.addScreen(side, size, null, player, true); + return InteractionResult.SUCCESS; + } + + @Override + public void neighborChanged(BlockState state, Level world, BlockPos pos, Block block, BlockPos source, + boolean isMoving) { + if (block != this && !world.isClientSide && !state.getValue(emitting)) { + for (BlockSide side : BlockSide.values()) { + Vector3i vec = new Vector3i(pos); + Multiblock.findOrigin(world, vec, side, null); + + ScreenBlockEntity tes = (ScreenBlockEntity) world.getBlockEntity(vec.toBlock()); + if (tes != null && tes.hasUpgrade(side, DefaultUpgrade.REDINPUT)) { + Direction facing = Direction.from2DDataValue(side.reverse().ordinal()); //Opposite face + vec.sub(pos.getX(), pos.getY(), pos.getZ()).neg(); +// tes.updateJSRedstone(side, new Vector2i(vec.dot(side.right), vec.dot(side.up)), world.getSignal(pos, facing)); + } + } + } + } + + public static boolean hit2pixels(BlockSide side, BlockPos bpos, Vector3i pos, ScreenData scr, float hitX, float hitY, float hitZ, Vector2i dst) { + if(side.right.x < 0) + hitX -= 1.f; + + if(side.right.z < 0 || side == BlockSide.TOP || side == BlockSide.BOTTOM) + hitZ -= 1.f; + + Vector3f rel = new Vector3f(hitX, hitY, hitZ); + + // this dot is acting as a "get distance from plane" where the plane is the edge of the screen + float cx = rel.dot(side.right.toFloat()) - 2.f / 16.f; + float cy = rel.dot(side.up.toFloat()) - 2.f / 16.f; + float sw = ((float) scr.size.x) - 4.f / 16.f; + float sh = ((float) scr.size.y) - 4.f / 16.f; + + cx /= sw; + cy /= sh; + + if (cx >= 0.f && cx <= 1.0 && cy >= 0.f && cy <= 1.f) { + if (side != BlockSide.BOTTOM) + cy = 1.f - cy; + + switch (scr.rotation) { + case ROT_90: + cy = 1.0f - cy; + break; + + case ROT_180: + cx = 1.0f - cx; + cy = 1.0f - cy; + break; + + case ROT_270: + cx = 1.0f - cx; + break; + } + + cx *= (float) scr.resolution.x; + cy *= (float) scr.resolution.y; + + if (scr.rotation.isVertical) { + dst.x = (int) cy; + dst.y = (int) cx; + } else { + dst.x = (int) cx; + dst.y = (int) cy; + } + + return true; + } + + return false; + } + + /************************************************* DESTRUCTION HANDLING *************************************************/ + + private void onDestroy(Level world, BlockPos pos, Player ply) { + if (!world.isClientSide) { + Vector3i bp = new Vector3i(pos); + Multiblock.BlockOverride override = new Multiblock.BlockOverride(bp, Multiblock.OverrideAction.SIMULATE); + + for (BlockSide bs : BlockSide.values()) + destroySide(world, bp.clone(), bs, override, ply); + } + } + + private void destroySide(Level world, Vector3i pos, BlockSide side, Multiblock.BlockOverride override, Player + source) { + Multiblock.findOrigin(world, pos, side, override); + BlockPos bp = pos.toBlock(); + BlockEntity te = world.getBlockEntity(bp); + + if (te instanceof ScreenBlockEntity) { + ((ScreenBlockEntity) te).onDestroy(source); + world.setBlock(bp, world.getBlockState(bp).setValue(hasTE, false), Block.UPDATE_ALL_IMMEDIATE); //Destroy tile entity. + } + } + + @Override + public boolean onDestroyedByPlayer(BlockState state, Level level, BlockPos pos, Player player, + boolean willHarvest, FluidState fluid) { + onDestroy(level, pos, player); + return super.onDestroyedByPlayer(state, level, pos, player, willHarvest, fluid); + } + + @Override + public void setPlacedBy(Level world, @NotNull BlockPos pos, @NotNull BlockState + state, @org.jetbrains.annotations.Nullable LivingEntity whoDidThisShit, @NotNull ItemStack stack) { + if (world.isClientSide) + return; + + Multiblock.BlockOverride override = new Multiblock.BlockOverride(new Vector3i(pos), Multiblock.OverrideAction.IGNORE); + Vector3i[] neighbors = new Vector3i[6]; + + neighbors[0] = new Vector3i(pos.getX() + 1, pos.getY(), pos.getZ()); + neighbors[1] = new Vector3i(pos.getX() - 1, pos.getY(), pos.getZ()); + neighbors[2] = new Vector3i(pos.getX(), pos.getY() + 1, pos.getZ()); + neighbors[3] = new Vector3i(pos.getX(), pos.getY() - 1, pos.getZ()); + neighbors[4] = new Vector3i(pos.getX(), pos.getY(), pos.getZ() + 1); + neighbors[5] = new Vector3i(pos.getX(), pos.getY(), pos.getZ() - 1); + + for (Vector3i neighbor : neighbors) { + if (world.getBlockState(neighbor.toBlock()).getBlock() instanceof ScreenBlock) { + for (BlockSide bs : BlockSide.values()) + destroySide(world, neighbor.clone(), bs, override, (whoDidThisShit instanceof Player) ? ((Player) whoDidThisShit) : null); + } + } + } + + /************************************************* STUFF THAT'S UNLIKELY TO BE TOUCHED BUT NEEDS TO BE HERE *************************************************/ + + @Override + public @NotNull PushReaction getPistonPushReaction(BlockState state) { + return PushReaction.IGNORE; + } + + @Override + public int getSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) { + return state.getValue(emitting) ? 15 : 0; + } + + @Override + public boolean isSignalSource(BlockState state) { + return state.getValue(emitting); + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return state.getValue(hasTE) ? new ScreenBlockEntity(pos, state) : null; + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(properties); + } +} diff --git a/src/main/java/net/montoyo/wd/block/WDContainerBlock.java b/src/main/java/net/montoyo/wd/block/WDContainerBlock.java new file mode 100644 index 0000000..6c7dc55 --- /dev/null +++ b/src/main/java/net/montoyo/wd/block/WDContainerBlock.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.block; + +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.level.block.BaseEntityBlock; + +public abstract class WDContainerBlock extends BaseEntityBlock { + + protected static BlockItem itemBlock; + + public WDContainerBlock(Properties arg) { + super(arg); + } + + public BlockItem getItem() { + return itemBlock; + } + +} diff --git a/src/main/java/net/montoyo/wd/block/item/KeyboardItem.java b/src/main/java/net/montoyo/wd/block/item/KeyboardItem.java new file mode 100644 index 0000000..ad776e0 --- /dev/null +++ b/src/main/java/net/montoyo/wd/block/item/KeyboardItem.java @@ -0,0 +1,54 @@ +package net.montoyo.wd.block.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.montoyo.wd.block.KeyboardBlockLeft; +import net.montoyo.wd.registry.BlockRegistry; + +public class KeyboardItem extends BlockItem { + public KeyboardItem(Block arg, Properties arg2) { + super(arg, arg2); + } + + @Override + protected boolean placeBlock(BlockPlaceContext arg, BlockState arg2) { + Direction facing = arg.getHorizontalDirection(); + arg2 = arg2.setValue(KeyboardBlockLeft.FACING, facing); + + Direction d = KeyboardBlockLeft.mapDirection(facing); + + if (isValid(arg.getClickedPos(), arg.getLevel(), arg2, d)) { + Block kbRight = BlockRegistry.blockKbRight.get(); + BlockState rightState = kbRight.defaultBlockState(); + + rightState = rightState.setValue(KeyboardBlockLeft.FACING, facing); + if (!arg.getLevel().setBlock( + arg.getClickedPos().relative(d), + rightState, + 11 + )) return false; + return arg.getLevel().setBlock(arg.getClickedPos(), arg2, 11);// 161 + } else if (isValid(arg.getClickedPos().relative(d.getOpposite(), 2), arg.getLevel(), arg2, d)) { + Block kbRight = BlockRegistry.blockKbRight.get(); + BlockState rightState = kbRight.defaultBlockState(); + + rightState = rightState.setValue(KeyboardBlockLeft.FACING, facing); + if (!arg.getLevel().setBlock( + arg.getClickedPos(), + rightState, + 11 + )) return false; + return arg.getLevel().setBlock(arg.getClickedPos().relative(d.getOpposite()), arg2, 11);// 161 + } + return false; + } + + private boolean isValid(BlockPos pos, Level level, BlockState state, Direction d) { + return level.getBlockState(pos.relative(d)).isAir(); + } +} diff --git a/src/main/java/net/montoyo/wd/client/ClientProxy.java b/src/main/java/net/montoyo/wd/client/ClientProxy.java new file mode 100644 index 0000000..b99db4d --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/ClientProxy.java @@ -0,0 +1,862 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client; + +import com.cinemamod.mcef.MCEF; +import com.cinemamod.mcef.MCEFBrowser; +import com.mojang.authlib.GameProfile; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.advancements.Advancement; +import net.minecraft.advancements.AdvancementHolder; +import net.minecraft.advancements.AdvancementProgress; +import net.minecraft.client.Camera; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Options; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.multiplayer.ClientAdvancements; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; +import net.minecraft.core.BlockPos; +import net.minecraft.core.NonNullList; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.packs.resources.ReloadableResourceManager; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.HumanoidArm; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.neoforge.client.event.ModelEvent; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.RenderHandEvent; +import net.neoforged.neoforge.client.event.RenderHighlightEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.tick.LevelTickEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.event.level.LevelEvent; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.LogicalSide; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import net.montoyo.wd.SharedProxy; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.block.ScreenBlock; +import net.montoyo.wd.client.gui.*; +import net.montoyo.wd.client.gui.loading.GuiLoader; +import net.montoyo.wd.client.renderers.*; +import net.montoyo.wd.core.HasAdvancement; +import net.montoyo.wd.data.GuiData; +import net.montoyo.wd.entity.ScreenBlockEntity; +import net.montoyo.wd.entity.ScreenData; +import net.montoyo.wd.item.ItemLaserPointer; +import net.montoyo.wd.item.ItemMinePad2; +import net.montoyo.wd.item.WDItem; +import net.montoyo.wd.miniserv.client.Client; +import net.montoyo.wd.registry.BlockRegistry; +import net.montoyo.wd.registry.ItemRegistry; +import net.montoyo.wd.registry.TileRegistry; +import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.utilities.Multiblock; +import net.montoyo.wd.utilities.browser.WDBrowser; +import net.montoyo.wd.utilities.browser.handlers.DisplayHandler; +import net.montoyo.wd.utilities.browser.handlers.WDRouter; +import net.montoyo.wd.utilities.data.BlockSide; +import net.montoyo.wd.utilities.data.Rotation; +import net.montoyo.wd.utilities.math.Vector2i; +import net.montoyo.wd.utilities.math.Vector3i; +import net.montoyo.wd.utilities.serialization.NameUUIDPair; +import net.montoyo.wd.data.WDDataComponents; +import org.cef.browser.CefBrowser; +import org.cef.browser.CefMessageRouter; +import org.cef.misc.CefCursorType; +import org.lwjgl.glfw.GLFW; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.*; + +public class ClientProxy extends SharedProxy implements ResourceManagerReloadListener { + + private static ClientProxy INSTANCE; + + public ClientProxy() { + INSTANCE = this; + } + + public static void renderCrosshair(Options options, int screenWidth, int screenHeight, int offset, GuiGraphics poseStack, CallbackInfo ci) { + ItemStack stack = Minecraft.getInstance().player.getItemInHand(InteractionHand.MAIN_HAND); + ItemStack stack1 = Minecraft.getInstance().player.getItemInHand(InteractionHand.OFF_HAND); + + if (stack.getItem() instanceof ItemMinePad2) { + float sign = 1; + if (Minecraft.getInstance().player.getMainArm() == HumanoidArm.LEFT) sign = -1; + if (!MinePadRenderer.renderAtSide(sign)) { + ci.cancel(); + return; + } + } else { + if (stack1.getItem() instanceof ItemMinePad2) { + float sign = -1; + if (Minecraft.getInstance().player.getMainArm() == HumanoidArm.LEFT) sign = 1; + if (!MinePadRenderer.renderAtSide(sign)) { + ci.cancel(); + return; + } + } + } + + if (!(stack.getItem() instanceof ItemLaserPointer || + stack1.getItem() instanceof ItemLaserPointer)) + return; + + if (!LaserPointerRenderer.isOn()) { + RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.ONE_MINUS_DST_COLOR, GlStateManager.DestFactor.ONE_MINUS_SRC_COLOR, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + + poseStack.blit(ResourceLocation.fromNamespaceAndPath( + "webdisplays", "textures/gui/cursors.png" + ), (screenWidth - 15) / 2, (screenHeight - 15) / 2, offset, 240, 240, 15, 15, 256, 256); + ci.cancel(); + return; + } + + Minecraft mc = Minecraft.getInstance(); + + BlockHitResult result = raycast(64.0); //TODO: Make that distance configurable + + BlockPos bpos = result.getBlockPos(); + + if (result.getType() != HitResult.Type.BLOCK || mc.level.getBlockState(bpos).getBlock() != BlockRegistry.SCREEN_BLOCk.get()) { + RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.ONE_MINUS_DST_COLOR, GlStateManager.DestFactor.ONE_MINUS_SRC_COLOR, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + + poseStack.blit(ResourceLocation.fromNamespaceAndPath( + "webdisplays", "textures/gui/cursors.png" + ), (screenWidth - 15) / 2, (screenHeight - 15) / 2, offset, 240, 240, 15, 15, 256, 256); + ci.cancel(); + return; + } + + Vector3i pos = new Vector3i(result.getBlockPos()); + BlockSide side = BlockSide.values()[result.getDirection().ordinal()]; + + Multiblock.findOrigin(mc.level, pos, side, null); + ScreenBlockEntity te = (ScreenBlockEntity) mc.level.getBlockEntity(pos.toBlock()); + + ScreenData sc = te.getScreen(side); + + if (sc == null) return; + + int coordX = sc.mouseType * 15; + int coordY = coordX / 255; + coordX -= coordY * 255; + coordY *= 15; + // for some reason, the cursor gets offset at this value + if (sc.mouseType >= CefCursorType.NOT_ALLOWED.ordinal()) coordX -= 15; + + RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.ONE_MINUS_DST_COLOR, GlStateManager.DestFactor.ONE_MINUS_SRC_COLOR, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + + poseStack.blit(ResourceLocation.fromNamespaceAndPath( + "webdisplays", "textures/gui/cursors.png" + ), (screenWidth - 15) / 2, (screenHeight - 15) / 2, offset, coordX, coordY, 15, 15, 256, 256); + + ci.cancel(); + } + + public List getScreens() { + return screenTracking; + } + + public List getPads() { + return padList; + } + + public class PadData { + + public CefBrowser view; + public final UUID id; + private boolean isInHotbar; + private long lastURLSent; + + public int activeCursor; + + private PadData(String url, UUID id) { + String webUrl; + try { + webUrl = ScreenBlockEntity.url(url); + } catch (IOException e) { + throw new RuntimeException(e); + } + view = WDBrowser.createBrowser(WebDisplays.applyBlacklist(webUrl), false); + if (view instanceof MCEFBrowser browser) { + browser.resize((int) WebDisplays.INSTANCE.padResX, (int) WebDisplays.INSTANCE.padResY); + browser.setCursorChangeListener((cursor) -> { + activeCursor = cursor; + }); + } + isInHotbar = true; + this.id = id; + } + + public void updateTime() { + lastURLSent = System.currentTimeMillis(); + } + + public long lastSent() { + return lastURLSent; + } + } + + private Minecraft mc; + private MinePadRenderer minePadRenderer; + private LaserPointerRenderer laserPointerRenderer; + private Screen nextScreen; + private boolean isF1Down; + + //Miniserv handling + private int miniservPort; + private boolean msClientStarted; + + //Client-side advancement hack + private final Field advancementToProgressField = findAdvancementToProgressField(); + private ClientAdvancements lastAdvMgr; + private Map advancementToProgress; + + //Tracking + private final ArrayList screenTracking = new ArrayList<>(); + private int lastTracked = 0; + + //MinePads Management + private final HashMap padMap = new HashMap<>(); + private final ArrayList padList = new ArrayList<>(); + private int minePadTickCounter = 0; + + /**************************************** INHERITED METHODS ****************************************/ + public static void onClientSetup(FMLClientSetupEvent event) { + BlockEntityRenderers.register(TileRegistry.SCREEN_BLOCK_ENTITY.get(), new ScreenRenderer.ScreenRendererProvider()); + } + + public static void onModelRegistryEvent(ModelEvent.RegisterGeometryLoaders event) { + event.register(ResourceLocation.fromNamespaceAndPath("webdisplays", "screen_loader"), new ScreenModelLoader()); + } + + @Override + public void preInit() { + super.preInit(); + mc = Minecraft.getInstance(); + NeoForge.EVENT_BUS.register(this); + } + + @Override + public void onCefInit() { + minePadRenderer = new MinePadRenderer(); + laserPointerRenderer = new LaserPointerRenderer(); + + if (!MCEF.isInitialized()) return; + + MCEF.getApp().getHandle().registerSchemeHandlerFactory( + "webdisplays", "", + (browser, frame, url, request) -> { + // TODO: check if it's a webdisplays browser? + return new WDScheme(request.getURL()); + } + ); + + MCEF.getClient().addDisplayHandler(DisplayHandler.INSTANCE); + MCEF.getClient().getHandle().addMessageRouter(CefMessageRouter.create(WDRouter.INSTANCE)); + + findAdvancementToProgressField(); + } + + @Override + public void postInit() { + ((ReloadableResourceManager) mc.getResourceManager()).registerReloadListener(this); + } + + @Override + public Level getWorld(ResourceKey dim) { + Level ret = mc.level; +// if(dim == CURRENT_DIMENSION) +// return ret; + if (ret != null) { + if (!ret.dimension().equals(dim)) + throw new RuntimeException("Can't get non-current dimension " + dim + " from client."); + return ret; + } else { + throw new RuntimeException("Level on client is null"); + } + } + + @Override + public void enqueue(Runnable r) { + mc.submit(r); + } + + @Override + public void displayGui(GuiData data) { + Screen gui = data.createGui(mc.screen, mc.level); + if (gui != null) + mc.setScreen(gui); + } + + @Override + public void trackScreen(ScreenBlockEntity tes, boolean track) { + int idx = -1; + for (int i = 0; i < screenTracking.size(); i++) { + if (screenTracking.get(i) == tes) { + idx = i; + break; + } + } + + if (track) { + if (idx < 0) + screenTracking.add(tes); + } else if (idx >= 0) + screenTracking.remove(idx); + } + + @Override + public void onAutocompleteResult(NameUUIDPair[] pairs) { + if (mc.screen != null && mc.screen instanceof WDScreen screen) { + if (pairs.length == 0) + (screen).onAutocompleteFailure(); + else + (screen).onAutocompleteResult(pairs); + } + } + + @Override + public GameProfile[] getOnlineGameProfiles() { + return new GameProfile[]{mc.player.getGameProfile()}; + } + + @Override + public void screenUpdateResolutionInGui(Vector3i pos, BlockSide side, Vector2i res) { + if (mc.screen != null && mc.screen instanceof GuiScreenConfig gsc) { + if (gsc.isForBlock(pos.toBlock(), side)) + gsc.updateResolution(res); + } + } + + @Override + public void screenUpdateRotationInGui(Vector3i pos, BlockSide side, Rotation rot) { + if (mc.screen != null && mc.screen instanceof GuiScreenConfig gsc) { + if (gsc.isForBlock(pos.toBlock(), side)) + gsc.updateRotation(rot); + } + } + + @Override + public void screenUpdateAutoVolumeInGui(Vector3i pos, BlockSide side, boolean av) { + if (mc.screen != null && mc.screen instanceof GuiScreenConfig gsc) { + if (gsc.isForBlock(pos.toBlock(), side)) + gsc.updateAutoVolume(av); + } + } + + @Override + public void displaySetPadURLGui(ItemStack is, String padURL) { + mc.setScreen(new GuiSetURL2(is, padURL)); + } + + @Override + public void openMinePadGui(UUID padId) { + PadData pd = padMap.get(padId); + + if (pd != null && pd.view != null) + mc.setScreen(new GuiMinePad(pd)); + } + + @Override + @Nonnull + public HasAdvancement hasClientPlayerAdvancement(@Nonnull ResourceLocation rl) { + if (advancementToProgressField != null && mc.player != null && mc.player.connection != null) { + ClientAdvancements cam = mc.player.connection.getAdvancements(); + AdvancementHolder adv = cam.get(rl); + + if (adv == null) + return HasAdvancement.DONT_KNOW; + + if (lastAdvMgr != cam) { + lastAdvMgr = cam; + + try { + advancementToProgress = (Map) advancementToProgressField.get(cam); + } catch (Throwable t) { + Log.warningEx("Could not get ClientAdvancementManager.advancementToProgress field", t); + advancementToProgress = null; + return HasAdvancement.DONT_KNOW; + } + } + + if (advancementToProgress == null) + return HasAdvancement.DONT_KNOW; + + Object progress = advancementToProgress.get(adv); + if (progress == null) + return HasAdvancement.NO; + + if (!(progress instanceof AdvancementProgress)) { + Log.warning("The ClientAdvancementManager.advancementToProgress map does not contain AdvancementProgress instances"); + advancementToProgress = null; //Invalidate this: it's wrong + return HasAdvancement.DONT_KNOW; + } + + return ((AdvancementProgress) progress).isDone() ? HasAdvancement.YES : HasAdvancement.NO; + } + + return HasAdvancement.DONT_KNOW; + } + + @Override + public MinecraftServer getServer() { + return mc.getSingleplayerServer(); + } + +// @Override +// public void handleJSResponseSuccess(int reqId, JSServerRequest type, byte[] data) { +// JSQueryDispatcher.ServerQuery q = jsDispatcher.fulfillQuery(reqId); +// +// if (q == null) +// Log.warning("Received success response for invalid query ID %d of type %s", reqId, type.toString()); +// else { +// if (type == JSServerRequest.CLEAR_REDSTONE || type == JSServerRequest.SET_REDSTONE_AT) +// q.success("{\"status\":\"success\"}"); +// else +// Log.warning("Received success response for query ID %d, but type is invalid", reqId); +// } +// } +// +// @Override +// public void handleJSResponseError(int reqId, JSServerRequest type, int errCode, String err) { +// JSQueryDispatcher.ServerQuery q = jsDispatcher.fulfillQuery(reqId); +// +// if (q == null) +// Log.warning("Received error response for invalid query ID %d of type %s", reqId, type.toString()); +// else +// q.error(errCode, err); +// } + + @Override + public void setMiniservClientPort(int port) { + miniservPort = port; + } + + @Override + public void startMiniservClient() { + if (miniservPort <= 0) { + Log.warning("Can't start miniserv client: miniserv is disabled"); + return; + } + + if (mc.player == null) { + Log.warning("Can't start miniserv client: player is null"); + return; + } + + SocketAddress saddr = mc.player.connection.getConnection().channel().remoteAddress(); + if (saddr == null || !(saddr instanceof InetSocketAddress)) { + Log.warning("Miniserv client: remote address is not inet, assuming local address"); + saddr = new InetSocketAddress("127.0.0.1", 1234); + } + + InetSocketAddress msAddr = new InetSocketAddress(((InetSocketAddress) saddr).getAddress(), miniservPort); + Client.getInstance().start(msAddr); + msClientStarted = true; + } + + @Override + public boolean isMiniservDisabled() { + return miniservPort <= 0; + } + + @Override + public void closeGui(BlockPos bp, BlockSide bs) { + if (mc.screen instanceof WDScreen) { + WDScreen scr = (WDScreen) mc.screen; + + if (scr.isForBlock(bp, bs)) + mc.setScreen(null); + } + } + + @Override + public void renderRecipes() { + nextScreen = new RenderRecipe(); + } + + @Override + public boolean isShiftDown() { + return Screen.hasShiftDown(); + } + + + /**************************************** RESOURCE MANAGER METHODS ****************************************/ + + @Override + public void onResourceManagerReload(ResourceManager resourceManager) { + Log.info("Resource manager reload: clearing GUI cache..."); + GuiLoader.clearCache(); + } + + /**************************************** JS HANDLER METHODS ****************************************/ + +// @Override +// public boolean handleQuery(IBrowser browser, long queryId, String query, boolean persistent, IJSQueryCallback cb) { +// if (browser != null && persistent && query != null && cb != null) { +// query = query.toLowerCase(); +// +// if (query.startsWith("webdisplays_")) { +// query = query.substring(12); +// +// String args; +// int parenthesis = query.indexOf('('); +// if (parenthesis < 0) +// args = null; +// else { +// if (query.indexOf(')') != query.length() - 1) { +// cb.failure(400, "Malformed request"); +// return true; +// } +// +// args = query.substring(parenthesis + 1, query.length() - 1); +// query = query.substring(0, parenthesis); +// } +// +// if (jsDispatcher.canHandleQuery(query)) +// jsDispatcher.enqueueQuery(browser, query, args, cb); +// else +// cb.failure(404, "Unknown WebDisplays query"); +// +// return true; +// } +// } +// +// return false; +// } +// +// @Override +// public void cancelQuery(IBrowser browser, long queryId) { +// } + + /**************************************** EVENT METHODS ****************************************/ + + @SubscribeEvent + public void onLevelTick(LevelTickEvent.Post ev) { + if (!ev.getLevel().isClientSide()) return; + + //Unload/load screens depending on client player distance + if (mc.player == null || screenTracking.isEmpty()) + return; + + int id = lastTracked % screenTracking.size(); + + ScreenBlockEntity tes = screenTracking.get(id); + + if (!tes.getLevel().equals(ev.getLevel())) + return; + + lastTracked++; + if (tes.getLevel() != mc.player.level()) { + // TODO: properly handle this + // probably gonna want a helper class for cross-dimensional distances + if (!tes.isLoaded()) + tes.load(); + } else { + Camera camera = mc.getEntityRenderDispatcher().camera; + Entity entity = null; + + // ide inspection says this is a bunch of constant expressions + // THIS IS NOT THE CASE + // a crash HAS occurred because of this going unchecked, and I'm confused about it + + //noinspection ConstantValue + if (camera != null) entity = camera.getEntity(); + //noinspection ConstantValue + if (entity == null) entity = mc.player; + //noinspection ConstantValue + if (entity != null) { + double dist = distanceTo(tes, entity.getPosition(0)); + + if (tes.isLoaded()) { + if (dist > WebDisplays.INSTANCE.unloadDistance2 * 16) + tes.deactivate(); +// else if (ClientConfig.AutoVolumeControl.enableAutoVolume) +// tes.updateTrackDistance(dist, 80); //ToDo find master volume + } else if (dist <= WebDisplays.INSTANCE.loadDistance2 * 16) + tes.activate(); + } + } + } + + @SubscribeEvent + public void onTick(ClientTickEvent.Post ev) { + + //Help + if (InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), GLFW.GLFW_KEY_F1)) { + if (!isF1Down) { + isF1Down = true; + + String wikiName = null; + if (mc.screen instanceof WDScreen) + wikiName = ((WDScreen) mc.screen).getWikiPageName(); + else if (mc.screen instanceof AbstractContainerScreen) { + Slot slot = ((AbstractContainerScreen) mc.screen).getSlotUnderMouse(); + + if (slot != null && slot.hasItem() && slot.getItem().getItem() instanceof WDItem) + wikiName = ((WDItem) slot.getItem().getItem()).getWikiName(slot.getItem()); + } + +// if (wikiName != null) +// mcef.openExampleBrowser("https://montoyo.net/wdwiki/index.php/" + wikiName); + } + } else if (isF1Down) + isF1Down = false; + + //Workaround cuz chat sux + if (nextScreen != null && mc.screen == null) { + mc.setScreen(nextScreen); + nextScreen = null; + } + + // handle r button + if (KEY_MOUSE.isDown()) { + if (!rDown) { + rDown = true; + mouseOn = !mouseOn; + } + } else rDown = false; + if ( + Minecraft.getInstance().player == null || + !(Minecraft.getInstance().player.getItemInHand(InteractionHand.MAIN_HAND).getItem() instanceof ItemLaserPointer) + ) mouseOn = false; + + + //Load/unload minePads depending on which item is in the player's hand + if (++minePadTickCounter >= 10) { + minePadTickCounter = 0; + Player ep = mc.player; + + for (PadData pd : padList) + pd.isInHotbar = false; + + if (ep != null) { + updateInventory(ep.getInventory().items, ep.getItemInHand(InteractionHand.MAIN_HAND), 9); + updateInventory(ep.getInventory().offhand, ep.getItemInHand(InteractionHand.OFF_HAND), 1); //Is this okay? + } + + //TODO: Check for GuiContainer.draggedStack + + for (int i = padList.size() - 1; i >= 0; i--) { + PadData pd = padList.get(i); + + if (!pd.isInHotbar) { + pd.view.close(true); + pd.view = null; //This is for GuiMinePad, in case the player dies with the GUI open + padList.remove(i); + padMap.remove(pd.id); + } + } + } + + //Laser pointer raycast + if (LaserPointerRenderer.isOn()) { + ItemLaserPointer.tick(mc); + } else { + ItemLaserPointer.deselect(mc); + } + + //Miniserv + if (msClientStarted && mc.player == null) { + msClientStarted = false; + Client.getInstance().stop(); + } + } + + @SubscribeEvent + public void onRenderPlayerHand(RenderHandEvent ev) { + Item item = ev.getItemStack().getItem(); + IItemRenderer renderer; + + if (item == ItemRegistry.MINEPAD.get()) + renderer = minePadRenderer; + else if (item == ItemRegistry.LASER_POINTER.get()) + renderer = laserPointerRenderer; + else + return; + + HumanoidArm handSide = mc.player.getMainArm(); + if (ev.getHand() == InteractionHand.OFF_HAND) + handSide = handSide.getOpposite(); + + if (renderer.render(ev.getPoseStack(), ev.getItemStack(), (handSide == HumanoidArm.RIGHT) ? 1.0f : -1.0f, ev.getSwingProgress(), ev.getEquipProgress(), ev.getMultiBufferSource(), ev.getPackedLight())) { + ev.setCanceled(true); + } + } + + @SubscribeEvent + public void onWorldUnload(LevelEvent.Unload ev) { + Log.info("World unloaded; killing screens..."); + if (ev.getLevel() instanceof Level level) { + ResourceLocation dim = level.dimension().location(); + for (int i = screenTracking.size() - 1; i >= 0; i--) { + if (screenTracking.get(i).getLevel().dimension().location().equals(dim)) //Could be world == ev.getWorld() + screenTracking.remove(i).unload(); + } + } + } + + public static BlockHitResult raycast(double dist) { + Minecraft mc = Minecraft.getInstance(); + + Vec3 start = mc.player.getEyePosition(1.0f); + Vec3 lookVec = mc.player.getLookAngle(); + Vec3 end = start.add(lookVec.x * dist, lookVec.y * dist, lookVec.z * dist); + + return mc.level.clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.ANY, mc.player)); + } + + private void updateInventory(NonNullList inv, ItemStack heldStack, int cnt) { + for (int i = 0; i < cnt; i++) { + ItemStack item = inv.get(i); + + if (item.getItem() == ItemRegistry.MINEPAD.get()) { + if (item.has(WDDataComponents.PAD_ID.get())) { + UUID padId = item.get(WDDataComponents.PAD_ID.get()); + updatePad(padId, item, item == heldStack); + } + } + } + } + + private void updatePad(UUID id, ItemStack stack, boolean isSelected) { + PadData pd = padMap.get(id); + + if (pd != null) + pd.isInHotbar = true; + else if (isSelected && stack.has(WDDataComponents.PAD_URL.get())) { + String url = stack.get(WDDataComponents.PAD_URL.get()); + pd = new PadData(url, id); + padMap.put(id, pd); + padList.add(pd); + } + } + + public MinePadRenderer getMinePadRenderer() { + return minePadRenderer; + } + + public PadData getPadByID(UUID id) { + return padMap.get(id); + } + + public static final class ScreenSidePair { + + public ScreenBlockEntity tes; + public BlockSide side; + + } + + public boolean findScreenFromBrowser(CefBrowser browser, ScreenSidePair pair) { + for (ScreenBlockEntity tes : screenTracking) { + for (int i = 0; i < tes.screenCount(); i++) { + ScreenData scr = tes.getScreen(i); + + if (scr.browser == browser) { + pair.tes = tes; + pair.side = scr.side; + return true; + } + } + } + + return false; + } + + private static Field findAdvancementToProgressField() { + Field[] fields = ClientAdvancements.class.getDeclaredFields(); + Optional result = Arrays.stream(fields).filter(f -> f.getType() == Map.class).findAny(); + + if (result.isPresent()) { + try { + Field ret = result.get(); + ret.setAccessible(true); + return ret; + } catch (Throwable t) { + t.printStackTrace(); + } + } + + Log.warning("ClientAdvancementManager.advancementToProgress field could not be found"); + return null; + } + + @Override + public BlockGetter getWorld(IPayloadContext context) { + BlockGetter senderLevel = super.getWorld(context); + if (senderLevel == null) return Minecraft.getInstance().level; + return senderLevel; + } + +@SubscribeEvent + public void onDrawSelection(RenderHighlightEvent.Block event) { + if (event.getTarget() instanceof BlockHitResult bhr) { + BlockState state = Minecraft.getInstance().level.getBlockState(bhr.getBlockPos()); + if (state.getBlock() instanceof ScreenBlock screen) { + Vector3i vec = new Vector3i(bhr.getBlockPos().getX(), bhr.getBlockPos().getY(), bhr.getBlockPos().getZ()); + BlockSide side = BlockSide.fromInt(bhr.getDirection().ordinal()); + Multiblock.findOrigin( + Minecraft.getInstance().level, vec, + side, null + ); + + BlockPos pos = new BlockPos(vec.x, vec.y, vec.z); + BlockEntity be = Minecraft.getInstance().level.getBlockEntity( + pos + ); + if (be instanceof ScreenBlockEntity tes) { + if (tes.getScreen(side) != null) { + event.setCanceled(true); + } + } + } + } + } + + /** + * KEYBINDS + **/ + public static final KeyMapping KEY_MOUSE = new KeyMapping("webdisplays.key.toggle_mouse", GLFW.GLFW_KEY_R, "key.categories.misc"); + static boolean rDown = false; + public static boolean mouseOn = false; + + public static void onKeybindRegistry(RegisterKeyMappingsEvent event) { + event.register(KEY_MOUSE); + } +} diff --git a/src/main/java/net/montoyo/wd/client/JSQueryDispatcher.java b/src/main/java/net/montoyo/wd/client/JSQueryDispatcher.java new file mode 100644 index 0000000..0e85d2f --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/JSQueryDispatcher.java @@ -0,0 +1,370 @@ +///* +// * Copyright (C) 2018 BARBOTIN Nicolas +// */ +// +//package net.montoyo.wd.client; +// +//import net.minecraft.client.Minecraft; +//import net.minecraft.core.BlockPos; +//import net.minecraft.core.Direction; +//import net.minecraft.world.item.ItemStack; +//import net.neoforged.api.distmarker.Dist; +//import net.neoforged.api.distmarker.OnlyIn; +//import net.montoyo.wd.block.BlockScreen; +//import net.montoyo.wd.core.DefaultUpgrade; +//import net.montoyo.wd.core.IScreenQueryHandler; +//import net.montoyo.wd.core.IUpgrade; +//import net.montoyo.wd.core.JSServerRequest; +//import net.montoyo.wd.entity.TileEntityScreen; +//import net.montoyo.wd.net.WDNetworkRegistry; +//import net.montoyo.wd.net.server_bound.C2SMessageScreenCtrl; +//import net.montoyo.wd.utilities.*; +// +//import java.util.*; +// +//@OnlyIn(Dist.CLIENT) +//public final class JSQueryDispatcher { +// +// private static final class QueryData { +// +// private final IBrowser browser; +// private final String query; +// private final String args; +// private final IJSQueryCallback callback; +// +// private QueryData(IBrowser b, String q, String a, IJSQueryCallback cb) { +// browser = b; +// query = q; +// args = a; +// callback = cb; +// } +// +// } +// +// public static final class ServerQuery { +// +// private static int lastId = 0; +// +// private final TileEntityScreen tes; +// private final BlockSide side; +// private final IJSQueryCallback callback; +// private final int id; +// +// private ServerQuery(TileEntityScreen t, BlockSide s, IJSQueryCallback cb) { +// tes = t; +// side = s; +// callback = cb; +// id = lastId++; +// } +// +// public TileEntityScreen getTileEntity() { +// return tes; +// } +// +// public BlockSide getSide() { +// return side; +// } +// +// public TileEntityScreen.Screen getScreen() { +// return tes.getScreen(side); +// } +// +// public void success(String resp) { +// callback.success(resp); +// } +// +// public void error(int errId, String errStr) { +// callback.failure(errId, errStr); +// } +// +// } +// +// private final ClientProxy proxy; +// private final ArrayDeque queue = new ArrayDeque<>(); +// private final ClientProxy.ScreenSidePair lookupResult = new ClientProxy.ScreenSidePair(); +// private final HashMap handlers = new HashMap<>(); +// private final ArrayList serverQueries = new ArrayList<>(); +// private final Minecraft mc = Minecraft.getInstance(); +// +// public JSQueryDispatcher(ClientProxy proxy) { +// this.proxy = proxy; +// registerDefaults(); +// } +// +// public void enqueueQuery(IBrowser b, String q, String a, IJSQueryCallback cb) { +// synchronized(queue) { +// queue.offer(new QueryData(b, q, a, cb)); +// } +// } +// +// public void handleQueries() { +// while(true) { +// QueryData next; +// synchronized(queue) { +// next = queue.poll(); +// } +// +// if(next == null) +// break; +// +// if(proxy.findScreenFromBrowser(next.browser, lookupResult)) { +// Object[] args = (next.args == null) ? new Object[0] : parseArgs(next.args); +// +// if(args == null) +// next.callback.failure(400, "Malformed request parameters"); +// else { +// try { +// handlers.get(next.query).handleQuery(next.callback, lookupResult.tes, lookupResult.side, args); +// } catch(Throwable t) { +// Log.warningEx("Could not execute JS query %s(%s)", t, next.query, (next.args == null) ? "" : next.args); +// next.callback.failure(500, "Internal error"); +// } +// } +// } else +// next.callback.failure(403, "A screen is required"); +// } +// } +// +// public boolean canHandleQuery(String q) { +// return handlers.containsKey(q); +// } +// +// private static Object[] parseArgs(String args) { +// ArrayList array = new ArrayList<>(); +// int lastIdx = 0; +// boolean inString = false; +// boolean escape = false; +// boolean hadString = false; +// +// for(int i = 0; i < args.length(); i++) { +// char chr = args.charAt(i); +// +// if(inString) { +// if(escape) +// escape = false; +// else { +// if(chr == '\"') +// inString = false; +// else if(chr == '\\') +// escape = true; +// } +// } else if(chr == '\"') { +// if(hadString) +// return null; +// +// inString = true; +// hadString = true; +// } else if(chr == ',') { +// array.add(args.substring(lastIdx, i).trim()); +// lastIdx = i + 1; +// hadString = false; +// } +// } +// +// if(inString) +// return null; //Non terminated string +// +// array.add(args.substring(lastIdx).trim()); +// Object[] ret = new Object[array.size()]; +// +// for(int i = 0; i < ret.length; i++) { +// String str = array.get(i); +// if(str.isEmpty()) +// return null; //Nah... +// +// if(str.charAt(0) == '\"') //String +// ret[i] = str.substring(1, str.length() - 1); +// else { +// try { +// ret[i] = Double.parseDouble(str); +// } catch(NumberFormatException ex) { +// return null; +// } +// } +// } +// +// return ret; +// } +// +// public void register(String query, IScreenQueryHandler handler) { +// handlers.put(query.toLowerCase(), handler); +// } +// +// public ServerQuery fulfillQuery(int id) { +// int toRemove = -1; +// +// for(int i = 0; i < serverQueries.size(); i++) { +// ServerQuery sq = serverQueries.get(i); +// +// if(sq.id == id) { +// toRemove = i; +// break; +// } +// } +// +// if(toRemove < 0) +// return null; +// else +// return serverQueries.remove(toRemove); +// } +// +// private void makeServerQuery(TileEntityScreen tes, BlockSide side, IJSQueryCallback cb, JSServerRequest type, Object ... data) { +// ServerQuery ret = new ServerQuery(tes, side, cb); +// serverQueries.add(ret); +// +// WDNetworkRegistry.INSTANCE.sendToServer(C2SMessageScreenCtrl.jsRequest(tes, side, ret.id, type, data)); +// } +// +// private void registerDefaults() { +// VideoType.registerQueries(this); +// +// register("GetSize", (cb, tes, side, args) -> { +// Vector2i size = tes.getScreen(side).size; +// cb.success("{\"x\":" + size.x + ",\"y\":" + size.y + "}"); +// }); +// +// register("GetRedstoneAt", (cb, tes, side, args) -> { +// if(!tes.hasUpgrade(side, DefaultUpgrade.REDINPUT)) { +// cb.failure(403, "Missing upgrade"); +// return; +// } +// +// if(args.length == 2 && args[0] instanceof Double && args[1] instanceof Double) { +// TileEntityScreen.Screen scr = tes.getScreen(side); +// int x = ((Double) args[0]).intValue(); +// int y = ((Double) args[1]).intValue(); +// +// if(x < 0 || x >= scr.size.x || y < 0 || y >= scr.size.y) +// cb.failure(403, "Out of range"); +// else { +// BlockPos bpos = (new Vector3i(tes.getBlockPos())).addMul(side.right, x).addMul(side.up, y).toBlock(); +// int level = tes.getLevel().getBlockState(bpos).getValue(BlockScreen.emitting) ? 0 : tes.getLevel().getSignal(bpos, Direction.values()[side.reverse().ordinal()]); +// cb.success("{\"level\":" + level + "}"); +// } +// } else +// cb.failure(400, "Wrong arguments"); +// }); +// +// register("GetRedstoneArray", (cb, tes, side, args) -> { +// if(tes.hasUpgrade(side, DefaultUpgrade.REDINPUT)) { +// final Direction facing = Direction.values()[side.reverse().ordinal()]; +// final StringJoiner resp = new StringJoiner(",", "{\"levels\":[", "]}"); +// +// tes.forEachScreenBlocks(side, bp -> { +// if(tes.getLevel().getBlockState(bp).getValue(BlockScreen.emitting)) +// resp.add("0"); +// else +// resp.add("" + tes.getLevel().getSignal(bp, facing)); +// }); +// +// cb.success(resp.toString()); +// } else +// cb.failure(403, "Missing upgrade"); +// }); +// +// register("ClearRedstone", (cb, tes, side, args) -> { +// if(tes.hasUpgrade(side, DefaultUpgrade.REDOUTPUT)) { +// if(tes.getScreen(side).owner.uuid.equals(mc.player.getGameProfile().getId())) +// makeServerQuery(tes, side, cb, JSServerRequest.CLEAR_REDSTONE); +// else +// cb.success("{\"status\":\"notOwner\"}"); +// } else +// cb.failure(403, "Missing upgrade"); +// }); +// +// register("SetRedstoneAt", (cb, tes, side, args) -> { +// if(args.length != 3 || !Arrays.stream(args).allMatch((obj) -> obj instanceof Double)) { +// cb.failure(400, "Wrong arguments"); +// return; +// } +// +// if(!tes.hasUpgrade(side, DefaultUpgrade.REDOUTPUT)) { +// cb.failure(403, "Missing upgrade"); +// return; +// } +// +// if(!tes.getScreen(side).owner.uuid.equals(mc.player.getGameProfile().getId())) { +// cb.success("{\"status\":\"notOwner\"}"); +// return; +// } +// +// int x = ((Double) args[0]).intValue(); +// int y = ((Double) args[1]).intValue(); +// boolean state = ((Double) args[2]) > 0.0; +// +// Vector2i size = tes.getScreen(side).size; +// if(x < 0 || x >= size.x || y < 0 || y >= size.y) { +// cb.failure(403, "Out of range"); +// return; +// } +// +// makeServerQuery(tes, side, cb, JSServerRequest.SET_REDSTONE_AT, x, y, state); +// }); +// +// register("IsEmitting", (cb, tes, side, args) -> { +// if(!tes.hasUpgrade(side, DefaultUpgrade.REDOUTPUT)) { +// cb.failure(403, "Missing upgrade"); +// return; +// } +// +// if(args.length == 2 && args[0] instanceof Double && args[1] instanceof Double) { +// TileEntityScreen.Screen scr = tes.getScreen(side); +// int x = ((Double) args[0]).intValue(); +// int y = ((Double) args[1]).intValue(); +// +// if(x < 0 || x >= scr.size.x || y < 0 || y >= scr.size.y) +// cb.failure(403, "Out of range"); +// else { +// BlockPos bpos = (new Vector3i(tes.getBlockPos())).addMul(side.right, x).addMul(side.up, y).toBlock(); +// boolean e = tes.getLevel().getBlockState(bpos).getValue(BlockScreen.emitting); +// cb.success("{\"emitting\":" + (e ? "true" : "false") + "}"); +// } +// } else +// cb.failure(400, "Wrong arguments"); +// }); +// +// register("GetEmissionArray", (cb, tes, side, args) -> { +// if(tes.hasUpgrade(side, DefaultUpgrade.REDOUTPUT)) { +// final StringJoiner resp = new StringJoiner(",", "{\"emission\":[", "]}"); +// tes.forEachScreenBlocks(side, bp -> resp.add(tes.getLevel().getBlockState(bp).getValue(BlockScreen.emitting) ? "1" : "0")); +// cb.success(resp.toString()); +// } else +// cb.failure(403, "Missing upgrade"); +// }); +// +// register("GetLocation", (cb, tes, side, args) -> { +// if(!tes.hasUpgrade(side, DefaultUpgrade.GPS)) { +// cb.failure(403, "Missing upgrade"); +// return; +// } +// +// BlockPos bp = tes.getBlockPos(); +// cb.success("{\"x\":" + bp.getX() + ",\"y\":" + bp.getY() + ",\"z\":" + bp.getZ() + ",\"side\":\"" + side + "\"}"); +// }); +// +// register("GetUpgrades", (cb, tes, side, args) -> { +// final StringBuilder sb = new StringBuilder("{\"upgrades\":["); +// final ArrayList upgrades = tes.getScreen(side).upgrades; +// +// for(int i = 0; i < upgrades.size(); i++) { +// if(i > 0) +// sb.append(','); +// +// sb.append('\"'); +// sb.append(Util.addSlashes(((IUpgrade) upgrades.get(i).getItem()).getJSName(upgrades.get(i)))); +// sb.append('\"'); +// } +// +// cb.success(sb.append("]}").toString()); +// }); +// +// register("IsOwner", (cb, tes, side, args) -> { +// boolean res = (tes.getScreen(side).owner != null && tes.getScreen(side).owner.uuid.equals(mc.player.getGameProfile().getId())); +// cb.success("{\"isOwner\":" + (res ? "true}" : "false}")); +// }); +// +// register("GetRotation", (cb, tes, side, args) -> cb.success("{\"rotation\":" + tes.getScreen(side).rotation.ordinal() + "}")); +// register("GetSide", (cb, tes, side, args) -> cb.success("{\"side\":" + tes.getScreen(side).side.ordinal() + "}")); +// } +// +//} diff --git a/src/main/java/net/montoyo/wd/client/WDScheme.java b/src/main/java/net/montoyo/wd/client/WDScheme.java new file mode 100644 index 0000000..0101b40 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/WDScheme.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client; + +import net.montoyo.wd.miniserv.Constants; +import net.montoyo.wd.miniserv.client.Client; +import net.montoyo.wd.miniserv.client.ClientTaskGetFile; +import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.utilities.serialization.Util; +import org.cef.callback.CefCallback; +import org.cef.handler.CefResourceHandler; +import org.cef.misc.IntRef; +import org.cef.misc.StringRef; +import org.cef.network.CefRequest; +import org.cef.network.CefResponse; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +public class WDScheme implements CefResourceHandler { + + private static final String ERROR_PAGE = "

%d %s


Miniserv powered by WebDisplays"; + private ClientTaskGetFile task; + private boolean isErrorPage; + + String url; + boolean onlyError = false; + + public WDScheme(String url) { + this.url = url; + } + + @Override + public boolean processRequest(CefRequest cefRequest, CefCallback cefCallback) { + url = cefRequest.getURL(); + + url = url.substring("webdisplays://".length()); + + int pos = url.indexOf('/'); + if (pos < 0) + return false; + + String uuidStr = url.substring(0, pos); + String fileStr = url.substring(pos + 1); + + fileStr = URLDecoder.decode(fileStr, StandardCharsets.UTF_8); + + if (uuidStr.isEmpty() || Util.isFileNameInvalid(fileStr)) { + // invalid URL or no UUID + onlyError = true; + cefCallback.Continue(); + return true; + } + + UUID uuid; + try { + uuid = UUID.fromString(uuidStr); + } catch (IllegalArgumentException ex) { + // invalid UUID + onlyError = true; + cefCallback.Continue(); + return true; + } + + task = new ClientTaskGetFile(uuid, fileStr); + boolean doContinue = Client.getInstance().addTask(task); + if (doContinue) cefCallback.Continue(); + return doContinue; + } + + @Override + public void getResponseHeaders(CefResponse cefResponse, IntRef contentLength, StringRef redir) { + int status; + if (onlyError) { + status = Constants.GETF_STATUS_BAD_NAME; + } else { + Log.info("Waiting for response..."); + status = task.waitForResponse(); + Log.info("Got response %d", status); + + if (status == 0) { + //OK + int extPos = task.getFileName().lastIndexOf('.'); + if (extPos >= 0) { + String mime = mapMime(task.getFileName().substring(extPos + 1)); + + if (mime != null) + cefResponse.setMimeType(mime); + } + + cefResponse.setStatus(200); + cefResponse.setStatusText("OK"); + contentLength.set(0); + return; + } + } + + int errCode; + String errStr; + + if (status == Constants.GETF_STATUS_NOT_FOUND) { + errCode = 404; + errStr = "Not Found"; + } else if (status == Constants.GETF_STATUS_TIMED_OUT) { + errCode = 408; + errStr = "Timed Out"; + } else if (status == Constants.GETF_STATUS_BAD_NAME) { + errCode = 418; + errStr = "I'm a teapot"; + } else { + errCode = 500; + errStr = "Internal Server Error"; + } + + // reporting the actual status and text makes CEF not display the page + cefResponse.setStatus(200); + cefResponse.setStatusText("OK"); + cefResponse.setMimeType("text/html"); + + dataToWrite = String.format(ERROR_PAGE, errCode, errStr).getBytes(StandardCharsets.UTF_8); + dataOffset = 0; + amountToWrite = dataToWrite.length; + isErrorPage = true; + contentLength.set(0); + } + + private byte[] dataToWrite; + private int dataOffset; + private int amountToWrite; + + @Override + public boolean readResponse(byte[] output, int bytesToRead, IntRef bytesRead, CefCallback cefCallback) { + if (dataToWrite == null) { + if (isErrorPage) { + bytesRead.set(0); + return false; + } + + dataToWrite = task.waitForData(); + dataOffset = 3; //packet ID + size + amountToWrite = task.getDataLength(); + + if (amountToWrite <= 0) { + dataToWrite = null; + bytesRead.set(0); + return false; + } + } + + int toWrite = bytesToRead; + if (toWrite > amountToWrite) + toWrite = amountToWrite; + + System.arraycopy(dataToWrite, dataOffset, output, 0, toWrite); + bytesRead.set(toWrite); + + dataOffset += toWrite; + amountToWrite -= toWrite; + + if (amountToWrite <= 0) { + if (!isErrorPage) + task.nextData(); + + dataToWrite = null; + } + + return true; + } + + @Override + public void cancel() { + Log.info("Scheme query canceled or finished."); + if (!onlyError) + task.cancel(); + } + + public static String mapMime(String ext) { + switch (ext) { + case "htm": + case "html": + return "text/html"; + + case "css": + return "text/css"; + + case "js": + return "text/javascript"; + + case "png": + return "image/png"; + + case "jpg": + case "jpeg": + return "image/jpeg"; + + case "gif": + return "image/gif"; + + case "svg": + return "image/svg+xml"; + + case "xml": + return "text/xml"; + + case "txt": + return "text/plain"; + + default: + return null; + } + } +} diff --git a/src/main/java/net/montoyo/wd/client/audio/WDAudioSource.java b/src/main/java/net/montoyo/wd/client/audio/WDAudioSource.java new file mode 100644 index 0000000..2e0bce0 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/audio/WDAudioSource.java @@ -0,0 +1,116 @@ +package net.montoyo.wd.client.audio; + +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.client.sounds.WeighedSoundEvents; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.util.valueproviders.SampledFloat; +import net.montoyo.wd.entity.ScreenBlockEntity; +import net.montoyo.wd.entity.ScreenData; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; + +public class WDAudioSource implements SoundInstance { + private static final ResourceLocation location = ResourceLocation.fromNamespaceAndPath("webdisplays", "audio_source"); + private static final WeighedSoundEvents events = new WeighedSoundEvents( + location, "webdisplays.browser" + ); + private static final SampledFloat CONST_1 = new SampledFloat() { + @Override + public float sample(RandomSource pRandom) { + return 1.0f; + } + }; + private final Sound sound = new Sound( + ResourceLocation.fromNamespaceAndPath("webdisplays", "unused"), + CONST_1, + CONST_1, + 1, Sound.Type.SOUND_EVENT, + true, false, + 100 + ); + ScreenBlockEntity blockEntity; + ScreenData data; + + public WDAudioSource(ScreenBlockEntity blockEntity, ScreenData data) { + this.blockEntity = blockEntity; + this.data = data; + } + + @Override + public ResourceLocation getLocation() { + return location; + } + + @Nullable + @Override + public WeighedSoundEvents resolve(SoundManager pManager) { + return events; + } + + @Override + public CompletableFuture getStream(SoundBufferLibrary soundBuffers, Sound sound, boolean looping) { + return null; + } + + @Override + public Sound getSound() { + return sound; + } + + @Override + public SoundSource getSource() { + return SoundSource.RECORDS; + } + + @Override + public boolean isLooping() { + return true; + } + + @Override + public boolean isRelative() { + return false; + } + + @Override + public int getDelay() { + return 0; + } + + @Override + public float getVolume() { + return blockEntity.ytVolume; + } + + @Override + public float getPitch() { + return 1; + } + + @Override + public double getX() { + return blockEntity.getBlockPos().getX(); + } + + @Override + public double getY() { + return blockEntity.getBlockPos().getY(); + } + + @Override + public double getZ() { + return blockEntity.getBlockPos().getZ(); + } + + @Override + public Attenuation getAttenuation() { + return Attenuation.LINEAR; + } +} diff --git a/src/main/java/net/montoyo/wd/client/gui/CommandHandler.java b/src/main/java/net/montoyo/wd/client/gui/CommandHandler.java new file mode 100644 index 0000000..ed4f299 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/CommandHandler.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandHandler { + + String value(); + +} diff --git a/src/main/java/net/montoyo/wd/client/gui/GuiKeyboard.java b/src/main/java/net/montoyo/wd/client/gui/GuiKeyboard.java new file mode 100644 index 0000000..2ee2622 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/GuiKeyboard.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import com.cinemamod.mcef.MCEFBrowser; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLPaths; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.client.gui.camera.KeyboardCamera; +import net.montoyo.wd.client.gui.controls.Button; +import net.montoyo.wd.client.gui.controls.Control; +import net.montoyo.wd.client.gui.controls.Label; +import net.montoyo.wd.client.gui.loading.FillControl; +import net.montoyo.wd.controls.builtin.ClickControl; +import net.montoyo.wd.entity.ScreenBlockEntity; +import net.montoyo.wd.entity.ScreenData; +import net.montoyo.wd.net.WDNetworkRegistry; +import net.montoyo.wd.net.server_bound.C2SMessageScreenCtrl; +import net.montoyo.wd.utilities.Log; +import net.montoyo.wd.utilities.data.BlockSide; +import net.montoyo.wd.utilities.math.Vector2i; +import net.montoyo.wd.utilities.serialization.TypeData; +import net.montoyo.wd.utilities.serialization.Util; +import org.cef.browser.CefBrowser; +import org.cef.misc.CefCursorType; +import org.joml.Matrix4f; +import org.joml.Vector4f; +import org.lwjgl.glfw.GLFW; +import org.vivecraft.client_vr.gameplay.VRPlayer; +import org.vivecraft.client_vr.gameplay.screenhandlers.KeyboardHandler; + +import java.io.*; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Map; +import java.util.function.Consumer; + +@OnlyIn(Dist.CLIENT) +public class GuiKeyboard extends WDScreen { + + private static final String WARNING_FNAME = "wd_keyboard_warning.txt"; + + private ScreenBlockEntity tes; + private BlockSide side; + private ScreenData data; + private final ArrayList evStack = new ArrayList<>(); + private BlockPos kbPos; + private boolean showWarning = true; + + @FillControl + private Label lblInfo; + + @FillControl + private Button btnOk; + + public GuiKeyboard() { + super(Component.nullToEmpty(null)); + } + + public GuiKeyboard(ScreenBlockEntity tes, BlockSide side, BlockPos kbPos) { + this(); + this.tes = tes; + this.side = side; + this.kbPos = kbPos; + } + + @Override + protected void addLoadCustomVariables(Map vars) { + vars.put("showWarning", showWarning ? 1.0 : 0.0); + } + + private static final boolean vivecraftPresent; + + static { + boolean vivePres = false; + if (ModList.get().isLoaded("vivecraft")) vivePres = true; + // I believe the non-mixin version of vivecraft is not a proper mod, so + // detect the mod reflectively if the mod is not found + else { + try { + Class clazz = Class.forName("org.vivecraft.gameplay.screenhandlers.KeyboardHandler"); + //noinspection ConstantConditions + if (clazz == null) vivePres = false; + else { + Method m = clazz.getMethod("setOverlayShowing", boolean.class); + //noinspection ConstantConditions + vivePres = m != null; + } + } catch (Throwable ignored) { + vivePres = false; + } + } + vivecraftPresent = vivePres; + } + + @Override + public void init() { + super.init(); + + if (minecraft.getSingleplayerServer() != null && !minecraft.getSingleplayerServer().isPublished()) + showWarning = false; //NO NEED + else + showWarning = !hasUserReadWarning(); + + loadFrom(ResourceLocation.fromNamespaceAndPath("webdisplays", "gui/kb_right.json")); + + if (showWarning) { + int maxLabelW = 0; + int totalH = 0; + + for (Control ctrl : controls) { + if (ctrl != lblInfo && ctrl instanceof Label) { + if (ctrl.getWidth() > maxLabelW) + maxLabelW = ctrl.getWidth(); + + totalH += ctrl.getHeight(); + ctrl.setPos((width - ctrl.getWidth()) / 2, 0); + } + } + + btnOk.setWidth(maxLabelW); + btnOk.setPos((width - maxLabelW) / 2, 0); + totalH += btnOk.getHeight(); + + int y = (height - totalH) / 2; + for (Control ctrl : controls) { + if (ctrl != lblInfo) { + ctrl.setPos(ctrl.getX(), y); + y += ctrl.getHeight(); + } + } + } else { + if (!minecraft.isWindowActive()) { + minecraft.setWindowActive(true); + minecraft.mouseHandler.grabMouse(); + } + } + + defaultBackground = showWarning; + syncTicks = 5; + + if (vivecraftPresent) + if (VRPlayer.get() != null) + KeyboardHandler.setOverlayShowing(true); + + KeyboardCamera.focus(tes, side); + + data = tes.getScreen(side); + CefBrowser browser = data.browser; + ((MCEFBrowser) browser).setCursor(CefCursorType.fromId(data.mouseType)); + ((MCEFBrowser) browser).setCursorChangeListener((id) -> { + data.mouseType = id; + ((MCEFBrowser) browser).setCursor(CefCursorType.fromId(id)); + }); + } + + @Override + public void removed() { + super.removed(); + if (vivecraftPresent) + if (VRPlayer.get() != null) + KeyboardHandler.setOverlayShowing(false); + KeyboardCamera.focus(null, null); + CefBrowser browser = data.browser; + if (browser instanceof MCEFBrowser mcef) { + mcef.setCursor(CefCursorType.POINTER); + mcef.setCursorChangeListener((cursor) -> data.mouseType = cursor); + } + } + + @Override + public void onClose() { + removed(); + super.onClose(); + this.minecraft.popGuiLayer(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (quitOnEscape && keyCode == GLFW.GLFW_KEY_ESCAPE) { + onClose(); + return true; + } + addKey(new TypeData(TypeData.Action.PRESS, keyCode, modifiers, scanCode)); + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char codePoint, int modifiers) { + addKey(new TypeData(TypeData.Action.TYPE, codePoint, modifiers, 0)); + return super.charTyped(codePoint, modifiers); + } + + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + addKey(new TypeData(TypeData.Action.RELEASE, keyCode, modifiers, scanCode)); + return super.keyPressed(keyCode, scanCode, modifiers); + } + + void addKey(TypeData data) { + tes.type(side, "[" + WebDisplays.GSON.toJson(data) + "]", kbPos); + + evStack.add(data); + if (!evStack.isEmpty() && !syncRequested()) + requestSync(); + } + + @Override + protected void sync() { + if(!evStack.isEmpty()) { + WDNetworkRegistry.INSTANCE.sendToServer(C2SMessageScreenCtrl.type(tes, side, WebDisplays.GSON.toJson(evStack), kbPos)); + evStack.clear(); + } + } + + @GuiSubscribe + public void onClick(Button.ClickEvent ev) { + if(showWarning && ev.getSource() == btnOk) { + writeUserAcknowledge(); + + for(Control ctrl: controls) { + if(ctrl instanceof Label) { + Label lbl = (Label) ctrl; + lbl.setVisible(!lbl.isVisible()); + } + } + + btnOk.setDisabled(true); + btnOk.setVisible(false); + showWarning = false; + defaultBackground = false; + minecraft.setWindowActive(true); + minecraft.mouseHandler.grabMouse(); + } + } + + private boolean hasUserReadWarning() { + try { + File f = new File(FMLPaths.GAMEDIR.get().toString(), WARNING_FNAME); + + if(f.exists()) { + BufferedReader br = new BufferedReader(new FileReader(f)); + String str = br.readLine(); + Util.silentClose(br); + + return str != null && str.trim().equalsIgnoreCase("read"); + } + } catch(Throwable t) { + Log.warningEx("Can't know if user has already read the warning", t); + } + + return false; + } + + private void writeUserAcknowledge() { + try { + File f = new File(FMLPaths.GAMEDIR.get().toString(), WARNING_FNAME); + + BufferedWriter bw = new BufferedWriter(new FileWriter(f)); + bw.write("read\n"); + Util.silentClose(bw); + } catch(Throwable t) { + Log.warningEx("Can't write that the user read the warning", t); + } + } + + @Override + public boolean isForBlock(BlockPos bp, BlockSide side) { + return bp.equals(kbPos) || (bp.equals(tes.getBlockPos()) && side == this.side); + } + + protected void mouse(double mouseX, double mouseY, Consumer func) { + float pct = this.lastPartialTick; + + double fov = Minecraft.getInstance().options.fov().get().doubleValue(); + + mouseX /= width; + mouseY /= height; + + mouseX -= 0.5; + mouseY -= 0.5; + mouseY = -mouseY; + + Matrix4f proj = Minecraft.getInstance().gameRenderer.getProjectionMatrix(fov); + + Entity e = Minecraft.getInstance().getEntityRenderDispatcher().camera.getEntity(); + + PoseStack camera = new PoseStack(); + float[] angle = KeyboardCamera.getAngle(e, pct); + camera.mulPose(Axis.XP.rotationDegrees(angle[0])); + camera.mulPose(Axis.YP.rotationDegrees(angle[1] + 180.0F)); + + Vector4f coord = new Vector4f(2f * (float) mouseX, 2 * (float) mouseY, 0, 1f); + coord.add(proj.invert().transform(coord)); + coord = camera.last().pose().invert().transform(coord); + + Vec3 vec3 = e.getEyePosition(pct); + Vec3 vec31 = new Vec3(coord.x, coord.y, coord.z).normalize(); + + BlockHitResult result = tes.trace(side, vec3, vec31); + if (result.getType() != HitResult.Type.MISS) { + tes.interact(result, func); + } + } + + @Override + public void render(GuiGraphics poseStack, int mouseX, int mouseY, float ptt) { + this.lastPartialTick = ptt; + super.render(poseStack, mouseX, mouseY, ptt); + } + + private float lastPartialTick = 0f; + + @Override + public void mouseMoved(double mouseX, double mouseY) { + mouse(mouseX, mouseY, (hit) -> { + tes.handleMouseEvent(side, ClickControl.ControlType.MOVE, hit, -1); + WDNetworkRegistry.INSTANCE.sendToServer(C2SMessageScreenCtrl.laserMove(tes, side, hit)); + }); + + super.mouseMoved(mouseX, mouseY); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + mouse(mouseX, mouseY, (hit) -> { + tes.handleMouseEvent(side, ClickControl.ControlType.MOVE, hit, -1); + tes.handleMouseEvent(side, ClickControl.ControlType.DOWN, hit, button); + WDNetworkRegistry.INSTANCE.sendToServer(C2SMessageScreenCtrl.laserDown(tes, side, hit, button)); + }); + + KeyboardCamera.setMouse(button, true); + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + mouse(mouseX, mouseY, (hit) -> { + tes.handleMouseEvent(side, ClickControl.ControlType.MOVE, hit, -1); + tes.handleMouseEvent(side, ClickControl.ControlType.UP, hit, button); + WDNetworkRegistry.INSTANCE.sendToServer(C2SMessageScreenCtrl.laserUp(tes, side, button)); + }); + + KeyboardCamera.setMouse(button, false); + + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public void tick() { + double mouseX = Minecraft.getInstance().mouseHandler.xpos() / Minecraft.getInstance().getWindow().getWidth(); + double mouseY = Minecraft.getInstance().mouseHandler.ypos() / Minecraft.getInstance().getWindow().getHeight(); + + mouse(mouseX * width, mouseY * height, (hit) -> { + tes.handleMouseEvent(side, ClickControl.ControlType.MOVE, hit, -1); + WDNetworkRegistry.INSTANCE.sendToServer(C2SMessageScreenCtrl.laserMove(tes, side, hit)); + }); + + super.tick(); + } +} diff --git a/src/main/java/net/montoyo/wd/client/gui/GuiMinePad.java b/src/main/java/net/montoyo/wd/client/gui/GuiMinePad.java new file mode 100644 index 0000000..b5ff787 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/GuiMinePad.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import com.cinemamod.mcef.MCEFBrowser; +import com.google.gson.JsonObject; +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.core.BlockPos; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; +import net.neoforged.api.distmarker.OnlyIn; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.client.ClientProxy; +import net.montoyo.wd.utilities.browser.WDBrowser; +import net.montoyo.wd.utilities.browser.handlers.js.Scripts; +import net.montoyo.wd.utilities.data.BlockSide; +import org.cef.misc.CefCursorType; +import org.lwjgl.glfw.GLFW; + +import java.util.Optional; + +import static net.neoforged.api.distmarker.Dist.CLIENT; + +@OnlyIn(CLIENT) +public class GuiMinePad extends WDScreen { + + private ClientProxy.PadData pad; + private double vx; + private double vy; + private double vw; + private double vh; + + public GuiMinePad() { + super(Component.nullToEmpty(null)); + } + + public GuiMinePad(ClientProxy.PadData pad) { + this(); + this.pad = pad; + } + + int trueWidth, trueHeight; + + @Override + public void init() { + vw = ((double) width) - 32.0f; + vh = vw / WebDisplays.PAD_RATIO; + vx = 16.0f; + vy = (((double) height) - vh) / 2.0f; + + trueWidth = width; + trueHeight = height; + + this.width = (int) vw; + this.height = (int) vh; + + super.init(); + + ((MCEFBrowser) pad.view).setCursor(CefCursorType.fromId(pad.activeCursor)); + ((MCEFBrowser) pad.view).setCursorChangeListener((id) -> { + pad.activeCursor = id; + ((MCEFBrowser) pad.view).setCursor(CefCursorType.fromId(id)); + }); + } + + private static void addRect(BufferBuilder bb, org.joml.Matrix4f pose, float x, float y, float w, float h) { + bb.addVertex(pose, x, y, 0.0f).setColor(255, 255, 255, 255); + bb.addVertex(pose, x + w, y, 0.0f).setColor(255, 255, 255, 255); + bb.addVertex(pose, x + w, y + h, 0.0f).setColor(255, 255, 255, 255); + bb.addVertex(pose, x, y + h, 0.0f).setColor(255, 255, 255, 255); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float ptt) { + width = trueWidth; + height = trueHeight; + renderBackground(graphics, mouseX, mouseY, ptt); + width = (int) vw; + height = (int) vh; + + RenderSystem.disableCull(); + RenderSystem.setShaderColor(0.73f, 0.73f, 0.73f, 1.0f); + + RenderSystem.setShader(GameRenderer::getPositionColorShader); + BufferBuilder bb = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + org.joml.Matrix4f pose = graphics.pose().last().pose(); + addRect(bb, pose, (float) vx, (float) (vy - 16), (float) vw, 16f); + addRect(bb, pose, (float) vx, (float) (vy + vh), (float) vw, 16f); + addRect(bb, pose, (float) (vx - 16), (float) vy, 16f, (float) vh); + addRect(bb, pose, (float) (vx + vw), (float) vy, 16f, (float) vh); + BufferUploader.drawWithShader(bb.buildOrThrow()); + + if (pad.view != null) { +// pad.view.draw(poseStack, vx, vy + vh, vx + vw, vy); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + RenderSystem.disableDepthTest(); + RenderSystem.setShader(GameRenderer::getPositionTexColorShader); + RenderSystem.setShaderTexture(0, ((MCEFBrowser) pad.view).getRenderer().getTextureID()); + BufferBuilder buffer = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); + double x1 = vx; + double y1 = vy; + double x2 = vx + vw; + double y2 = vy + vh; + buffer.addVertex(graphics.pose().last().pose(), (float) x1, (float) y1, 0.0f).setUv(0.0F, 0.0F).setColor(1f, 1f, 1f, 1f); + buffer.addVertex(graphics.pose().last().pose(), (float) x2, (float) y1, 0.0f).setUv(1.0F, 0.0F).setColor(1f, 1f, 1f, 1f); + buffer.addVertex(graphics.pose().last().pose(), (float) x2, (float) y2, 0.0f).setUv(1.0F, 1.0F).setColor(1f, 1f, 1f, 1f); + buffer.addVertex(graphics.pose().last().pose(), (float) x1, (float) y2, 0.0f).setUv(0.0F, 1.0F).setColor(1f, 1f, 1f, 1f); + BufferUploader.drawWithShader(buffer.buildOrThrow()); + RenderSystem.enableDepthTest(); + } + + RenderSystem.enableCull(); + + graphics.drawString( + minecraft.font, Language.getInstance().getOrDefault( + "webdisplays.gui.minepad.close" + ), (int) vx + 4, (int) vy - minecraft.font.lineHeight - 3, 16777215, true + ); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + return this.keyChanged(keyCode, scanCode, modifiers, true) || super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + return this.keyChanged(keyCode, scanCode, modifiers, false) || super.keyReleased(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char codePoint, int modifiers) { + if (pad.view != null) { + ((MCEFBrowser) pad.view).sendKeyTyped(codePoint, modifiers); + return true; + } else { + return super.charTyped(codePoint, modifiers); + } + } + + /* copied from MCEF */ + public boolean keyChanged(int keyCode, int scanCode, int modifiers, boolean pressed) { + assert minecraft != null; + if ((modifiers & GLFW.GLFW_MOD_SHIFT) == GLFW.GLFW_MOD_SHIFT && keyCode == GLFW.GLFW_KEY_ESCAPE) { + onClose(); + return true; + } + + InputConstants.Key iuKey = InputConstants.getKey(keyCode, scanCode); + String keystr = iuKey.getDisplayName().getString(); +// System.out.println("KEY STR " + keystr); + if (keystr.length() == 0) + return false; + + char key = keystr.charAt(keystr.length() - 1); + + if (keystr.equals("Enter")) { + keyCode = 10; + key = '\n'; + } + + if (pad.view != null) { + if (pressed) + ((MCEFBrowser) pad.view).sendKeyPress(keyCode, scanCode, modifiers); + else + ((MCEFBrowser) pad.view).sendKeyRelease(keyCode, scanCode, modifiers); + + if (pressed && key == '\n') + if (modifiers != 0) ((MCEFBrowser) pad.view).sendKeyTyped('\r', modifiers); + return true; + } + + return false; + } + + @Override + public void mouseMoved(double mouseX, double mouseY) { + super.mouseMoved(mouseX, mouseY); + mouse(-1, false, (int) mouseX, (int) mouseY, 0); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + mouse(button, true, (int) mouseX, (int) mouseY, 0); + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + mouse(button, false, (int) mouseX, (int) mouseY, 0); + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + double mx = (mouseX - vx) / vw; + double my = (mouseY - vy) / vh; + int sx = (int) (mx * WebDisplays.INSTANCE.padResX); + int sy = (int) (my * WebDisplays.INSTANCE.padResY); + // TODO: this doesn't work, and I don't understand why? + ((MCEFBrowser) pad.view).sendMouseWheel(sx, sy, scrollY, (hasControlDown() && !hasAltDown() && !hasShiftDown()) ? GLFW.GLFW_MOD_CONTROL : 0); + + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); + } + + public void capturedMouse(double scaledX, double scaledY, int sx, int sy) { + double centerX = (int) (0.5 * (double) this.minecraft.getWindow().getGuiScaledWidth()); + double centerY = (int) (0.5 * (double) this.minecraft.getWindow().getGuiScaledHeight()); + + if (sx == (int) centerX && sy == (int) centerY) return; + + double mx = (centerX - vx) / vw; + double my = (centerY - vy) / vh; + double scaledCentX = (mx * WebDisplays.INSTANCE.padResX); + double scaledCentY = (my * WebDisplays.INSTANCE.padResY); + + double deltX = scaledX - scaledCentX; + double deltY = scaledY - scaledCentY; + + String scr = Scripts.MOUSE_EVENT; + pad.view.executeJavaScript( + scr + .replace("%xCoord%", "" + (int) centerX) + .replace("%yCoord%", "" + (int) centerY) + .replace("%xDelta%", "" + (deltX)) + .replace("%yDelta%", "" + (deltY)), + "WebDisplays", 0 + ); + + // lock mouse + try { + double xpos = (this.minecraft.getWindow().getScreenWidth() / 2); + double ypos = (this.minecraft.getWindow().getScreenHeight() / 2); + GLFW.glfwSetCursorPos(minecraft.getWindow().getWindow(), xpos, ypos); + } catch (Throwable ignored) { + } + } + + public void mouse(int btn, boolean pressed, int sx, int sy, double scrollAmount) { + double mx = (sx - vx) / vw; + double my = (sy - vy) / vh; + + if (pad.view != null && mx >= 0 && mx <= 1) { + //Scale again according to the webview + int scaledX = (int) (mx * WebDisplays.INSTANCE.padResX); + int scaledY = (int) (my * WebDisplays.INSTANCE.padResY); + + if (btn == -1) { + if (locked) + capturedMouse(mx * WebDisplays.INSTANCE.padResX, my * WebDisplays.INSTANCE.padResY, sx, sy); + else ((MCEFBrowser) pad.view).sendMouseMove(scaledX, scaledY); + } else if (pressed) + ((MCEFBrowser) pad.view).sendMousePress(scaledX, scaledY, btn); + else ((MCEFBrowser) pad.view).sendMouseRelease(scaledX, scaledY, btn); + pad.view.setFocus(true); + } + } + + public static Optional getChar(int keyCode, int scanCode) { + String keystr = GLFW.glfwGetKeyName(keyCode, scanCode); + if (keystr == null) { + keystr = "\0"; + } + if (keyCode == GLFW.GLFW_KEY_ENTER) { + keystr = "\n"; + } + if (keystr.length() == 0) { + return Optional.empty(); + } + + return Optional.of(keystr.charAt(keystr.length() - 1)); + } + + @Override + public void tick() { + if (pad.view == null) + minecraft.setScreen(null); //In case the user dies with the pad in the hand + pollElement(); + } + + @Override + public boolean isForBlock(BlockPos bp, BlockSide side) { + return false; + } + + @Override + public void removed() { + super.removed(); + InputConstants.updateRawMouseInput( + minecraft.getWindow().getWindow(), + Minecraft.getInstance().options.rawMouseInput().get() + ); + if (pad.view instanceof MCEFBrowser browser) { + browser.setCursor(CefCursorType.POINTER); + browser.setCursorChangeListener((cursor) -> { + pad.activeCursor = cursor; + }); + } + } + + @Override + public void onClose() { + super.onClose(); + removed(); + this.minecraft.popGuiLayer(); + } + + boolean locked = false; + double lockCenterX = -1; + double lockCenterY = -1; + + protected void updateCrd(JsonObject obj) { + if (obj.getAsJsonPrimitive("exists").getAsBoolean()) { + locked = true; + RenderSystem.recordRenderCall(() -> { + InputConstants.updateRawMouseInput( + minecraft.getWindow().getWindow(), + obj.getAsJsonPrimitive("unadjust").getAsBoolean() + ); + GLFW.glfwSetInputMode(Minecraft.getInstance().getWindow().getWindow(), 208897, GLFW.GLFW_CURSOR_DISABLED); + }); + lockCenterX = obj.getAsJsonPrimitive("x").getAsDouble() + obj.getAsJsonPrimitive("w").getAsDouble() / 2; + lockCenterY = obj.getAsJsonPrimitive("y").getAsDouble() + obj.getAsJsonPrimitive("h").getAsDouble() / 2; + } else { + if (locked) { + locked = false; + RenderSystem.recordRenderCall(()->{ + InputConstants.updateRawMouseInput( + minecraft.getWindow().getWindow(), + Minecraft.getInstance().options.rawMouseInput().get() + ); + GLFW.glfwSetInputMode(Minecraft.getInstance().getWindow().getWindow(), 208897, GLFW.GLFW_CURSOR_NORMAL); + GLFW.glfwSetCursor(Minecraft.getInstance().getWindow().getWindow(), CefCursorType.fromId(pad.activeCursor).glfwId); + }); + } + } + } + + protected void pollElement() { + if (pad.view instanceof WDBrowser browser) { + JsonObject object = browser.pointerLockElement().getObj(); + if (object != null) updateCrd(object); + } + } +} diff --git a/src/main/java/net/montoyo/wd/client/gui/GuiRedstoneCtrl.java b/src/main/java/net/montoyo/wd/client/gui/GuiRedstoneCtrl.java new file mode 100644 index 0000000..8475283 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/GuiRedstoneCtrl.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.montoyo.wd.client.gui.controls.Button; +import net.montoyo.wd.client.gui.controls.TextField; +import net.montoyo.wd.client.gui.loading.FillControl; +import net.montoyo.wd.utilities.data.BlockSide; +import net.montoyo.wd.utilities.math.Vector3i; + +import javax.annotation.Nullable; + +public class GuiRedstoneCtrl extends WDScreen { + + private ResourceLocation dimension; + private Vector3i pos; + private String risingEdgeURL; + private String fallingEdgeURL; + + @FillControl + private TextField tfRisingEdge; + + @FillControl + private TextField tfFallingEdge; + + @FillControl + private Button btnOk; + + public GuiRedstoneCtrl(Component component, ResourceLocation d, Vector3i p, String r, String f) { + super(component); + dimension = d; + pos = p; + risingEdgeURL = r; + fallingEdgeURL = f; + } + + @Override + public void init() { + super.init(); + loadFrom(ResourceLocation.fromNamespaceAndPath("webdisplays", "gui/redstonectrl.json")); + tfRisingEdge.setText(risingEdgeURL); + tfFallingEdge.setText(fallingEdgeURL); + } + +// @GuiSubscribe +// public void onClick(Button.ClickEvent ev) { +// if(ev.getSource() == btnOk) { +// API mcef = ((ClientProxy) WebDisplays.PROXY).getMCEF(); +// +// String rising = mcef.punycode(Util.addProtocol(tfRisingEdge.getText())); +// String falling = mcef.punycode(Util.addProtocol(tfFallingEdge.getText())); +// WDNetworkRegistry.INSTANCE.sendToServer(new C2SMessageRedstoneCtrl(pos, rising, falling)); +// } +// +// minecraft.setScreen(null); +// } + + @Override + public boolean isForBlock(BlockPos bp, BlockSide side) { + return pos.equalsBlockPos(bp); + } + + @Nullable + @Override + public String getWikiPageName() { + return "Redstone_Controller"; + } + +} diff --git a/src/main/java/net/montoyo/wd/client/gui/GuiScreenConfig.java b/src/main/java/net/montoyo/wd/client/gui/GuiScreenConfig.java new file mode 100644 index 0000000..81bb572 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/GuiScreenConfig.java @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.client.gui.controls.*; +import net.montoyo.wd.client.gui.loading.FillControl; +import net.montoyo.wd.core.ScreenRights; +import net.montoyo.wd.entity.ScreenData; +import net.montoyo.wd.entity.ScreenBlockEntity; +import net.montoyo.wd.item.WDItem; +import net.montoyo.wd.net.WDNetworkRegistry; +import net.montoyo.wd.net.server_bound.C2SMessageScreenCtrl; +import net.montoyo.wd.utilities.*; +import net.montoyo.wd.utilities.math.Vector2i; +import net.montoyo.wd.utilities.data.BlockSide; +import net.montoyo.wd.utilities.data.Rotation; +import net.montoyo.wd.utilities.serialization.NameUUIDPair; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.UUID; + +public class GuiScreenConfig extends WDScreen { + + //Screen data + private final ScreenBlockEntity tes; + private final BlockSide side; + private NameUUIDPair owner; + private NameUUIDPair[] friends; + private int friendRights; + private int otherRights; + private Rotation rotation = Rotation.ROT_0; + private float aspectRatio; + + //Autocomplete handling + private boolean waitingAC; + private int acFailTicks = -1; + + private final ArrayList acResults = new ArrayList<>(); + private boolean adding; + + //Controls + @FillControl + private Label lblOwner; + + @FillControl + private List lstFriends; + + @FillControl + private Button btnAdd; + + @FillControl + private TextField tfFriend; + + @FillControl + private TextField tfResX; + + @FillControl + private TextField tfResY; + + @FillControl + private ControlGroup grpFriends; + + @FillControl + private ControlGroup grpOthers; + + @FillControl + private CheckBox boxFSetUrl; + + @FillControl + private CheckBox boxFClick; + + @FillControl + private CheckBox boxFFriends; + + @FillControl + private CheckBox boxFOthers; + + @FillControl + private CheckBox boxFUpgrades; + + @FillControl + private CheckBox boxFResolution; + + @FillControl + private CheckBox boxOSetUrl; + + @FillControl + private CheckBox boxOClick; + + @FillControl + private CheckBox boxOUpgrades; + + @FillControl + private CheckBox boxOResolution; + + @FillControl + private Button btnSetRes; + + @FillControl + private UpgradeGroup ugUpgrades; + + @FillControl + private Button btnChangeRot; + + @FillControl + private CheckBox cbLockRatio; + + @FillControl + private CheckBox cbAutoVolume; + + private CheckBox[] friendBoxes; + private CheckBox[] otherBoxes; + + public GuiScreenConfig(Component component, ScreenBlockEntity tes, BlockSide side, NameUUIDPair[] friends, int fr, int or) { + super(component); + this.tes = tes; + this.side = side; + this.friends = friends; + friendRights = fr; + otherRights = or; + } + + @Override + public void init() { + super.init(); + loadFrom(ResourceLocation.fromNamespaceAndPath("webdisplays", "gui/screencfg.json")); + + friendBoxes = new CheckBox[] { boxFResolution, boxFUpgrades, boxFOthers, boxFFriends, boxFClick, boxFSetUrl }; + boxFResolution.setUserdata(ScreenRights.MODIFY_SCREEN); + boxFUpgrades.setUserdata(ScreenRights.MANAGE_UPGRADES); + boxFOthers.setUserdata(ScreenRights.MANAGE_OTHER_RIGHTS); + boxFFriends.setUserdata(ScreenRights.MANAGE_FRIEND_LIST); + boxFClick.setUserdata(ScreenRights.INTERACT); + boxFSetUrl.setUserdata(ScreenRights.CHANGE_URL); + + otherBoxes = new CheckBox[] { boxOResolution, boxOUpgrades, boxOClick, boxOSetUrl }; + boxOResolution.setUserdata(ScreenRights.MODIFY_SCREEN); + boxOUpgrades.setUserdata(ScreenRights.MANAGE_UPGRADES); + boxOClick.setUserdata(ScreenRights.INTERACT); + boxOSetUrl.setUserdata(ScreenRights.CHANGE_URL); + + ScreenData scr = tes.getScreen(side); + if(scr != null) { + owner = scr.owner; + rotation = scr.rotation; + + tfResX.setText("" + scr.resolution.x); + tfResY.setText("" + scr.resolution.y); + aspectRatio = ((float) scr.resolution.x) / ((float) scr.resolution.y); + + //Hopefully upgrades have been synchronized... + ugUpgrades.setUpgrades(scr.upgrades); + cbAutoVolume.setChecked(scr.autoVolume); + } + + if(owner == null) + owner = new NameUUIDPair("???", UUID.randomUUID()); + + lblOwner.setLabel(lblOwner.getLabel() + ' ' + owner.name); + for(NameUUIDPair f : friends) + lstFriends.addElementRaw(f.name, f); + + lstFriends.updateContent(); + updateRights(friendRights, friendRights, friendBoxes, true); + updateRights(otherRights, otherRights, otherBoxes, true); + updateMyRights(); + updateRotationStr(); + + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI( WebDisplays.INSTANCE.soundScreenCfg, 1.0f, 1.0f)); + } + + private void updateRotationStr() { + btnChangeRot.setLabel(I18n.get("webdisplays.gui.screencfg.rot" + rotation.getAngleAsInt())); + } + + private void addFriend(String name) { + if(!name.isEmpty()) { + requestAutocomplete(name, true); + tfFriend.setDisabled(true); + adding = true; + waitingAC = true; + } + } + + private void clickSetRes() { + ScreenData scr = tes.getScreen(side); + if(scr == null) + return; //WHATDAFUQ? + + try { + int x = Integer.parseInt(tfResX.getText()); + int y = Integer.parseInt(tfResY.getText()); + if(x < 1 || y < 1) + throw new NumberFormatException(); //I'm lazy + + if(x != scr.resolution.x || y != scr.resolution.y) + WDNetworkRegistry.INSTANCE.sendToServer(C2SMessageScreenCtrl.resolution(tes, side, new Vector2i(x, y))); + } catch(NumberFormatException ex) { + //Roll back + tfResX.setText("" + scr.resolution.x); + tfResY.setText("" + scr.resolution.y); + } + + btnSetRes.setDisabled(true); + } + + @GuiSubscribe + public void onClick(Button.ClickEvent ev) { + if(ev.getSource() == btnAdd && !waitingAC) + addFriend(tfFriend.getText().trim()); + else if(ev.getSource() == btnSetRes) + clickSetRes(); + else if(ev.getSource() == btnChangeRot) { + Rotation[] rots = Rotation.values(); + WDNetworkRegistry.INSTANCE.sendToServer(new C2SMessageScreenCtrl(tes, side, rots[(rotation.ordinal() + 1) % rots.length])); + } + } + + @GuiSubscribe + public void onEnterPressed(TextField.EnterPressedEvent ev) { + if(ev.getSource() == tfFriend && !waitingAC) + addFriend(ev.getText().trim()); + else if((ev.getSource() == tfResX || ev.getSource() == tfResY) && !btnSetRes.isDisabled()) + clickSetRes(); + } + + @GuiSubscribe + public void onAutocomplete(TextField.TabPressedEvent ev) { + if(ev.getSource() == tfFriend && !waitingAC && !ev.getBeginning().isEmpty()) { + if(acResults.isEmpty()) { + waitingAC = true; + requestAutocomplete(ev.getBeginning(), false); + } else { + NameUUIDPair pair = acResults.remove(0); + tfFriend.setText(pair.name); + } + } else if(ev.getSource() == tfResX) { + tfResX.setFocused(false); + tfResY.focus(); + tfResY.getMcField().setCursorPosition(0); + tfResY.getMcField().setHighlightPos(tfResY.getText().length()); + } + } + + @GuiSubscribe + public void onTextChanged(TextField.TextChangedEvent ev) { + if(ev.getSource() == tfResX || ev.getSource() == tfResY) { + for(int i = 0; i < ev.getNewContent().length(); i++) { + if(!Character.isDigit(ev.getNewContent().charAt(i))) { + ev.getSource().setText(ev.getOldContent()); + return; + } + } + + if(cbLockRatio.isChecked()) { + if(ev.getSource() == tfResX) { + try { + float val = (float) Integer.parseInt(ev.getNewContent()); + val /= aspectRatio; + tfResY.setText("" + ((int) val)); + } catch(NumberFormatException ex) {} + } else { + try { + float val = (float) Integer.parseInt(ev.getNewContent()); + val *= aspectRatio; + tfResX.setText("" + ((int) val)); + } catch(NumberFormatException ex) {} + } + } + + btnSetRes.setDisabled(false); + } + } + + @GuiSubscribe + public void onRemovePlayer(List.EntryClick ev) { + if(ev.getSource() == lstFriends) + WDNetworkRegistry.INSTANCE.sendToServer(new C2SMessageScreenCtrl(tes, side, (NameUUIDPair) ev.getUserdata(), true)); + } + + @GuiSubscribe + public void onCheckboxChanged(CheckBox.CheckedEvent ev) { + if(isFriendCheckbox(ev.getSource())) { + int flag = (Integer) ev.getSource().getUserdata(); + if(ev.isChecked()) + friendRights |= flag; + else + friendRights &= ~flag; + + requestSync(); + } else if(isOtherCheckbox(ev.getSource())) { + int flag = (Integer) ev.getSource().getUserdata(); + if(ev.isChecked()) + otherRights |= flag; + else + otherRights &= ~flag; + + requestSync(); + } else if(ev.getSource() == cbLockRatio && ev.isChecked()) { + try { + int x = Integer.parseInt(tfResX.getText()); + int y = Integer.parseInt(tfResY.getText()); + + aspectRatio = ((float) x) / ((float) y); + } catch(NumberFormatException ex) { + cbLockRatio.setChecked(false); + } + } else if(ev.getSource() == cbAutoVolume) WDNetworkRegistry.INSTANCE.sendToServer(C2SMessageScreenCtrl.autoVol(tes, side, ev.isChecked())); + } + + @GuiSubscribe + public void onRemoveUpgrade(UpgradeGroup.ClickEvent ev) { + WDNetworkRegistry.INSTANCE.sendToServer(new C2SMessageScreenCtrl(tes, side, ev.getMouseOverStack())); + } + + public boolean isFriendCheckbox(CheckBox cb) { + return Arrays.stream(friendBoxes).anyMatch(fb -> cb == fb); + } + + public boolean isOtherCheckbox(CheckBox cb) { + return Arrays.stream(otherBoxes).anyMatch(ob -> cb == ob); + } + + public boolean hasFriend(NameUUIDPair f) { + return Arrays.stream(friends).anyMatch(f::equals); + } + + @Override + public void onAutocompleteResult(NameUUIDPair pairs[]) { + waitingAC = false; + + if(adding) { + if(!hasFriend(pairs[0])) + WDNetworkRegistry.INSTANCE.sendToServer(new C2SMessageScreenCtrl(tes, side, pairs[0], false)); + + tfFriend.setDisabled(false); + tfFriend.clear(); + tfFriend.focus(); + adding = false; + } else { + acResults.clear(); + acResults.addAll(Arrays.asList(pairs)); + + NameUUIDPair pair = acResults.remove(0); + tfFriend.setText(pair.name); + } + } + + @Override + public void onAutocompleteFailure() { + waitingAC = false; + acResults.clear(); + acFailTicks = 0; + tfFriend.setTextColor(Control.COLOR_RED); + + if(adding) { + tfFriend.setDisabled(false); + adding = false; + } + } + + @Override + public void tick() { + super.tick(); + + if(acFailTicks >= 0) { + if(++acFailTicks >= 10) { + acFailTicks = -1; + tfFriend.setTextColor(TextField.DEFAULT_TEXT_COLOR); + } + } + } + + public void updateFriends(NameUUIDPair[] friends) { + boolean diff = false; + if(friends.length != this.friends.length) + diff = true; + else { + for(NameUUIDPair pair : friends) { + if(!hasFriend(pair)) { + diff = true; + break; + } + } + } + + if(diff) { + this.friends = friends; + lstFriends.clearRaw(); + for(NameUUIDPair pair : friends) + lstFriends.addElementRaw(pair.name, pair); + + lstFriends.updateContent(); + } + } + + private int updateRights(int current, int newVal, CheckBox[] boxes, boolean force) { + if(force || current != newVal) { + for(CheckBox box : boxes) { + int flag = (Integer) box.getUserdata(); + box.setChecked((newVal & flag) != 0); + } + + if(!force) { + Log.info("Screen check boxes were updated"); + abortSync(); //Value changed by another user, abort modifications by local user + } + } + + return newVal; + } + + public void updateFriendRights(int rights) { + friendRights = updateRights(friendRights, rights, friendBoxes, false); + } + + public void updateOtherRights(int rights) { + otherRights = updateRights(otherRights, rights, otherBoxes, false); + } + + @Override + protected void sync() { + WDNetworkRegistry.INSTANCE.sendToServer(new C2SMessageScreenCtrl(tes, side, friendRights, otherRights)); + Log.info("Sent sync packet"); + } + + public void updateMyRights() { + NameUUIDPair me = new NameUUIDPair(minecraft.player.getGameProfile()); + int myRights; + boolean clientIsOwner = false; + + if(me.equals(owner)) { + myRights = ScreenRights.ALL; + clientIsOwner = true; + } else if(hasFriend(me)) + myRights = friendRights; + else + myRights = otherRights; + + //Disable components according to client rights + grpFriends.setDisabled(!clientIsOwner); + + boolean flag = (myRights & ScreenRights.MANAGE_FRIEND_LIST) == 0; + lstFriends.setDisabled(flag); + tfFriend.setDisabled(flag); + btnAdd.setDisabled(flag); + + flag = (myRights & ScreenRights.MANAGE_OTHER_RIGHTS) == 0; + grpOthers.setDisabled(flag); + + flag = (myRights & ScreenRights.MODIFY_SCREEN) == 0; + tfResX.setDisabled(flag); + tfResY.setDisabled(flag); + btnChangeRot.setDisabled(flag); + + if(flag) + btnSetRes.setDisabled(true); + + flag = (myRights & ScreenRights.MANAGE_UPGRADES) == 0; + ugUpgrades.setDisabled(flag); + cbAutoVolume.setDisabled(flag); + } + + public void updateResolution(Vector2i res) { + aspectRatio = ((float) res.x) / ((float) res.y); + tfResX.setText("" + res.x); + tfResY.setText("" + res.y); + btnSetRes.setDisabled(true); + } + + public void updateRotation(Rotation rot) { + rotation = rot; + updateRotationStr(); + } + + public void updateAutoVolume(boolean av) { + cbAutoVolume.setChecked(av); + } + + @Override + public boolean isForBlock(BlockPos bp, BlockSide side) { + return bp.equals(tes.getBlockPos()) && side == this.side; + } + + @Nullable + @Override + public String getWikiPageName() { + ItemStack is = ugUpgrades.getMouseOverUpgrade(); + if(is != null) { + if(is.getItem() instanceof WDItem) + return ((WDItem) is.getItem()).getWikiName(is); + else + return null; + } + + return "Screen_Configurator"; + } + + // reason: allow closing the UI, lol + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + Minecraft.getInstance().setScreen(null); + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + +} diff --git a/src/main/java/net/montoyo/wd/client/gui/GuiServer.java b/src/main/java/net/montoyo/wd/client/gui/GuiServer.java new file mode 100644 index 0000000..d5ae64c --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/GuiServer.java @@ -0,0 +1,810 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.*; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.miniserv.Constants; +import net.montoyo.wd.miniserv.client.*; +import net.montoyo.wd.net.WDNetworkRegistry; +import net.montoyo.wd.utilities.*; +import net.montoyo.wd.utilities.math.Vector3i; +import net.montoyo.wd.utilities.data.BlockSide; +import net.montoyo.wd.utilities.serialization.NameUUIDPair; +import net.montoyo.wd.utilities.serialization.Util; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nullable; +import javax.swing.filechooser.FileSystemView; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.function.Supplier; + +import static net.montoyo.wd.client.gui.GuiMinePad.getChar; + +public class GuiServer extends WDScreen { + + private static final ResourceLocation BG_IMAGE = ResourceLocation.fromNamespaceAndPath("webdisplays", "textures/gui/server_bg.png"); + private static final ResourceLocation FG_IMAGE = ResourceLocation.fromNamespaceAndPath("webdisplays", "textures/gui/server_fg.png"); + private static final HashMap COMMAND_MAP = new HashMap<>(); + private static final int MAX_LINE_LEN = 32; + private static final int MAX_LINES = 12; + + private final Vector3i serverPos; + private final NameUUIDPair owner; + private final ArrayList lines = new ArrayList<>(); + private String prompt = ""; + private String userPrompt; + private int blinkTime; + private String lastCmd; + private boolean promptLocked; + private volatile long queryTime; + private ClientTask currentTask; + private int selectedLine = -1; + + //Access command + private int accessTrials; + private int accessTime; + private int accessState = -1; + private SimpleSoundInstance accessSound; + + //Upload wizard + private boolean uploadWizard; + private File uploadDir; + private final ArrayList uploadFiles = new ArrayList<>(); + private int uploadOffset; + private boolean uploadFirstIsParent; + private String uploadFilter = ""; + private long uploadFilterTime; + + public GuiServer(Vector3i vec, NameUUIDPair owner) { + super(Component.nullToEmpty(null)); + serverPos = vec; + this.owner = owner; + userPrompt = "> "; + + if (COMMAND_MAP.isEmpty()) + buildCommandMap(); + + lines.add("MiniServ 1.0"); + lines.add(tr("info")); + uploadCD(FileSystemView.getFileSystemView().getDefaultDirectory()); + } + + private static String tr(String key, Object... args) { + return I18n.get("webdisplays.server." + key, args); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float ptt) { + super.render(graphics, mouseX, mouseY, ptt); + + int x = (width - 256) / 2; + int y = (height - 176) / 2; + +// RenderSystem.enableTexture(); + RenderSystem.setShaderTexture(0, BG_IMAGE); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + graphics.blit(BG_IMAGE, x, y, 0, 0, 256, 256); + + x += 18; + y += 18; + + for (int i = 0; i < lines.size(); i++) { + if (selectedLine == i) { + drawWhiteQuad(x - 1, y - 2, font.width(lines.get(i)) + 1, 12); + graphics.drawString(Minecraft.getInstance().font, lines.get(i), x, y, 0xFF129700, false); + } else + graphics.drawString(Minecraft.getInstance().font, lines.get(i), x, y, 0xFFFFFFFF, false); + + y += 12; + } + + if (!promptLocked) { + if (queue.isEmpty()) { + x = graphics.drawString(Minecraft.getInstance().font, userPrompt, x, y, 0xFFFFFFFF, false); + x = graphics.drawString(Minecraft.getInstance().font, prompt, x, y, 0xFFFFFFFF, false); + } else { + x = graphics.drawString(Minecraft.getInstance().font, tr("press_for_more"), x, y, 0xFFFFFFFF, false); + } + } + + if (!uploadWizard && blinkTime < 5) + drawWhiteQuad(x + 1, y, 6, 8); + +// RenderSystem.enableTexture(); + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + RenderSystem.setShaderTexture(0, FG_IMAGE); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); +// blit(graphics,(width - 256) / 2, (height - 176) / 2, 0, 0, 256, 176); + } + + private void drawWhiteQuad(int x, int y, int w, int h) { + float xd = (float) x; + float xd2 = (float) (x + w); + float yd = (float) y; + float yd2 = (float) (y + h); + float zd = (float) getBlitOffset(); + +// RenderSystem.disableTexture(); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + RenderSystem.setShader(GameRenderer::getPositionShader); + BufferBuilder bb = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION); + bb.addVertex(xd, yd2, zd); + bb.addVertex(xd2, yd2, zd); + bb.addVertex(xd2, yd, zd); + bb.addVertex(xd, yd, zd); + BufferUploader.drawWithShader(bb.buildOrThrow()); +// RenderSystem.enableTexture(); + } + + private float getBlitOffset() { + return 0; + } + + @Override + public void tick() { + super.tick(); + + if (accessState >= 0) { + if (--accessTime <= 0) { + accessState++; + + if (accessState == 1) { + if (lines.size() > 0) + lines.remove(lines.size() - 1); + + lines.add("access: PERMISSION DENIED....and..."); + accessTime = 20; + } else { + if (accessSound == null) { + accessSound = new SimpleSoundInstance(WebDisplays.INSTANCE.soundServer.getLocation(), SoundSource.MASTER, 1.0f, 1.0f, RandomSource.create(), true, 0, SoundInstance.Attenuation.NONE, 0.0f, 0.0f, 0.0f, false); + minecraft.getSoundManager().play(accessSound); + } + + writeLine("YOU DIDN'T SAY THE MAGIC WORD!"); + accessTime = 2; + } + } + } else { + blinkTime = (blinkTime + 1) % 10; + + if (currentTask != null) { + long queryTime; + synchronized (this) { + queryTime = this.queryTime; + } + + if (System.currentTimeMillis() - queryTime >= 10000) { + writeLine(tr("timeout")); + currentTask.cancel(); + clearTask(); + } + } + + if (!uploadFilter.isEmpty() && System.currentTimeMillis() - uploadFilterTime >= 1000) { + Log.info("Upload filter cleared"); + uploadFilter = ""; + } + } + + final int maxl = uploadWizard ? MAX_LINES : (MAX_LINES - 1); //Cuz prompt is hidden + if (!queue.isEmpty()) { + while (!queue.isEmpty()) { + if (lines.size() >= maxl) + break; + writeLine(queue.remove(0)); + } + } + + while (lines.size() > maxl) + lines.remove(0); + } + + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + Supplier predicate = () -> super.keyReleased(keyCode, scanCode, modifiers); + + try { + return handleKeyboardInput(keyCode, false, predicate); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE && !uploadWizard) { + Minecraft.getInstance().setScreen(null); + return true; + } + + getChar(keyCode, scanCode).ifPresent(c -> { + try { + keyTyped(c, keyCode, modifiers); + } catch (IOException e) { + e.printStackTrace(); + } + }); + + try { + return handleKeyboardInput(keyCode, true, () -> true); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + public boolean handleKeyboardInput(int keyCode, boolean keyState, Supplier booleanSupplier) throws IOException { + if (!queue.isEmpty()) + return false; + + if (uploadWizard) { + if (keyState) { + if (keyCode == GLFW.GLFW_KEY_UP) { + if (selectedLine > 3) + selectedLine--; + else if (uploadOffset > 0) { + uploadOffset--; + updateUploadScreen(); + } + } else if (keyCode == GLFW.GLFW_KEY_DOWN) { + if (selectedLine < MAX_LINES - 1) + selectedLine++; + else if (uploadOffset + selectedLine - 2 < uploadFiles.size()) { + uploadOffset++; + updateUploadScreen(); + } + } else if (keyCode == GLFW.GLFW_KEY_PAGE_DOWN) { + selectedLine = 3; + int dst = uploadOffset - (MAX_LINES - 3); + if (dst < 0) + dst = 0; + + selectFile(dst); + } else if (keyCode == GLFW.GLFW_KEY_PAGE_UP) { + selectedLine = 3; + int dst = uploadOffset + (MAX_LINES - 3); + if (dst >= uploadFiles.size()) + dst = uploadFiles.size() - 1; + + selectFile(dst); + } else if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { + File file = uploadFiles.get(uploadOffset + selectedLine - 3); + + if (file.isDirectory()) { + uploadCD(file); + updateUploadScreen(); + } else + startFileUpload(file, true); + } else if (keyCode == GLFW.GLFW_KEY_F5) { + uploadCD(uploadDir); + updateUploadScreen(); + } + } + + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + quitUploadWizard(); + return true; //Don't let the screen handle this + } + + return booleanSupplier.get(); + } else { + boolean value = booleanSupplier.get(); + + if (keyState) { + boolean ctrl = Screen.hasControlDown(); + + if (keyCode == GLFW.GLFW_KEY_L && ctrl) + lines.clear(); + else if (keyCode == GLFW.GLFW_KEY_V && ctrl) { + prompt += Minecraft.getInstance().keyboardHandler.getClipboard(); + + if (prompt.length() > MAX_LINE_LEN) + prompt = prompt.substring(0, MAX_LINE_LEN); + } else if (keyCode == GLFW.GLFW_KEY_UP) { + if (lastCmd != null) { + String tmp = prompt; + prompt = lastCmd; + lastCmd = tmp; + } + } + } + + return value; + } + } + + @Override + public boolean charTyped(char codePoint, int modifiers) { + return super.charTyped(codePoint, modifiers); + + } + + protected void keyTyped(char typedChar, int keyCode, int modifier) throws IOException { + //this.charTyped(typedChar, modifier); + + if (keyCode == GLFW.GLFW_KEY_DOWN) { + if (!queue.isEmpty()) { + writeLine(queue.remove(0)); + return; + } + } + if (!queue.isEmpty()) + return; + + if (uploadWizard) { + boolean found = false; + uploadFilter += Character.toLowerCase(typedChar); + uploadFilterTime = System.currentTimeMillis(); + + for (int i = uploadFirstIsParent ? 1 : 0; i < uploadFiles.size(); i++) { + if (uploadFiles.get(i).getName().toLowerCase().startsWith(uploadFilter)) { + selectFile(i); + found = true; + break; + } + } + + if (!found && uploadFilter.length() == 1) + uploadFilter = ""; + + return; + } else if (promptLocked) + return; + + if (keyCode == GLFW.GLFW_KEY_SPACE) + typedChar = ' '; + if ( + (typedChar == 'v' || typedChar == 'V') && + (modifier & 2) == 2 + ) return; + + if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { + if (prompt.length() > 0) + prompt = prompt.substring(0, prompt.length() - 1); + } else if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { + if (prompt.length() > 0) { + writeLine(userPrompt + prompt); + evaluateCommand(prompt); + lastCmd = prompt; + prompt = ""; + } else + writeLine(userPrompt); + } else if (prompt.length() + 1 < MAX_LINE_LEN && typedChar >= 32 && typedChar <= 126) + prompt = prompt + typedChar; + + blinkTime = 0; + } + + private void evaluateCommand(String str) { + String[] args = str.trim().split("\\s+"); + Method handler = COMMAND_MAP.get(args[0].toLowerCase()); + + if (handler == null) { + writeLine(tr("unknowncmd")); + return; + } + + Object[] params; + if (handler.getParameterCount() == 0) + params = new Object[0]; + else { + String[] args2 = new String[args.length - 1]; + System.arraycopy(args, 1, args2, 0, args2.length); + params = new Object[]{args2}; + } + + try { + handler.invoke(this, params); + } catch (IllegalAccessException | InvocationTargetException e) { + Log.errorEx("Caught exception while running command \"%s\"", e, str); + writeLine(tr("error")); + } + } + + private void writeLine(String line) { + final int maxl = uploadWizard ? MAX_LINES : (MAX_LINES - 1); //Cuz prompt is hidden + while (lines.size() >= maxl) + lines.remove(0); + + lines.add(line); + } + + private static void buildCommandMap() { + COMMAND_MAP.clear(); + + Method[] methods = GuiServer.class.getMethods(); + for (Method m : methods) { + CommandHandler cmd = m.getAnnotation(CommandHandler.class); + + if (cmd != null && Modifier.isPublic(m.getModifiers())) { + if (m.getParameterCount() == 0 || (m.getParameterCount() == 1 && m.getParameterTypes()[0] == String[].class)) + COMMAND_MAP.put(cmd.value().toLowerCase(), m); + } + } + } + + private void quitUploadWizard() { + lines.clear(); + promptLocked = false; + uploadWizard = false; + selectedLine = -1; + } + + @Override + public void onClose() { + super.onClose(); + + if (accessSound != null) + Minecraft.getInstance().getSoundManager().stop(accessSound); + } + + private boolean queueTask(ClientTask task) { + if (Client.getInstance().addTask(task)) { + promptLocked = true; + queryTime = System.currentTimeMillis(); //No task is running so it's okay to have an unsynchronized access here + currentTask = task; + return true; + } else { + writeLine(tr("queryerr")); + return false; + } + } + + private void clearTask() { + promptLocked = false; + currentTask = null; + } + + private static String trimStringL(String str) { + int delta = str.length() - MAX_LINE_LEN; + if (delta <= 0) + return str; + + return "..." + str.substring(delta + 3); + } + + private static String trimStringR(String str) { + return (str.length() <= MAX_LINE_LEN) ? str : (str.substring(0, MAX_LINE_LEN - 3) + "..."); + } + + @CommandHandler("clear") + public void commandClear() { + lines.clear(); + } + + @CommandHandler("help") + public void commandHelp(String[] args) { + queueRead = lines.size(); + + if (args.length > 0) { + String cmd = args[0].toLowerCase(); + + if (COMMAND_MAP.containsKey(cmd)) + queueLine(tr("help." + cmd)); + else + queueLine(tr("unknowncmd")); + } else { + for (String c : COMMAND_MAP.keySet()) + queueLine(c + " - " + tr("help." + c)); + } + } + + @CommandHandler("exit") + public void commandExit() { + minecraft.setScreen(null); + } + + @CommandHandler("access") + public void commandAccess(String[] args) { + boolean handled = false; + + if (args.length >= 1 && args[0].equalsIgnoreCase("security")) { + if (args.length == 1 || (args.length == 2 && args[1].equalsIgnoreCase("grid"))) + handled = true; + } else if (args.length == 3 && args[0].equalsIgnoreCase("main") && args[1].equalsIgnoreCase("security") && args[2].equalsIgnoreCase("grid")) + handled = true; + + if (handled) { + writeLine("access: PERMISSION DENIED."); + + if (++accessTrials >= 3) { + promptLocked = true; + accessState = 0; + accessTime = 20; + } + } else + writeLine(tr("argerror")); + } + + @CommandHandler("owner") + public void commandOwner() { + writeLine(tr("ownername", owner.name)); + writeLine(tr("owneruuid")); + writeLine(owner.uuid.toString()); + } + + @CommandHandler("quota") + public void commandQuota() { + if (!minecraft.player.getGameProfile().getId().equals(owner.uuid)) { + writeLine(tr("errowner")); + return; + } + + ClientTaskGetQuota task = new ClientTaskGetQuota(); + task.setFinishCallback((t) -> { + writeLine(tr("quota", Util.sizeString(t.getQuota()), Util.sizeString(t.getMaxQuota()))); + clearTask(); + }); + + queueTask(task); + } + + @CommandHandler("ls") + public void commandList() { + ClientTaskGetFileList task = new ClientTaskGetFileList(owner.uuid); + task.setFinishCallback((t) -> { + String[] files = t.getFileList(); + if (files != null) + Arrays.stream(files).forEach(this::writeLine); + + clearTask(); + }); + + queueTask(task); + } + + @CommandHandler("url") + public void commandURL(String[] args) { + if (args.length < 1) { + writeLine(tr("fnamearg")); + return; + } + + String fname = Util.join(args, " "); + if (Util.isFileNameInvalid(fname)) { + writeLine(tr("nameerr")); + return; + } + + ClientTaskCheckFile task = new ClientTaskCheckFile(owner.uuid, fname); + task.setFinishCallback((t) -> { + int status = t.getStatus(); + if (status == 0) { + writeLine(tr("urlcopied")); + Minecraft.getInstance().keyboardHandler.setClipboard(t.getURL()); + } else if (status == Constants.GETF_STATUS_NOT_FOUND) + writeLine(tr("notfound")); + else + writeLine(tr("error2", status)); + + clearTask(); + }); + + queueTask(task); + } + + private void uploadCD(File newDir) { + try { + uploadDir = newDir.getCanonicalFile(); + } catch (IOException ex) { + uploadDir = newDir; + } + + uploadFiles.clear(); + File parent = uploadDir.getParentFile(); + + if (parent != null && parent.exists()) { + uploadFiles.add(parent); + uploadFirstIsParent = true; + } else + uploadFirstIsParent = false; + + File[] children = uploadDir.listFiles(); + if (children != null) { + Collator c = Collator.getInstance(); + c.setStrength(Collator.SECONDARY); + c.setDecomposition(Collator.CANONICAL_DECOMPOSITION); + + Arrays.stream(children).filter(f -> !f.isHidden() && (f.isDirectory() || f.isFile())).sorted((a, b) -> c.compare(a.getName(), b.getName())).forEach(uploadFiles::add); + } + + uploadOffset = 0; + uploadFilter = ""; + + if (uploadWizard) + selectedLine = 3; + } + + private void updateUploadScreen() { + lines.clear(); + + lines.add(tr("upload.info")); + lines.add(trimStringL(uploadDir.getPath())); + lines.add(""); + + for (int i = uploadOffset; i < uploadFiles.size() && lines.size() < MAX_LINES; i++) { + if (i == 0 && uploadFirstIsParent) + lines.add(tr("upload.parent")); + else + lines.add(trimStringR(uploadFiles.get(i).getName())); + } + } + + private void selectFile(int i) { + int pos = 3 + i - uploadOffset; + if (pos >= 3 && pos < MAX_LINES) { + selectedLine = pos; + return; + } + + uploadOffset = i; + if (uploadOffset + MAX_LINES - 3 > uploadFiles.size()) + uploadOffset = uploadFiles.size() - MAX_LINES + 3; + + updateUploadScreen(); + selectedLine = 3 + i - uploadOffset; + } + + @CommandHandler("upload") + public void commandUpload(String[] args) { + if (!minecraft.player.getGameProfile().getId().equals(owner.uuid)) { + writeLine(tr("errowner")); + return; + } + + if (args.length > 0) { + File fle = new File(Util.join(args, " ")); + if (!fle.exists()) { + writeLine(tr("notfound")); + return; + } + + if (fle.isDirectory()) + uploadCD(fle); + else if (fle.isFile()) { + startFileUpload(fle, false); + return; + } else { + writeLine(tr("notfound")); + return; + } + } + + uploadWizard = true; + promptLocked = true; + uploadOffset = 0; + selectedLine = 3; + updateUploadScreen(); + } + + @CommandHandler("rm") + public void commandDelete(String[] args) { + if (!minecraft.player.getGameProfile().getId().equals(owner.uuid)) { + writeLine(tr("errowner")); + return; + } + + if (args.length < 1) { + writeLine(tr("fnamearg")); + return; + } + + String fname = Util.join(args, " "); + if (Util.isFileNameInvalid(fname)) { + writeLine(tr("nameerr")); + return; + } + + ClientTaskDeleteFile task = new ClientTaskDeleteFile(fname); + task.setFinishCallback((t) -> { + int status = t.getStatus(); + if (status == 1) + writeLine(tr("notfound")); + else if (status != 0) + writeLine(tr("error")); + + clearTask(); + }); + + queueTask(task); + } + + @CommandHandler("reconnect") + public void commandReconnect() { + Client.getInstance().stop(); + WDNetworkRegistry.INSTANCE.sendToServer(Client.getInstance().beginConnection()); + } + + private void startFileUpload(File f, boolean quit) { + if (quit) + quitUploadWizard(); + + if (Util.isFileNameInvalid(f.getName()) || f.getName().length() >= MAX_LINE_LEN - 3) { + writeLine(tr("nameerr")); + return; + } + + ClientTaskUploadFile task; + try { + task = new ClientTaskUploadFile(f); + } catch (IOException ex) { + writeLine(tr("error")); + ex.printStackTrace(); + return; + } + + task.setProgressCallback((cur, total) -> { + synchronized (GuiServer.this) { + queryTime = System.currentTimeMillis(); + } + }); + + task.setFinishCallback(t -> { + int status = t.getUploadStatus(); + if (status == 0) + writeLine(tr("upload.done")); + else if (status == Constants.FUPA_STATUS_FILE_EXISTS) + writeLine(tr("upload.exists")); + else if (status == Constants.FUPA_STATUS_EXCEEDS_QUOTA) + writeLine(tr("upload.quota")); + else + writeLine(tr("error2", status)); + + clearTask(); + }); + + if (queueTask(task)) + writeLine(tr("upload.uploading")); + } + + @Override + public boolean isForBlock(BlockPos bp, BlockSide side) { + return serverPos.equalsBlockPos(bp); + } + + @Nullable + @Override + public String getWikiPageName() { + return "Server"; + } + + int queueRead = 0; + ArrayList queue = new ArrayList<>(); + + private void queueLine(String line) { + final int maxl = uploadWizard ? MAX_LINES : (MAX_LINES - 1); //Cuz prompt is hidden + if (lines.size() < maxl) + writeLine(line); + else if (queueRead > 1) { + writeLine(line); + queueRead -= 1; + } else queue.add(line); + } +} diff --git a/src/main/java/net/montoyo/wd/client/gui/GuiSetURL2.java b/src/main/java/net/montoyo/wd/client/gui/GuiSetURL2.java new file mode 100644 index 0000000..a128fcd --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/GuiSetURL2.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2019 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.neoforged.api.distmarker.Dist; +import net.montoyo.wd.WebDisplays; +import net.montoyo.wd.client.ClientProxy; +import net.montoyo.wd.client.gui.controls.Button; +import net.montoyo.wd.client.gui.controls.TextField; +import net.montoyo.wd.client.gui.loading.FillControl; +import net.montoyo.wd.entity.ScreenBlockEntity; +import net.montoyo.wd.item.ItemMinePad2; +import net.montoyo.wd.net.WDNetworkRegistry; +import net.montoyo.wd.net.server_bound.C2SMessageMinepadUrl; +import net.montoyo.wd.net.server_bound.C2SMessageScreenCtrl; +import net.montoyo.wd.utilities.data.BlockSide; +import net.montoyo.wd.utilities.serialization.Util; +import net.montoyo.wd.utilities.math.Vector3i; +import net.montoyo.wd.data.WDDataComponents; + +import java.io.IOException; +import java.util.Map; +import java.util.UUID; + +public class GuiSetURL2 extends WDScreen { + + //Screen data + private ScreenBlockEntity tileEntity; + private BlockSide screenSide; + private Vector3i remoteLocation; + + //Pad data + private ItemStack stack; + private final boolean isPad; + + //Common + private final String screenURL; + + @FillControl + private TextField tfURL; + + @FillControl + private Button btnShutDown; + + @FillControl + private Button btnCancel; + + @FillControl + private Button btnOk; + + public GuiSetURL2(ScreenBlockEntity tes, BlockSide side, String url, Vector3i rl) { + super(Component.nullToEmpty(null)); + tileEntity = tes; + screenSide = side; + remoteLocation = rl; + isPad = false; + screenURL = url; + } + + public GuiSetURL2(ItemStack is, String url) { + super(Component.nullToEmpty(null)); + isPad = true; + stack = is; + screenURL = url; + } + + @Override + public void init() { + super.init(); + loadFrom(ResourceLocation.fromNamespaceAndPath("webdisplays", "gui/seturl.json")); + // Guard against null URL to avoid UI NPEs + tfURL.setText(screenURL == null ? "" : screenURL); + } + + @Override + protected void addLoadCustomVariables(Map vars) { + vars.put("isPad", isPad ? 1.0 : 0.0); + } + + protected UUID getUUID() { + if (stack == null || !(stack.getItem() instanceof ItemMinePad2)) + throw new RuntimeException("Get UUID is being called for a non-minepad UI"); + if (!stack.has(WDDataComponents.PAD_ID.get())) { + UUID newUUID = UUID.randomUUID(); + stack.set(WDDataComponents.PAD_ID.get(), newUUID); + } + + return stack.get(WDDataComponents.PAD_ID.get()); + } + + @GuiSubscribe + public void onButtonClicked(Button.ClickEvent ev) { + if (ev.getSource() == btnCancel) + minecraft.setScreen(null); + else if (ev.getSource() == btnOk) + validate(tfURL.getText()); + else if (ev.getSource() == btnShutDown) { + if (isPad) { + WDNetworkRegistry.INSTANCE.sendToServer(new C2SMessageMinepadUrl( + getUUID(), + "" + )); + stack.remove(WDDataComponents.PAD_ID.get()); + } + + minecraft.setScreen(null); + } + } + + @GuiSubscribe + public void onEnterPressed(TextField.EnterPressedEvent ev) { + validate(ev.getText()); + } + + private void validate(String url) { + if (!url.isEmpty()) { + + try { + ScreenBlockEntity.url(url); + } catch (IOException e) { + throw new RuntimeException(e); + } + + url = Util.addProtocol(url); +// url = ((ClientProxy) WebDisplays.PROXY).getMCEF().punycode(url); + + if (isPad) { + UUID uuid = getUUID(); + WDNetworkRegistry.INSTANCE.sendToServer(new C2SMessageMinepadUrl(uuid, url)); + stack.set(WDDataComponents.PAD_URL.get(), url); + + ClientProxy.PadData pd = ((ClientProxy) WebDisplays.PROXY).getPadByID(uuid); + + if (pd != null && pd.view != null) { + pd.view.loadURL(WebDisplays.applyBlacklist(url)); + } + } else + WDNetworkRegistry.INSTANCE.sendToServer(C2SMessageScreenCtrl.setURL(tileEntity, screenSide, url, remoteLocation)); + } + + minecraft.setScreen(null); + } + + @Override + public boolean isForBlock(BlockPos bp, BlockSide side) { + return (remoteLocation != null && remoteLocation.equalsBlockPos(bp)) || (bp.equals(tileEntity.getBlockPos()) && side == screenSide); + } + +} diff --git a/src/main/java/net/montoyo/wd/client/gui/GuiSubscribe.java b/src/main/java/net/montoyo/wd/client/gui/GuiSubscribe.java new file mode 100644 index 0000000..5814b77 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/GuiSubscribe.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface GuiSubscribe { +} diff --git a/src/main/java/net/montoyo/wd/client/gui/RenderRecipe.java b/src/main/java/net/montoyo/wd/client/gui/RenderRecipe.java new file mode 100644 index 0000000..1c7f0f7 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/RenderRecipe.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.entity.ItemRenderer; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.ShapedRecipe; +import net.neoforged.api.distmarker.OnlyIn; +import net.montoyo.wd.utilities.Log; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.stream.IntStream; + +import static net.neoforged.api.distmarker.Dist.CLIENT; + +@OnlyIn(CLIENT) +public class RenderRecipe extends Screen { + public RenderRecipe() { + super(Component.nullToEmpty(null)); + } + + private static class NameRecipePair { + + private final String name; + private final ShapedRecipe recipe; + + private NameRecipePair(String n, ShapedRecipe r) { + this.name = n; + this.recipe = r; + } + + } + + private static final ResourceLocation CRAFTING_TABLE_GUI_TEXTURES = ResourceLocation.fromNamespaceAndPath("minecraft", "textures/gui/container/crafting_table.png"); + private static final int SIZE_X = 176; + private static final int SIZE_Y = 166; + private int x; + private int y; + private ItemRenderer renderItem; + private final ItemStack[] recipe = new ItemStack[3 * 3]; + private ItemStack recipeResult; + private String recipeName; + private final ArrayList recipes = new ArrayList<>(); + private ByteBuffer buffer; + private int[] array; + + @Override + public void init() { + x = (width - SIZE_X) / 2; + y = (height - SIZE_Y) / 2; + renderItem = minecraft.getItemRenderer(); + + for (RecipeHolder holder : minecraft.level.getRecipeManager().getRecipes()) { + ResourceLocation regName = holder.id(); + Recipe rec = holder.value(); + + if (regName != null && regName.getNamespace().equals("webdisplays")) { + if (rec instanceof ShapedRecipe shaped) + recipes.add(new NameRecipePair(regName.getPath(), shaped)); + else + Log.warning("Found non-shaped recipe %s", regName.toString()); + } + } + + Log.info("Loaded %d recipes", recipes.size()); + nextRecipe(); + } + + @Override + public void render(GuiGraphics context, int mouseX, int mouseY, float partialTick) { + renderBackground(context, mouseX, mouseY, partialTick); + + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + RenderSystem.setShaderTexture(0, CRAFTING_TABLE_GUI_TEXTURES); +// context.blit(x, y, 0, 0, SIZE_X, SIZE_Y); +// font.draw(poseStack, I18n.get("container.crafting"), x + 28, y + 6, 0x404040); + + Lighting.setupForFlatItems(); +// RenderSystem.disableLighting(); //TODO: Need this? + + for(int sy = 0; sy < 3; sy++) { + for(int sx = 0; sx < 3; sx++) { + ItemStack is = recipe[sy * 3 + sx]; + + if(is != null) { + int x = this.x + 30 + sx * 18; + int y = this.y + 17 + sy * 18; + + context.renderItem(is, x, y); + context.renderItemDecorations(font, is, x, y); + } + } + } + + if(recipeResult != null) { + context.renderItem(recipeResult, x, y); + context.renderItemDecorations(font, recipeResult, x, y); + } + +// GlStateManager.enableLighting(); + Lighting.setupFor3DItems(); + } + + private void setRecipe(ShapedRecipe recipe) { + IntStream.range(0, this.recipe.length).forEach(i -> this.recipe[i] = null); + NonNullList ingredients = recipe.getIngredients(); + int pos = 0; + + for(int y = 0; y < recipe.getHeight(); y++) { + for(int x = 0; x < recipe.getWidth(); x++) { + ItemStack[] stacks = ingredients.get(pos++).getItems(); + + if(stacks.length > 0) + this.recipe[y * 3 + x] = stacks[0]; + } + } + +// recipeResult = recipe.getResultItem(); + } + + private void nextRecipe() { + if(recipes.isEmpty()) + minecraft.setScreen(null); + else { + NameRecipePair pair = recipes.remove(0); + setRecipe(pair.recipe); + recipeName = pair.name; + } + } + + private int screen2DisplayX(int x) { + double ret = ((double) x) / ((double) width) * ((double) minecraft.getWindow().getWidth()); + return (int) ret; + } + + private int screen2DisplayY(int y) { + double ret = ((double) y) / ((double) height) * ((double) minecraft.getWindow().getHeight()); + return (int) ret; + } + + private void takeScreenshot() throws Throwable { //TODO: Figure out how to do this. + /* + int x = screen2DisplayX(this.x + 27); + int y = minecraft.getWindow().getHeight() - screen2DisplayY(this.y + 4); + int w = screen2DisplayX(120); + int h = screen2DisplayY(68); + y -= h; + + if(buffer == null) + buffer = BufferUtils.createByteBuffer(w * h); + + int oldPack = glGetInteger(GL_PACK_ALIGNMENT); + RenderSystem.pixelStore(GL_PACK_ALIGNMENT, 1); + buffer.clear(); + RenderSystem.readPixels(x, y, w, h, EXTBGRA.GL_BGRA_EXT, GL_UNSIGNED_BYTE, buffer); + RenderSystem.pixelStore(GL_PACK_ALIGNMENT, oldPack); + + if(array == null) + array = new int[w * h]; + + buffer.clear(); + buffer.asIntBuffer().get(array); + TextureUtil.processPixelValues(array, w, h); + + File f = new File(minecraft.gameDirectory, "wd_recipes"); + if(!f.exists()) + f.mkdir(); + + f = new File(f, recipeName + ".png"); + + BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + bi.setRGB(0, 0, w, h, array, 0, w); + ImageIO.write(bi, "PNG", f); + */ + } + + @Override + public void tick() { + if(recipeName != null) { + try { + takeScreenshot(); + nextRecipe(); + } catch(Throwable t) { + t.printStackTrace(); + minecraft.setScreen(null); + } + } + } + +} diff --git a/src/main/java/net/montoyo/wd/client/gui/WDScreen.java b/src/main/java/net/montoyo/wd/client/gui/WDScreen.java new file mode 100644 index 0000000..a33b866 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/WDScreen.java @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.world.item.ItemStack; +import net.montoyo.wd.client.gui.controls.Container; +import net.montoyo.wd.client.gui.controls.Control; +import net.montoyo.wd.client.gui.controls.Event; +import net.montoyo.wd.client.gui.loading.FillControl; +import net.montoyo.wd.client.gui.loading.GuiLoader; +import net.montoyo.wd.client.gui.loading.JsonOWrapper; +import net.montoyo.wd.net.WDNetworkRegistry; +import net.montoyo.wd.net.server_bound.C2SMessageACQuery; +import net.montoyo.wd.utilities.*; +import net.montoyo.wd.utilities.data.Bounds; +import net.montoyo.wd.utilities.math.Vector3i; +import net.montoyo.wd.utilities.data.BlockSide; +import net.montoyo.wd.utilities.serialization.NameUUIDPair; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public abstract class WDScreen extends Screen { + + public static WDScreen CURRENT_SCREEN = null; + + protected final ArrayList controls = new ArrayList<>(); + protected final ArrayList postDrawList = new ArrayList<>(); + private final HashMap, Method> eventMap = new HashMap<>(); + protected boolean quitOnEscape = true; + protected boolean defaultBackground = true; + protected int syncTicks = 40; + private int syncTicksLeft = -1; + + public WDScreen(Component component) { + super(component); + Method[] methods = getClass().getMethods(); + + for(Method m : methods) { + if(m.getAnnotation(GuiSubscribe.class) != null) { + if(!Modifier.isPublic(m.getModifiers())) + throw new RuntimeException("Found non public @GuiSubscribe"); + + Class params[] = m.getParameterTypes(); + if(params.length != 1 || !Event.class.isAssignableFrom(params[0])) + throw new RuntimeException("Invalid parameters for @GuiSubscribe"); + + eventMap.put((Class) params[0], m); + } + } + } + + protected T addControl(T ctrl) { + controls.add(ctrl); + return ctrl; + } + + public int screen2DisplayX(int x) { + double ret = ((double) x) / ((double) width) * ((double) minecraft.getWindow().getWidth()); + return (int) ret; + } + + public int screen2DisplayY(int y) { + double ret = ((double) y) / ((double) height) * ((double) minecraft.getWindow().getHeight()); + return (int) ret; + } + + public int display2ScreenX(int x) { + double ret = ((double) x) / ((double) minecraft.getWindow().getWidth()) * ((double) width); + return (int) ret; + } + + public int display2ScreenY(int y) { + double ret = ((double) y) / ((double) minecraft.getWindow().getHeight()) * ((double) height); + return (int) ret; + } + + protected void centerControls() { + //Determine bounding box + Bounds bounds = Control.findBounds(controls); + + //Translation vector + int diffX = (width - bounds.maxX - bounds.minX) / 2; + int diffY = (height - bounds.maxY - bounds.minY) / 2; + + //Translate controls + for(Control ctrl : controls) { + int x = ctrl.getX(); + int y = ctrl.getY(); + + ctrl.setPos(x + diffX, y + diffY); + } + } + + @Override + public void render(GuiGraphics poseStack, int mouseX, int mouseY, float ptt) { + if(defaultBackground) + renderBackground(poseStack, mouseX, mouseY, ptt); + + RenderSystem.setShaderColor(1.f, 1.f, 1.f, 1.f); + + for(Control ctrl: controls) + ctrl.draw(poseStack, mouseX, mouseY, ptt); + + for(Control ctrl: postDrawList) + ctrl.postDraw(poseStack, mouseX, mouseY, ptt); + } + + @Override + public boolean charTyped(char codePoint, int modifiers) { + boolean typed = false; + + for(Control ctrl: controls) + typed = typed || ctrl.keyTyped(codePoint, modifiers); + + return typed; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + boolean clicked = false; + + Control clickedEl = null; + for(Control ctrl: controls) { + clicked = ctrl.mouseClicked(mouseX, mouseY, button); + if (clicked) { + clickedEl = ctrl; + break; // don't assume the compiler will optimize stuff + } + } + + if (clicked) { + for (Control control : controls) { + if (control != clickedEl) + control.unfocus(); + } + } + + return clicked; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + boolean mouseReleased = false; + + for(Control ctrl: controls) + mouseReleased = mouseReleased || ctrl.mouseReleased(mouseX, mouseY, button); + + return mouseReleased; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + boolean dragged = false; + + for(Control ctrl: controls) + dragged = dragged || ctrl.mouseClickMove(mouseX, mouseY, button, dragX, dragY); + + return dragged; + } + + @Override + protected void init() { + CURRENT_SCREEN = this; +// minecraft.keyboardHandler.setSendRepeatsToGui(true); + } + + @Override + public void onClose() { + if(syncTicksLeft >= 0) { + sync(); + syncTicksLeft = -1; + } + + for(Control ctrl : controls) + ctrl.destroy(); + +// Minecraft.getInstance().keyboardHandler.setSendRepeatsToGui(false); + CURRENT_SCREEN = null; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + boolean scrolled = false; + + for(Control ctrl : controls) + scrolled = scrolled || ctrl.mouseScroll(mouseX, mouseY, scrollY); + + return scrolled; + } + + @Override + public void mouseMoved(double mouseX, double mouseY) { + boolean moved = false; + + for(Control ctrl : controls) + moved = moved || ctrl.mouseMove(mouseX, mouseY); + + super.mouseMoved(mouseX, mouseY); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + boolean down = false; + + for (Control ctrl : controls) + down = down || ctrl.keyDown(keyCode, scanCode, modifiers); + + if (this instanceof GuiKeyboard) { + return down; + } else { + return new GuiServer(new Vector3i(), new NameUUIDPair()).keyPressed(keyCode, scanCode, modifiers); + } + } + + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + boolean up = false; + + for(Control ctrl : controls) + up = up || ctrl.keyUp(keyCode, scanCode, modifiers); + + return up || super.keyReleased(keyCode, scanCode, modifiers); + } + + public Object actionPerformed(Event ev) { + Method m = eventMap.get(ev.getClass()); + + if(m != null) { + try { + return m.invoke(this, ev); + } catch(IllegalAccessException e) { + Log.errorEx("Access to event %s of screen %s is denied", e, ev.getClass().getSimpleName(), getClass().getSimpleName()); + } catch(InvocationTargetException e) { + Log.errorEx("Event %s of screen %s failed", e, ev.getClass().getSimpleName(), getClass().getSimpleName()); + } + } + + return null; + } + + public T getControlByName(String name) { + for(Control ctrl : controls) { + if(name.equals(ctrl.getName())) + return (T) ctrl; + + if(ctrl instanceof Container) { + Control ret = ((Container) ctrl).getByName(name); + + if(ret != null) + return (T) ret; + } + } + + return null; + } + + protected void addLoadCustomVariables(Map vars) { + } + + public void loadFrom(ResourceLocation resLoc) { + try { + JsonObject root = GuiLoader.getJson(resLoc); + + if(root == null) + throw new RuntimeException("Could not load GUI file " + resLoc.toString()); + + if(!root.has("controls") || !root.get("controls").isJsonArray()) + throw new RuntimeException("In GUI file " + resLoc.toString() + ": missing root 'controls' object."); + + HashMap vars = new HashMap<>(); + vars.put("width", (double) width); + vars.put("height", (double) height); + vars.put("displayWidth", (double) minecraft.getWindow().getWidth()); + vars.put("displayHeight", (double) minecraft.getWindow().getHeight()); + addLoadCustomVariables(vars); + + JsonArray content = root.get("controls").getAsJsonArray(); + for(JsonElement elem: content) + controls.add(GuiLoader.create(new JsonOWrapper(elem.getAsJsonObject(), vars))); + + Field[] fields = getClass().getDeclaredFields(); + for(Field f: fields) { + f.setAccessible(true); + FillControl fc = f.getAnnotation(FillControl.class); + + if(fc != null) { + String name = fc.name().isEmpty() ? f.getName() : fc.name(); + Control ctrl = getControlByName(name); + + if(ctrl == null) { + if(fc.required()) + throw new RuntimeException("In GUI file " + resLoc.toString() + ": missing required control " + name); + + continue; + } + + if(!f.getType().isAssignableFrom(ctrl.getClass())) + throw new RuntimeException("In GUI file " + resLoc.toString() + ": invalid type for control " + name); + + try { + f.set(this, ctrl); + } catch(IllegalAccessException e) { + if(fc.required()) + throw new RuntimeException(e); + } + } + } + + if(root.has("center") && root.get("center").getAsBoolean()) + centerControls(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void resize(Minecraft minecraft, int width, int height) { + for(Control ctrl : controls) + ctrl.destroy(); + + controls.clear(); + super.resize(minecraft, width, height); + } + + protected void requestAutocomplete(String beginning, boolean matchExact) { + WDNetworkRegistry.INSTANCE.sendToServer(new C2SMessageACQuery(beginning, matchExact)); + } + + public void onAutocompleteResult(NameUUIDPair pairs[]) { + } + + public void onAutocompleteFailure() { + } + + protected void requestSync() { + syncTicksLeft = syncTicks - 1; + } + + protected boolean syncRequested() { + return syncTicksLeft >= 0; + } + + protected void abortSync() { + syncTicksLeft = -1; + } + + protected void sync() { + } + + @Override + public void tick() { + if(syncTicksLeft >= 0) { + if(--syncTicksLeft < 0) + sync(); + } + } + + public void drawItemStackTooltip(GuiGraphics poseStack, ItemStack is, int x, int y) { + poseStack.renderTooltip(Minecraft.getInstance().font, is, x, y); //Since it's protected... + } + + public void drawTooltip(GuiGraphics poseStack, List lines, int x, int y) { + poseStack.renderTooltip(Minecraft.getInstance().font, lines.stream().map(a -> FormattedCharSequence.forward(a, Style.EMPTY)).collect(Collectors.toList()), x, y); //This is also protected... + } + + public void requirePostDraw(Control ctrl) { + if(!postDrawList.contains(ctrl)) + postDrawList.add(ctrl); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + public abstract boolean isForBlock(BlockPos bp, BlockSide side); + + @Nullable + public String getWikiPageName() { + return null; + } + + //Bypass for needing to use Components + +} diff --git a/src/main/java/net/montoyo/wd/client/gui/camera/KeyboardCamera.java b/src/main/java/net/montoyo/wd/client/gui/camera/KeyboardCamera.java new file mode 100644 index 0000000..ef0a651 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/camera/KeyboardCamera.java @@ -0,0 +1,271 @@ +package net.montoyo.wd.client.gui.camera; + +import net.minecraft.client.Minecraft; +import net.minecraft.commands.arguments.EntityAnchorArgument; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.Vec2; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.client.event.ViewportEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.montoyo.wd.client.gui.GuiKeyboard; +import net.montoyo.wd.config.ClientConfig; +import net.montoyo.wd.entity.ScreenBlockEntity; +import net.montoyo.wd.entity.ScreenData; +import net.montoyo.wd.utilities.browser.WDBrowser; +import net.montoyo.wd.utilities.browser.handlers.js.queries.ElementCenterQuery; +import net.montoyo.wd.utilities.data.BlockSide; + +public class KeyboardCamera { + private static ScreenBlockEntity tes; + private static BlockSide side; + + private static double oxCrd = -1; + private static double xCrd = -1; + private static double nxCrd = -1; + private static double oyCrd = -1; + private static double yCrd = -1; + private static double nyCrd = -1; + + private static double nextX = -1; + private static double nextY = -1; + private static double focalX = -1; + private static double focalY = -1; + + private static final boolean[] mouseStatus = new boolean[2]; + + protected static Vec2 pxToHit(ScreenData scr, Vec2 dst) { + float cx, cy; + if (scr.rotation.isVertical) { + cy = dst.x; + cx = dst.y; + } else { + cx = dst.x; + cy = dst.y; + } + + cx /= (float) scr.resolution.x; + cy /= (float) scr.resolution.y; + + switch (scr.rotation) { + case ROT_270: + cx = 1.0f - cx; + break; + + case ROT_180: + cx = 1.0f - cx; + cy = 1.0f - cy; + break; + + case ROT_90: + cy = 1.0f - cy; + break; + } + + if (side != BlockSide.BOTTOM) + cy = 1.0f - cy; + + float swInverse = (((float) scr.size.x) - 4.f / 16.f); + float shInverse = (((float) scr.size.y) - 4.f / 16.f); + + cx *= swInverse; + cy *= shInverse; + + if (side.right.x > 0 || side.right.z > 0) + cx += 1.f; + + if (side == BlockSide.TOP || side == BlockSide.BOTTOM) + cy -= 1.f; + + return new Vec2(cx + (2 / 16f), cy + (2 / 16f)); + } + + protected static void updateCrd(ElementCenterQuery lock) { + ScreenData scr = tes.getScreen(side); + if (scr != null) { + Vec2 c; + + if (!mouseStatus[0] && !mouseStatus[1]) { + if (lock.hasFocused()) { + if (ClientConfig.Input.keyboardCamera) { + nextX = lock.getX(); + nextY = lock.getY(); + + c = pxToHit(scr, new Vec2((float) nextX, (float) nextY)); + } else c = new Vec2(scr.size.x / 2f, scr.size.y / 2f); + } else c = new Vec2(scr.size.x / 2f, scr.size.y / 2f); +// } else c = new Vec2((float) focalX, (float) focalY); + } else return; + + focalX = c.x; + focalY = c.y; + + nextX = c.x; + nextY = c.y; + + if (nextX < 0) nextX = 0; + else if (nextX > scr.size.x) nextX = scr.size.x; + if (nextY < 0) nextY = 0; + else if (nextY > scr.size.y) nextY = scr.size.y; + + float scl = Math.max(scr.size.x, scr.size.y); + + double mx = Minecraft.getInstance().mouseHandler.xpos(); + mx /= Minecraft.getInstance().getWindow().getWidth(); + + double my = Minecraft.getInstance().mouseHandler.ypos(); + my /= Minecraft.getInstance().getWindow().getHeight(); + + Vec2 v2 = new Vec2((float) mx, (float) my).add(-0.5f); + + nextX += v2.x * scl; + nextY -= v2.y * scl; + } + } + + protected static void pollElement() { + ScreenBlockEntity teTmp = tes; + BlockSide sdTmp = side; + + // async nonsense can occur here + if (teTmp == null || sdTmp == null) return; + + ScreenData scr = teTmp.getScreen(sdTmp); + if (scr != null) { + if (scr.browser instanceof WDBrowser wdBrowser) { + wdBrowser.focusedElement().dispatch(scr.browser); + updateCrd(((WDBrowser) scr.browser).focusedElement()); + } + } + } + + public static float[] getAngle(Entity e, double pct) { + BlockEntity tes = KeyboardCamera.tes; + BlockSide side = KeyboardCamera.side; + if (tes == null) return new float[]{Float.NaN, 0}; + if (side == null) return new float[]{Float.NaN, 0}; + + double coxCrd = Mth.lerp(0.5 * pct, oxCrd, xCrd); + double coyCrd = Mth.lerp(0.5 * pct, oyCrd, yCrd); + + double focalX = tes.getBlockPos().getX() + + side.right.x * (coxCrd - 1) + side.up.x * coyCrd + Math.abs(side.forward.x) * 0.5; + double focalY = tes.getBlockPos().getY() + + side.right.y * (coxCrd - 1) + side.up.y * coyCrd + Math.abs(side.forward.y) * 0.5; + double focalZ = tes.getBlockPos().getZ() + + side.right.z * (coxCrd - 1) + side.up.z * coyCrd + Math.abs(side.forward.z) * 0.5; + + focalX += side.forward.x * 0.5f; + focalY += side.forward.y * 0.5f; + focalZ += side.forward.z * 0.5f; + + float[] angle = lookAt( + e, EntityAnchorArgument.Anchor.EYES, + new Vec3(focalX, focalY, focalZ) + ); + + return angle; + } + + public static void setMouse(int side, boolean pressed) { + mouseStatus[side] = pressed; + } + + public static void updateCamera(ViewportEvent.ComputeCameraAngles event) { + if (tes == null) { + xCrd = -1; + yCrd = -1; + return; // nothing to do + } + + if (xCrd == -1) return; + if (yCrd == -1) return; + + float[] angle = getAngle(event.getCamera().getEntity(), event.getPartialTick()); + + if (Float.isNaN(angle[0])) return; + +// float xRot = event.getYaw(); // left right +// float yRot = event.getPitch(); // up down + + // TODO: smooth in/out + event.setYaw(angle[1]); + event.setPitch(angle[0]); + } + + public static void focus(ScreenBlockEntity screen, BlockSide side) { + KeyboardCamera.tes = screen; + KeyboardCamera.side = side; + } + + public static float[] lookAt(Entity entity, EntityAnchorArgument.Anchor pAnchor, Vec3 pTarget) { + Vec3 vec3 = pAnchor.apply(entity); + double d0 = pTarget.x - vec3.x; + double d1 = pTarget.y - vec3.y; + double d2 = pTarget.z - vec3.z; + double d3 = Math.sqrt(d0 * d0 + d2 * d2); + float xr = (Mth.wrapDegrees((float) (-(Mth.atan2(d1, d3) * (double) (180F / (float) Math.PI))))); + float yr = (Mth.wrapDegrees((float) (Mth.atan2(d2, d0) * (double) (180F / (float) Math.PI)) - 90.0F)); + return new float[]{xr, yr}; + } + + protected static int delay = 8; + + public static void gameTick(ClientTickEvent.Post event) { + if (mouseStatus[0] || mouseStatus[1]) { + oxCrd = Mth.lerp(0.5, oxCrd, xCrd); + oyCrd = Mth.lerp(0.5, oyCrd, yCrd); + return; + } + if (side == null) { + delay = 1; + oxCrd = -1; + oyCrd = -1; + xCrd = -1; + yCrd = -1; + nxCrd = -1; + nyCrd = -1; + return; + } + + if (!(Minecraft.getInstance().screen instanceof GuiKeyboard)) { + tes = null; + side = null; + return; + } + + pollElement(); + + double anxx = nextX; + double anxy = nextY; + + if ( + anxx == -1 || anxy == -1 || + nxCrd == -1 || nyCrd == -1 || + oxCrd == -1 || oyCrd == -1 || + xCrd == -1 || yCrd == -1 + ) { + ScreenData data = tes.getScreen(side); + if (data == null) + return; + + anxx = data.size.x / 2.0; + anxy = data.size.y / 2.0; + + if (nxCrd == -1) { + oxCrd = xCrd = anxx; + oyCrd = yCrd = anxy; + } + } + + nxCrd = anxx; + nyCrd = anxy; + + oxCrd = Mth.lerp(0.5, oxCrd, xCrd); + xCrd = Mth.lerp(0.15, xCrd, nxCrd); + + oyCrd = Mth.lerp(0.5, oyCrd, yCrd); + yCrd = Mth.lerp(0.15, yCrd, nyCrd); + } +} diff --git a/src/main/java/net/montoyo/wd/client/gui/controls/BasicControl.java b/src/main/java/net/montoyo/wd/client/gui/controls/BasicControl.java new file mode 100644 index 0000000..7d886e9 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/controls/BasicControl.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui.controls; + +import net.montoyo.wd.client.gui.loading.JsonOWrapper; + +public abstract class BasicControl extends Control { + + protected int x; + protected int y; + protected boolean visible = true; + protected boolean disabled = false; + + @Override + public int getX() { + return x; + } + + @Override + public int getY() { + return y; + } + + @Override + public void setPos(int x, int y) { + this.x = x; + this.y = y; + } + + public boolean isDisabled() { + return disabled; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public void enable() { + disabled = false; + } + + public void disable() { + disabled = true; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public void show() { + visible = true; + } + + public void hide() { + visible = false; + } + + @Override + public void load(JsonOWrapper json) { + super.load(json); + x = json.getInt("x", 0); + y = json.getInt("y", 0); + disabled = json.getBool("disabled", false); + visible = json.getBool("visible", true); + } + +} diff --git a/src/main/java/net/montoyo/wd/client/gui/controls/Button.java b/src/main/java/net/montoyo/wd/client/gui/controls/Button.java new file mode 100644 index 0000000..f0d1ae0 --- /dev/null +++ b/src/main/java/net/montoyo/wd/client/gui/controls/Button.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2018 BARBOTIN Nicolas + */ + +package net.montoyo.wd.client.gui.controls; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.montoyo.wd.client.gui.loading.JsonOWrapper; +import org.lwjgl.glfw.GLFW; + +import java.util.function.Supplier; + +public class Button extends Control { + + protected final net.minecraft.client.gui.components.Button btn; + protected boolean selected = false; + protected boolean shiftDown = false; + protected int originalColor = 0; + protected int shiftColor = 0; + + public static class ClickEvent extends Event