From 52ca9cb066e51b2007774092221462ebb051f82a Mon Sep 17 00:00:00 2001 From: 3944Realms Date: Wed, 10 Jun 2026 14:46:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8C=87=E4=BB=A4=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 +- gradle.properties | 4 +- .../1de3d2ee724999f84a11b20b51c37030049be277 | 4 +- .../2a65ee2815744be1ef1ffdae1c9a37f2a9cbe2ac | 4 +- .../82018c5420b46ddbb7071e62df09fdecd98133e6 | 4 +- .../c622617f6fabf890a00b9275cd5f643584a8a2c8 | 4 +- .../assets/dglabgame/lang/en_us.json | 36 ++ .../resources/assets/dglabgame/lang/lzh.json | 36 ++ .../assets/dglabgame/lang/zh_cn.json | 36 ++ .../assets/dglabgame/lang/zh_tw.json | 36 ++ .../leisuretimedock/dglabgame/DGLabGame.java | 6 + .../dglabgame/client/DLGKeyMapping.java | 8 + .../dglabgame/client/gui/ConfirmGui.java | 6 +- .../client/gui/DGLabManagementGui.java | 374 ++++++++++++++++ .../dglabgame/config/ClientConfig.java | 39 ++ .../dglabgame/config/CommonConfig.java | 91 ++++ .../core/capability/DGLabDataSyncManager.java | 8 +- .../core/capability/EntityDGLabData.java | 4 + .../dglabgame/core/command/DGLabCommands.java | 410 ++++++++++++++++++ .../core/event/ClientEventHandler.java | 11 + .../core/event/CommonEventHandler.java | 7 + .../dglabgame/core/manager/ClientManager.java | 67 +++ .../core/manager/DGLabManagerHolder.java | 44 ++ .../dglabgame/core/manager/IManager.java | 9 + .../dglabgame/core/manager/ServerManager.java | 75 ++++ .../core/network/DLGNetworkHandler.java | 12 + .../toServer/UpdateServerConfigPacket.java | 33 ++ .../toServer/UpdateStrengthConfigPacket.java | 37 ++ .../dglabgame/datagen/value/DLGLangKey.java | 186 ++++++++ .../dglabgame/ui/dg_lab_user_agreement.ui | Bin 10708 -> 11850 bytes 30 files changed, 1575 insertions(+), 24 deletions(-) create mode 100644 src/main/java/top/leisuretimedock/dglabgame/client/gui/DGLabManagementGui.java create mode 100644 src/main/java/top/leisuretimedock/dglabgame/config/ClientConfig.java create mode 100644 src/main/java/top/leisuretimedock/dglabgame/config/CommonConfig.java create mode 100644 src/main/java/top/leisuretimedock/dglabgame/core/command/DGLabCommands.java create mode 100644 src/main/java/top/leisuretimedock/dglabgame/core/manager/ClientManager.java create mode 100644 src/main/java/top/leisuretimedock/dglabgame/core/manager/DGLabManagerHolder.java create mode 100644 src/main/java/top/leisuretimedock/dglabgame/core/manager/IManager.java create mode 100644 src/main/java/top/leisuretimedock/dglabgame/core/manager/ServerManager.java create mode 100644 src/main/java/top/leisuretimedock/dglabgame/core/network/toServer/UpdateServerConfigPacket.java create mode 100644 src/main/java/top/leisuretimedock/dglabgame/core/network/toServer/UpdateStrengthConfigPacket.java diff --git a/build.gradle b/build.gradle index 4664f53..a498c97 100644 --- a/build.gradle +++ b/build.gradle @@ -134,8 +134,7 @@ dependencies { modImplementation("com.lowdragmc.ldlib:ldlib-forge-1.20.1:1.0.49") { transitive = false } - modImplementation("top.r3944realms.lib39:lib39:1.20.1-${lib39_version}") - implementation(jarJar("top.r3944realms.dg_lab.api:CommonApi:${dg_lab_version}")) + modImplementation("top.r3944realms.lib39:lib39-forge-1.20.1:${lib39_version}") implementation(jarJar("top.r3944realms.dg_lab:Common:${dg_lab_version}")) } mixin { @@ -255,11 +254,6 @@ publishing { } } -// ===================== 构建任务依赖 ===================== -tasks.named('build') { - dependsOn sourceJar, apiSourceJar, apiJar, apiJavadocJar -} - tasks.named('clean') { delete fileTree(dir: "${project.projectDir}/mcmodsrepo") } diff --git a/gradle.properties b/gradle.properties index 41b5091..86ca6a7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -64,7 +64,7 @@ mod_authors= R3944Realms, LeisureTimeDock mod_description=Use DgLab PowerBox to improve Player's game sense. mod_credits= -lib39_version=0.4.1 +lib39_version=0.5.6 mutil_version=1.20.1-6.0.0 -dg_lab_version=4.4.14.18 +dg_lab_version=4.4.14.20 mutil_version_range=[6.0.0,] diff --git a/src/generated/resources/.cache/1de3d2ee724999f84a11b20b51c37030049be277 b/src/generated/resources/.cache/1de3d2ee724999f84a11b20b51c37030049be277 index 796cd97..61f4c15 100644 --- a/src/generated/resources/.cache/1de3d2ee724999f84a11b20b51c37030049be277 +++ b/src/generated/resources/.cache/1de3d2ee724999f84a11b20b51c37030049be277 @@ -1,2 +1,2 @@ -// 1.20.1 2026-03-08T19:34:45.1815278 Languages: zh_tw -4eb9507db96ec64d8176a664e22e36bf8763d09d assets/dglabgame/lang/zh_tw.json +// 1.20.1 2026-06-10T14:38:47.691308 Languages: zh_tw +de843a2665a924368d9ef5ba456d22eb7e24d89b assets/dglabgame/lang/zh_tw.json diff --git a/src/generated/resources/.cache/2a65ee2815744be1ef1ffdae1c9a37f2a9cbe2ac b/src/generated/resources/.cache/2a65ee2815744be1ef1ffdae1c9a37f2a9cbe2ac index fe5857f..80f4e82 100644 --- a/src/generated/resources/.cache/2a65ee2815744be1ef1ffdae1c9a37f2a9cbe2ac +++ b/src/generated/resources/.cache/2a65ee2815744be1ef1ffdae1c9a37f2a9cbe2ac @@ -1,2 +1,2 @@ -// 1.20.1 2026-03-08T19:34:45.1719971 Languages: zh_cn -9bb1075f2b018219335d8c3784d293cdd80464ac assets/dglabgame/lang/zh_cn.json +// 1.20.1 2026-06-10T14:38:47.6853295 Languages: zh_cn +1fff14dbc8d8f08ca771484db1a902dbefbb4709 assets/dglabgame/lang/zh_cn.json diff --git a/src/generated/resources/.cache/82018c5420b46ddbb7071e62df09fdecd98133e6 b/src/generated/resources/.cache/82018c5420b46ddbb7071e62df09fdecd98133e6 index 25442ce..e6e8ee1 100644 --- a/src/generated/resources/.cache/82018c5420b46ddbb7071e62df09fdecd98133e6 +++ b/src/generated/resources/.cache/82018c5420b46ddbb7071e62df09fdecd98133e6 @@ -1,2 +1,2 @@ -// 1.20.1 2026-03-08T19:34:45.1785204 Languages: lzh -41c40b3de74f63b246b0e67bbd74c5e220cd1e69 assets/dglabgame/lang/lzh.json +// 1.20.1 2026-06-10T14:38:47.690311 Languages: lzh +a51fc9b128855ce2e4ae7e81c861ec5a23681574 assets/dglabgame/lang/lzh.json diff --git a/src/generated/resources/.cache/c622617f6fabf890a00b9275cd5f643584a8a2c8 b/src/generated/resources/.cache/c622617f6fabf890a00b9275cd5f643584a8a2c8 index 2af0a8e..689502a 100644 --- a/src/generated/resources/.cache/c622617f6fabf890a00b9275cd5f643584a8a2c8 +++ b/src/generated/resources/.cache/c622617f6fabf890a00b9275cd5f643584a8a2c8 @@ -1,2 +1,2 @@ -// 1.20.1 2026-03-08T19:34:45.1765211 Languages: en_us -ca592519439edf564117cb3e2d7f98ae82defe15 assets/dglabgame/lang/en_us.json +// 1.20.1 2026-06-10T14:38:47.6883175 Languages: en_us +543ea588d9607a5f1d99e220157eb42903c4f4a2 assets/dglabgame/lang/en_us.json diff --git a/src/generated/resources/assets/dglabgame/lang/en_us.json b/src/generated/resources/assets/dglabgame/lang/en_us.json index 8385b8e..8bbe94f 100644 --- a/src/generated/resources/assets/dglabgame/lang/en_us.json +++ b/src/generated/resources/assets/dglabgame/lang/en_us.json @@ -1,4 +1,24 @@ { + "dglabgame.command.clear.sent": "Clear command sent for channel %s (%s target(s))", + "dglabgame.command.client.created": "DGLab client \"%s\" created (%s:%s)", + "dglabgame.command.client.list": "Registered clients: %s", + "dglabgame.command.client.none": "No DGLab clients registered", + "dglabgame.command.client.not_found": "Client \"%s\" not found", + "dglabgame.command.client.removed": "DGLab client \"%s\" removed", + "dglabgame.command.client.started": "DGLab client \"%s\" started", + "dglabgame.command.client.stopped": "DGLab client \"%s\" stopped", + "dglabgame.command.feedback.sent": "Feedback value %s sent to %s target(s)", + "dglabgame.command.invalid_channel": "Invalid channel. Use A or B.", + "dglabgame.command.invalid_policy": "Invalid policy. Use goto/set, increase/inc, or decrease/dec.", + "dglabgame.command.no_output": "No active DGLab output available. Start the server or a client first.", + "dglabgame.command.pulse.sent": "Pulse waveform sent to channel %s (%s target(s))", + "dglabgame.command.server.already_running": "DGLab server is already running", + "dglabgame.command.server.not_created": "DGLab server has not been created yet", + "dglabgame.command.server.not_running": "DGLab server is not running", + "dglabgame.command.server.started": "DGLab WebSocket server started on port %s", + "dglabgame.command.server.status": "Server status: %s, connected apps: %s", + "dglabgame.command.server.stopped": "DGLab server stopped", + "dglabgame.command.strength.sent": "Strength [%s %s %s] sent to %s target(s)", "dglabgame.name": "DG Lab Game", "dglabgame.user_agreement.accept": "§2§lAccept", "dglabgame.user_agreement.line1": "1. This mod is developed by §e§lR3944Realms §r, completely free and open source.", @@ -8,6 +28,22 @@ "dglabgame.user_agreement.line5": "5. §4Prohibited for any illegal or immoral activities.", "dglabgame.user_agreement.line6": "6. §4Developers are not responsible for any personal injury or property damage.", "dglabgame.user_agreement.refuse": "§4§lRefuse", + "dglabgame.user_agreement.reject_use": "Not agreed to user guidelines, usage rejected", + "gui.dglabgame.client.label": "Client:", + "gui.dglabgame.feedback.send": "Send Feedback:", + "gui.dglabgame.qrcode.copy": "Copy QR URL to clipboard", + "gui.dglabgame.server.max_conn": "Max:", + "gui.dglabgame.server.port": "Port:", + "gui.dglabgame.server.status": "Status: %s", + "gui.dglabgame.strength.channel_a": "Channel A", + "gui.dglabgame.strength.channel_b": "Channel B", + "gui.dglabgame.tab.feedback": "Feedback", + "gui.dglabgame.tab.qrcode": "QR Code", + "gui.dglabgame.tab.server": "Server", + "gui.dglabgame.tab.strength": "Strength", + "gui.dglabgame.tab.waveform": "Waveform", + "gui.dglabgame.waveform.channel": "Channel:", "key.categories.dglabgame": "DG Lab Game", + "key.dglabgame.management": "Open Management", "key.dglabgame.test": "Test" } \ No newline at end of file diff --git a/src/generated/resources/assets/dglabgame/lang/lzh.json b/src/generated/resources/assets/dglabgame/lang/lzh.json index 734ba9f..46eaad8 100644 --- a/src/generated/resources/assets/dglabgame/lang/lzh.json +++ b/src/generated/resources/assets/dglabgame/lang/lzh.json @@ -1,4 +1,24 @@ { + "dglabgame.command.clear.sent": "清除之令已送至通道 %s (%s 標的)", + "dglabgame.command.client.created": "DGLab 客戶 \"%s\" 已建 (%s:%s)", + "dglabgame.command.client.list": "已註客戶: %s", + "dglabgame.command.client.none": "無已註之 DGLab 客戶", + "dglabgame.command.client.not_found": "未見客戶 \"%s\"", + "dglabgame.command.client.removed": "DGLab 客戶 \"%s\" 已除", + "dglabgame.command.client.started": "DGLab 客戶 \"%s\" 已啟", + "dglabgame.command.client.stopped": "DGLab 客戶 \"%s\" 已停", + "dglabgame.command.feedback.sent": "回應值 %s 已送至 %s 標的", + "dglabgame.command.invalid_channel": "通道有誤,當用 A 或 B。", + "dglabgame.command.invalid_policy": "策略有誤,當用 goto/set、increase/inc 或 decrease/dec。", + "dglabgame.command.no_output": "無可用之 DGLab 輸出。請先啟伺服器或客戶。", + "dglabgame.command.pulse.sent": "脈衝波形已送至通道 %s (%s 標的)", + "dglabgame.command.server.already_running": "DGLab 伺服器已在運作中", + "dglabgame.command.server.not_created": "DGLab 伺服器尚未建", + "dglabgame.command.server.not_running": "DGLab 伺服器未啟", + "dglabgame.command.server.started": "DGLab WebSocket 伺服器已於埠 %s 啟", + "dglabgame.command.server.status": "伺服器態: %s, 已連應用: %s", + "dglabgame.command.server.stopped": "DGLab 伺服器已停", + "dglabgame.command.strength.sent": "強度 [%s %s %s] 已送至 %s 標的", "dglabgame.name": "郊狼戯", "dglabgame.user_agreement.accept": "§2§l認", "dglabgame.user_agreement.line1": "1. 本模組乃 §e§lR3944Realms §r 所製,全然免費而開源。", @@ -8,6 +28,22 @@ "dglabgame.user_agreement.line5": "5. §4禁作非法不德之事。", "dglabgame.user_agreement.line6": "6. §4凡人身之傷、財物之損,開發者概不負責。", "dglabgame.user_agreement.refuse": "§4§l否", + "dglabgame.user_agreement.reject_use": "未允用戶之約,拒用", + "gui.dglabgame.client.label": "客戶:", + "gui.dglabgame.feedback.send": "發回應:", + "gui.dglabgame.qrcode.copy": "複QR URL", + "gui.dglabgame.server.max_conn": "至多:", + "gui.dglabgame.server.port": "埠:", + "gui.dglabgame.server.status": "態: %s", + "gui.dglabgame.strength.channel_a": "通道甲", + "gui.dglabgame.strength.channel_b": "通道乙", + "gui.dglabgame.tab.feedback": "回應", + "gui.dglabgame.tab.qrcode": "二維碼", + "gui.dglabgame.tab.server": "伺服器", + "gui.dglabgame.tab.strength": "強度", + "gui.dglabgame.tab.waveform": "波形", + "gui.dglabgame.waveform.channel": "通道:", "key.categories.dglabgame": "郊狼戯", + "key.dglabgame.management": "啟管理", "key.dglabgame.test": "式" } \ No newline at end of file diff --git a/src/generated/resources/assets/dglabgame/lang/zh_cn.json b/src/generated/resources/assets/dglabgame/lang/zh_cn.json index 45ca884..c08ef12 100644 --- a/src/generated/resources/assets/dglabgame/lang/zh_cn.json +++ b/src/generated/resources/assets/dglabgame/lang/zh_cn.json @@ -1,4 +1,24 @@ { + "dglabgame.command.clear.sent": "清除指令已发送至通道 %s (%s 个目标)", + "dglabgame.command.client.created": "DGLab 客户端 \"%s\" 已创建 (%s:%s)", + "dglabgame.command.client.list": "已注册的客户端: %s", + "dglabgame.command.client.none": "没有已注册的 DGLab 客户端", + "dglabgame.command.client.not_found": "未找到客户端 \"%s\"", + "dglabgame.command.client.removed": "DGLab 客户端 \"%s\" 已移除", + "dglabgame.command.client.started": "DGLab 客户端 \"%s\" 已启动", + "dglabgame.command.client.stopped": "DGLab 客户端 \"%s\" 已关闭", + "dglabgame.command.feedback.sent": "反馈值 %s 已发送至 %s 个目标", + "dglabgame.command.invalid_channel": "无效的通道。请使用 A 或 B。", + "dglabgame.command.invalid_policy": "无效的策略。请使用 goto/set, increase/inc 或 decrease/dec。", + "dglabgame.command.no_output": "没有可用的 DGLab 输出。请先启动服务器或客户端。", + "dglabgame.command.pulse.sent": "脉冲波形已发送至通道 %s (%s 个目标)", + "dglabgame.command.server.already_running": "DGLab 服务器已在运行中", + "dglabgame.command.server.not_created": "DGLab 服务器尚未创建", + "dglabgame.command.server.not_running": "DGLab 服务器未在运行", + "dglabgame.command.server.started": "DGLab WebSocket 服务器已在端口 %s 启动", + "dglabgame.command.server.status": "服务器状态: %s, 已连接应用: %s", + "dglabgame.command.server.stopped": "DGLab 服务器已关闭", + "dglabgame.command.strength.sent": "强度 [%s %s %s] 已发送至 %s 个目标", "dglabgame.name": "郊狼游戏", "dglabgame.user_agreement.accept": "§2§l接受", "dglabgame.user_agreement.line1": "1. 本模组由 §e§lR3944Realms §r开发,完全免费开源。", @@ -8,6 +28,22 @@ "dglabgame.user_agreement.line5": "5. §4禁止用于任何违法或不道德的活动。", "dglabgame.user_agreement.line6": "6. §4开发者不对任何人身伤害或财产损失负责。", "dglabgame.user_agreement.refuse": "§4§l拒绝", + "dglabgame.user_agreement.reject_use": "未同意用户使用准则,拒绝使用", + "gui.dglabgame.client.label": "客户端:", + "gui.dglabgame.feedback.send": "发送反馈:", + "gui.dglabgame.qrcode.copy": "复制QR URL到剪贴板", + "gui.dglabgame.server.max_conn": "最大:", + "gui.dglabgame.server.port": "端口:", + "gui.dglabgame.server.status": "状态: %s", + "gui.dglabgame.strength.channel_a": "通道A", + "gui.dglabgame.strength.channel_b": "通道B", + "gui.dglabgame.tab.feedback": "反馈", + "gui.dglabgame.tab.qrcode": "二维码", + "gui.dglabgame.tab.server": "服务器", + "gui.dglabgame.tab.strength": "强度", + "gui.dglabgame.tab.waveform": "波形", + "gui.dglabgame.waveform.channel": "通道:", "key.categories.dglabgame": "郊狼游戏", + "key.dglabgame.management": "打开管理", "key.dglabgame.test": "测试" } \ No newline at end of file diff --git a/src/generated/resources/assets/dglabgame/lang/zh_tw.json b/src/generated/resources/assets/dglabgame/lang/zh_tw.json index c32843c..5202262 100644 --- a/src/generated/resources/assets/dglabgame/lang/zh_tw.json +++ b/src/generated/resources/assets/dglabgame/lang/zh_tw.json @@ -1,4 +1,24 @@ { + "dglabgame.command.clear.sent": "清除指令已傳送至通道 %s (%s 個目標)", + "dglabgame.command.client.created": "DGLab 客戶端 \"%s\" 已創建 (%s:%s)", + "dglabgame.command.client.list": "已註冊的客戶端: %s", + "dglabgame.command.client.none": "沒有已註冊的 DGLab 客戶端", + "dglabgame.command.client.not_found": "未找到客戶端 \"%s\"", + "dglabgame.command.client.removed": "DGLab 客戶端 \"%s\" 已移除", + "dglabgame.command.client.started": "DGLab 客戶端 \"%s\" 已啟動", + "dglabgame.command.client.stopped": "DGLab 客戶端 \"%s\" 已關閉", + "dglabgame.command.feedback.sent": "回饋值 %s 已傳送至 %s 個目標", + "dglabgame.command.invalid_channel": "無效的通道。請使用 A 或 B。", + "dglabgame.command.invalid_policy": "無效的策略。請使用 goto/set, increase/inc 或 decrease/dec。", + "dglabgame.command.no_output": "沒有可用的 DGLab 輸出。請先啟動伺服器或客戶端。", + "dglabgame.command.pulse.sent": "脈衝波形已傳送至通道 %s (%s 個目標)", + "dglabgame.command.server.already_running": "DGLab 伺服器已在執行中", + "dglabgame.command.server.not_created": "DGLab 伺服器尚未創建", + "dglabgame.command.server.not_running": "DGLab 伺服器未在執行", + "dglabgame.command.server.started": "DGLab WebSocket 伺服器已在連接埠 %s 啟動", + "dglabgame.command.server.status": "伺服器狀態: %s, 已連線應用: %s", + "dglabgame.command.server.stopped": "DGLab 伺服器已關閉", + "dglabgame.command.strength.sent": "強度 [%s %s %s] 已傳送至 %s 個目標", "dglabgame.name": "郊狼游戲", "dglabgame.user_agreement.accept": "§2§l接受", "dglabgame.user_agreement.line1": "1. 本模組由 §e§lR3944Realms §r 開發,完全免費開源。", @@ -8,6 +28,22 @@ "dglabgame.user_agreement.line5": "5. §4禁止用於任何違法或不道德之活動。", "dglabgame.user_agreement.line6": "6. §4開發者不對任何人身份害或財產損失負責。", "dglabgame.user_agreement.refuse": "§4§l拒絕", + "dglabgame.user_agreement.reject_use": "未同意使用者準則,拒絕使用", + "gui.dglabgame.client.label": "客戶端:", + "gui.dglabgame.feedback.send": "發送回饋:", + "gui.dglabgame.qrcode.copy": "複製QR URL到剪貼板", + "gui.dglabgame.server.max_conn": "最大:", + "gui.dglabgame.server.port": "連接埠:", + "gui.dglabgame.server.status": "狀態: %s", + "gui.dglabgame.strength.channel_a": "通道A", + "gui.dglabgame.strength.channel_b": "通道B", + "gui.dglabgame.tab.feedback": "回饋", + "gui.dglabgame.tab.qrcode": "QR碼", + "gui.dglabgame.tab.server": "伺服器", + "gui.dglabgame.tab.strength": "強度", + "gui.dglabgame.tab.waveform": "波形", + "gui.dglabgame.waveform.channel": "通道:", "key.categories.dglabgame": "郊狼游戲", + "key.dglabgame.management": "開啟管理", "key.dglabgame.test": "测试" } \ No newline at end of file diff --git a/src/main/java/top/leisuretimedock/dglabgame/DGLabGame.java b/src/main/java/top/leisuretimedock/dglabgame/DGLabGame.java index d86fa5d..21a265c 100644 --- a/src/main/java/top/leisuretimedock/dglabgame/DGLabGame.java +++ b/src/main/java/top/leisuretimedock/dglabgame/DGLabGame.java @@ -3,10 +3,14 @@ package top.leisuretimedock.dglabgame; import com.mojang.logging.LogUtils; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; +import top.leisuretimedock.dglabgame.config.ClientConfig; +import top.leisuretimedock.dglabgame.config.CommonConfig; import top.leisuretimedock.dglabgame.core.network.DLGNetworkHandler; @@ -19,6 +23,8 @@ public class DGLabGame { } public DGLabGame() { DLGNetworkHandler.register(); + ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, CommonConfig.SPEC); + ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, ClientConfig.SPEC); } @Contract("_ -> new") public static @NotNull ResourceLocation rl(String path) { diff --git a/src/main/java/top/leisuretimedock/dglabgame/client/DLGKeyMapping.java b/src/main/java/top/leisuretimedock/dglabgame/client/DLGKeyMapping.java index be6c7c8..03f8129 100644 --- a/src/main/java/top/leisuretimedock/dglabgame/client/DLGKeyMapping.java +++ b/src/main/java/top/leisuretimedock/dglabgame/client/DLGKeyMapping.java @@ -15,4 +15,12 @@ public class DLGKeyMapping { GLFW.GLFW_KEY_Z, "key.categories.dglabgame" ); + public static final KeyMapping KEY_MANAGEMENT = new KeyMapping( + "key.dglabgame.management", + KeyConflictContext.IN_GAME, + KeyModifier.NONE, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_M, + "key.categories.dglabgame" + ); } diff --git a/src/main/java/top/leisuretimedock/dglabgame/client/gui/ConfirmGui.java b/src/main/java/top/leisuretimedock/dglabgame/client/gui/ConfirmGui.java index f7a627c..10dbb5d 100644 --- a/src/main/java/top/leisuretimedock/dglabgame/client/gui/ConfirmGui.java +++ b/src/main/java/top/leisuretimedock/dglabgame/client/gui/ConfirmGui.java @@ -8,11 +8,11 @@ import com.lowdragmc.lowdraglib.gui.widget.Widget; import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; import net.minecraft.client.Minecraft; import org.jetbrains.annotations.Nullable; +import top.leisuretimedock.dglabgame.config.CommonConfig; import top.leisuretimedock.dglabgame.core.network.DLGNetworkHandler; import top.leisuretimedock.dglabgame.core.network.toServer.AcceptUserAgreementPacket; import top.leisuretimedock.dglabgame.util.UILoader; -import java.awt.*; import java.util.Optional; import java.util.Timer; import java.util.TimerTask; @@ -26,11 +26,11 @@ public class ConfirmGui implements ILdlibGui { widgetGroup.ifPresent(group -> { Widget yes = group.getFirstWidgetById("yes"); if (yes instanceof ButtonWidget yesButton) { - ImageWidget imageWidget = new ImageWidget(yesButton.getPositionX() + 3, yesButton.getPositionY() + 3, yesButton.getSizeWidth() - 6, yesButton.getSizeHeight() - 6, new ColorRectTexture(Color.DARK_GRAY)); + ImageWidget imageWidget = new ImageWidget(yesButton.getPositionX() + 3, yesButton.getPositionY() + 3, yesButton.getSizeWidth() - 6, yesButton.getSizeHeight() - 6, new ColorRectTexture(0xff373737)); group.addWidget(imageWidget); yesButton.setActive(false); Timer timer = new Timer(); - final int[] countdown = {5}; + final int[] countdown = {CommonConfig.COUNTDOWN_SECONDS.get()}; timer.scheduleAtFixedRate( new TimerTask() { @Override diff --git a/src/main/java/top/leisuretimedock/dglabgame/client/gui/DGLabManagementGui.java b/src/main/java/top/leisuretimedock/dglabgame/client/gui/DGLabManagementGui.java new file mode 100644 index 0000000..3eb3b34 --- /dev/null +++ b/src/main/java/top/leisuretimedock/dglabgame/client/gui/DGLabManagementGui.java @@ -0,0 +1,374 @@ +package top.leisuretimedock.dglabgame.client.gui; + +import com.lowdragmc.lowdraglib.gui.texture.ColorRectTexture; +import com.lowdragmc.lowdraglib.gui.texture.ResourceBorderTexture; +import com.lowdragmc.lowdraglib.gui.texture.ResourceTexture; +import com.lowdragmc.lowdraglib.gui.texture.TextTexture; +import com.lowdragmc.lowdraglib.gui.widget.*; +import com.r3944realms.dg_lab.api.manager.Status; +import com.r3944realms.dg_lab.api.message.IPowerBoxMsg; +import com.r3944realms.dg_lab.api.message.argType.Channel; +import com.r3944realms.dg_lab.api.message.argType.ChangePolicy; +import com.r3944realms.dg_lab.api.message.data.PulseWaveListGenerator; +import com.r3944realms.dg_lab.api.websocket.message.MessageDirection; +import com.r3944realms.dg_lab.api.websocket.message.PowerBoxMessage; +import com.r3944realms.dg_lab.manager.DGPBClientManager; +import com.r3944realms.dg_lab.websocket.sharedData.ClientPowerBoxSharedData; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import top.leisuretimedock.dglabgame.config.ClientConfig; +import top.leisuretimedock.dglabgame.config.CommonConfig; +import top.leisuretimedock.dglabgame.core.manager.DGLabManagerHolder; +import top.leisuretimedock.dglabgame.core.manager.ServerManager; +import top.leisuretimedock.dglabgame.core.network.DLGNetworkHandler; +import top.leisuretimedock.dglabgame.core.network.toServer.UpdateServerConfigPacket; +import top.leisuretimedock.dglabgame.core.network.toServer.UpdateStrengthConfigPacket; + +import java.awt.*; +import java.awt.datatransfer.StringSelection; +import java.util.Optional; + +@OnlyIn(Dist.CLIENT) +public class DGLabManagementGui implements ILdlibGui { + private static final int W = 380; + private static final int H = 260; + private static final String CLIENT_ID = "dglabgame-gui"; + private String currentPage = "qrcode"; + + @Override + public WidgetGroup createGui() { + WidgetGroup root = new WidgetGroup(0, 0, W, H); + root.setBackground(new ColorRectTexture(0xff1a1a1a)); + + // top nav bar + root.addWidget(navButton("qrcode", 5, 5, 55, 18, "gui.dglabgame.tab.qrcode")); + root.addWidget(navButton("strength", 65, 5, 55, 18, "gui.dglabgame.tab.strength")); + root.addWidget(navButton("waveform", 125, 5, 55, 18, "gui.dglabgame.tab.waveform")); + root.addWidget(navButton("feedback", 185, 5, 55, 18, "gui.dglabgame.tab.feedback")); + + boolean isAdmin = Minecraft.getInstance().player != null + && Minecraft.getInstance().player.hasPermissions(4); + if (isAdmin) { + root.addWidget(navButton("server", 245, 5, 55, 18, "gui.dglabgame.tab.server")); + } + + // content area + root.addWidget(buildContentArea()); + + // bottom bar: client selector + root.addWidget(buildBottomBar()); + + return root; + } + + private ButtonWidget navButton(String page, int x, int y, int w, int h, String langKey) { + ButtonWidget btn = new ButtonWidget(x, y, w, h, + new TextTexture(Component.translatable(langKey).getString(), 0xffffffff), + cd -> switchPage(page) + ); + btn.setBackground(new ColorRectTexture(page.equals(currentPage) ? 0xff4444aa : 0xff333333)); + return btn; + } + + private void switchPage(String page) { + currentPage = page; + display(); + } + + private WidgetGroup buildContentArea() { + WidgetGroup area = new WidgetGroup(5, 28, W - 10, H - 62); + area.setBackground(new ColorRectTexture(0xff222222)); + + switch (currentPage) { + case "qrcode": buildQrCodeTab(area); break; + case "strength": buildStrengthTab(area); break; + case "waveform": buildWaveformTab(area); break; + case "feedback": buildFeedbackTab(area); break; + case "server": buildServerTab(area); break; + } + return area; + } + + // ==================== QR Code Tab ==================== + + private void buildQrCodeTab(WidgetGroup group) { + int y = 10; + TextFieldWidget urlField = new TextFieldWidget(10, y, W - 40, 15, () -> "", s -> {}); + urlField.setCurrentString(""); + urlField.setBordered(true); + group.addWidget(urlField); + + group.addWidget(new ButtonWidget(W - 45, y, 22, 15, cd -> { + String url = getQrCodeUrl(); + Toolkit.getDefaultToolkit().getSystemClipboard() + .setContents(new StringSelection(url), null); + }).setBackground(new ColorRectTexture(0xff336633))); + + y += 22; + group.addWidget(new ImageWidget(10, y, W - 40, 14, + new TextTexture(Component.translatable("gui.dglabgame.qrcode.copy").getString(), 0xffaaaaaa))); + + y += 20; + var names = DGLabManagerHolder.getClientManager().getClientNames(); + for (String name : names) { + group.addWidget(new ImageWidget(10, y, W - 40, 10, + new TextTexture(name, 0xffcccccc))); + y += 14; + } + } + + private String getQrCodeUrl() { + var names = DGLabManagerHolder.getClientManager().getClientNames(); + for (String name : names) { + Optional client = DGLabManagerHolder.getClientManager().getClient(name); + if (client.isPresent()) { + ClientPowerBoxSharedData sd = client.get().getSharedData(); + if (sd != null && sd.rqCodeUrl != null && !sd.rqCodeUrl.isEmpty()) { + return sd.rqCodeUrl; + } + } + } + return ""; + } + + // ==================== Strength Tab ==================== + + private void buildStrengthTab(WidgetGroup group) { + int y = 8; + group.addWidget(new ImageWidget(10, y, 60, 10, + new TextTexture(Component.translatable("gui.dglabgame.strength.channel_a").getString(), 0xffff4444))); + + y += 14; + group.addWidget(strengthField("maxA", W / 2 - 55, y, CommonConfig.MAX_STRENGTH_A.get())); + group.addWidget(strengthField("minA", W / 2 + 15, y, CommonConfig.MIN_STRENGTH_A.get())); + + y += 20; + group.addWidget(new ImageWidget(10, y, 60, 10, + new TextTexture(Component.translatable("gui.dglabgame.strength.channel_b").getString(), 0xff4444ff))); + + y += 14; + group.addWidget(strengthField("maxB", W / 2 - 55, y, CommonConfig.MAX_STRENGTH_B.get())); + group.addWidget(strengthField("minB", W / 2 + 15, y, CommonConfig.MIN_STRENGTH_B.get())); + + y += 22; + group.addWidget(new ButtonWidget(W / 2 - 30, y, 60, 16, cd -> { + TextFieldWidget maxAF = (TextFieldWidget) group.getFirstWidgetById("maxA"); + TextFieldWidget minAF = (TextFieldWidget) group.getFirstWidgetById("minA"); + TextFieldWidget maxBF = (TextFieldWidget) group.getFirstWidgetById("maxB"); + TextFieldWidget minBF = (TextFieldWidget) group.getFirstWidgetById("minB"); + try { + int maxA = Integer.parseInt(maxAF.getCurrentString()); + int minA = Integer.parseInt(minAF.getCurrentString()); + int maxB = Integer.parseInt(maxBF.getCurrentString()); + int minB = Integer.parseInt(minBF.getCurrentString()); + DLGNetworkHandler.CHANNEL.sendToServer(new UpdateStrengthConfigPacket(maxA, minA, maxB, minB)); + } catch (NumberFormatException ignored) {} + }).setBackground(new ColorRectTexture(0xff336633))); + } + + private TextFieldWidget strengthField(String id, int x, int y, int value) { + TextFieldWidget field = new TextFieldWidget(x, y, 50, 14, () -> "", s -> {}); + field.setCurrentString(String.valueOf(value)); + field.setBordered(true); + field.setId(id); + return field; + } + + // ==================== Waveform Tab ==================== + + private void buildWaveformTab(WidgetGroup group) { + int y = 8; + String[] labels = {"f1", "s1", "f2", "s2", "f3", "s3", "f4", "s4"}; + int[] defaults = {80, 50, 80, 50, 80, 50, 80, 50}; + + group.addWidget(new ImageWidget(10, y, 80, 10, + new TextTexture(Component.translatable("gui.dglabgame.waveform.channel").getString(), 0xffcccccc))); + + group.addWidget(new ButtonWidget(W / 2 + 40, y - 2, 40, 14, cd -> {}) + .setBackground(new ColorRectTexture(0xff4444aa))); + + y += 16; + for (int i = 0; i < 4; i++) { + int fx = 10 + i * (W - 20) / 4; + TextFieldWidget ff = new TextFieldWidget(fx, y, 35, 12, () -> "", s -> {}); + ff.setCurrentString(String.valueOf(defaults[i * 2])); + ff.setBordered(true); + ff.setId("wf_f" + (i + 1)); + group.addWidget(ff); + + TextFieldWidget sf = new TextFieldWidget(fx + 40, y, 35, 12, () -> "", s -> {}); + sf.setCurrentString(String.valueOf(defaults[i * 2 + 1])); + sf.setBordered(true); + sf.setId("wf_s" + (i + 1)); + group.addWidget(sf); + } + + y += 18; + group.addWidget(new ButtonWidget(W / 2 - 30, y, 60, 16, cd -> { + try { + int f1 = parseIntField(group, "wf_f1"), f2 = parseIntField(group, "wf_f2"); + int f3 = parseIntField(group, "wf_f3"), f4 = parseIntField(group, "wf_f4"); + int s1 = parseIntField(group, "wf_s1"), s2 = parseIntField(group, "wf_s2"); + int s3 = parseIntField(group, "wf_s3"), s4 = parseIntField(group, "wf_s4"); + + var waveList = PulseWaveListGenerator.pulseWave( + new int[]{f1, f2, f3, f4}, new int[]{s1, s2, s3, s4}); + IPowerBoxMsg.Pulse msg = new IPowerBoxMsg.Pulse(Channel.A, waveList, 0); + PowerBoxMessage pbm = msg.toPowerBoxMessage(CLIENT_ID, CLIENT_ID, + MessageDirection.DirectType.CLIENT_TO_SERVER); + DGLabManagerHolder.getServerManager().ifPresent(mgr -> mgr.sendToAll(pbm)); + } catch (NumberFormatException ignored) {} + }).setBackground(new ColorRectTexture(0xff994400))); + + group.addWidget(new ButtonWidget(W / 2 + 38, y, 60, 16, cd -> { + try { + int f1 = parseIntField(group, "wf_f1"), f2 = parseIntField(group, "wf_f2"); + int f3 = parseIntField(group, "wf_f3"), f4 = parseIntField(group, "wf_f4"); + int s1 = parseIntField(group, "wf_s1"), s2 = parseIntField(group, "wf_s2"); + int s3 = parseIntField(group, "wf_s3"), s4 = parseIntField(group, "wf_s4"); + + var waveList = PulseWaveListGenerator.pulseWave( + new int[]{f1, f2, f3, f4}, new int[]{s1, s2, s3, s4}); + IPowerBoxMsg.Clear clear = new IPowerBoxMsg.Clear(Channel.A); + IPowerBoxMsg.Pulse pulse = new IPowerBoxMsg.Pulse(Channel.A, waveList, 0); + PowerBoxMessage clearMsg = clear.toPowerBoxMessage(CLIENT_ID, CLIENT_ID, + MessageDirection.DirectType.CLIENT_TO_SERVER); + PowerBoxMessage pulseMsg = pulse.toPowerBoxMessage(CLIENT_ID, CLIENT_ID, + MessageDirection.DirectType.CLIENT_TO_SERVER); + DGLabManagerHolder.getServerManager().ifPresent(mgr -> { + mgr.sendToAll(clearMsg); + mgr.sendToAll(pulseMsg); + }); + } catch (NumberFormatException ignored) {} + }).setBackground(new ColorRectTexture(0xff994400))); + } + + private int parseIntField(WidgetGroup group, String id) { + Widget w = group.getFirstWidgetById(id); + if (w instanceof TextFieldWidget tf) { + return Integer.parseInt(tf.getCurrentString()); + } + throw new NumberFormatException(); + } + + // ==================== Feedback Tab ==================== + + private void buildFeedbackTab(WidgetGroup group) { + int y = 8; + group.addWidget(new ImageWidget(10, y, 80, 10, + new TextTexture(Component.translatable("gui.dglabgame.feedback.send").getString(), 0xffcccccc))); + + y += 16; + var presets = ClientConfig.FEEDBACK_PRESETS.get(); + int cols = 4; + int btnW = (W - 30) / cols; + int row = 0, col = 0; + for (String presetStr : presets) { + try { + int val = Integer.parseInt(presetStr.trim()); + int bx = 10 + col * (btnW + 2); + int by = y + row * 18; + group.addWidget(new ButtonWidget(bx, by, btnW, 14, cd -> sendFeedback(val)) + .setBackground(new ColorRectTexture(0xff444488))); + col++; + if (col >= cols) { col = 0; row++; } + } catch (NumberFormatException ignored) {} + } + + y += (row + 1) * 18 + 6; + TextFieldWidget customField = new TextFieldWidget(10, y, 50, 14, () -> "", s -> {}); + customField.setCurrentString("5"); + customField.setBordered(true); + customField.setId("feedback_custom"); + group.addWidget(customField); + + group.addWidget(new ButtonWidget(65, y, 50, 14, cd -> { + try { + TextFieldWidget f = (TextFieldWidget) group.getFirstWidgetById("feedback_custom"); + sendFeedback(Integer.parseInt(f.getCurrentString())); + } catch (NumberFormatException ignored) {} + }).setBackground(new ColorRectTexture(0xff444488))); + } + + private void sendFeedback(int value) { + IPowerBoxMsg.Feedback msg = new IPowerBoxMsg.Feedback(Math.max(0, Math.min(10, value))); + PowerBoxMessage pbm = msg.toPowerBoxMessage(CLIENT_ID, CLIENT_ID, + MessageDirection.DirectType.CLIENT_TO_SERVER); + DGLabManagerHolder.getServerManager().ifPresent(mgr -> mgr.sendToAll(pbm)); + } + + // ==================== Server Tab ==================== + + private void buildServerTab(WidgetGroup group) { + int y = 8; + group.addWidget(new ImageWidget(10, y, 60, 10, + new TextTexture(Component.translatable("gui.dglabgame.server.port").getString(), 0xffcccccc))); + TextFieldWidget portField = new TextFieldWidget(80, y, 60, 14, () -> "", s -> {}); + portField.setCurrentString(String.valueOf(CommonConfig.SERVER_PORT.get())); + portField.setBordered(true); + portField.setId("server_port"); + group.addWidget(portField); + + y += 18; + group.addWidget(new ImageWidget(10, y, 60, 10, + new TextTexture(Component.translatable("gui.dglabgame.server.max_conn").getString(), 0xffcccccc))); + TextFieldWidget maxField = new TextFieldWidget(80, y, 60, 14, () -> "", s -> {}); + maxField.setCurrentString(String.valueOf(CommonConfig.MAX_CONNECTIONS.get())); + maxField.setBordered(true); + maxField.setId("max_connections"); + group.addWidget(maxField); + + y += 20; + group.addWidget(new ButtonWidget(10, y, 60, 16, cd -> { + try { + TextFieldWidget pf = (TextFieldWidget) group.getFirstWidgetById("server_port"); + TextFieldWidget mf = (TextFieldWidget) group.getFirstWidgetById("max_connections"); + int port = Integer.parseInt(pf.getCurrentString()); + int max = Integer.parseInt(mf.getCurrentString()); + DLGNetworkHandler.CHANNEL.sendToServer(new UpdateServerConfigPacket(port, max)); + } catch (NumberFormatException ignored) {} + }).setBackground(new ColorRectTexture(0xff336633))); + + Optional mgr = DGLabManagerHolder.getServerManager(); + String statusText = mgr.map(m -> m.getStatus().name()).orElse("NO SERVER"); + group.addWidget(new ImageWidget(80, y + 2, 120, 10, + new TextTexture(Component.translatable("gui.dglabgame.server.status", statusText).getString(), 0xffaaaaaa))); + + y += 20; + group.addWidget(new ButtonWidget(10, y, 60, 16, cd -> { + Optional sm = DGLabManagerHolder.getServerManager(); + if (sm.isEmpty() || sm.get().getStatus() == Status.STOPPED) { + DGLabManagerHolder.getOrCreateServerManager().start(); + } + }).setBackground(new ColorRectTexture(0xff226622))); + + group.addWidget(new ButtonWidget(80, y, 60, 16, cd -> { + DGLabManagerHolder.getServerManager().ifPresent(ServerManager::stop); + }).setBackground(new ColorRectTexture(0xff662222))); + } + + // ==================== Bottom Bar ==================== + + private WidgetGroup buildBottomBar() { + WidgetGroup bar = new WidgetGroup(5, H - 30, W - 10, 22); + bar.setBackground(ResourceBorderTexture.BORDERED_BACKGROUND); + + bar.addWidget(new ImageWidget(10, 4, 40, 10, + new TextTexture(Component.translatable("gui.dglabgame.client.label").getString(), 0xffaaaaaa))); + + var names = DGLabManagerHolder.getClientManager().getClientNames(); + String current = names.isEmpty() ? "none" : names.iterator().next(); + + bar.addWidget(new ButtonWidget(55, 2, 60, 14, cd -> { + DGLabManagerHolder.getClientManager().getClient(current).ifPresent(DGPBClientManager::start); + }).setBackground(new ColorRectTexture(0xff226622))); + + bar.addWidget(new ButtonWidget(120, 2, 60, 14, cd -> { + DGLabManagerHolder.getClientManager().getClient(current).ifPresent(DGPBClientManager::stop); + }).setBackground(new ColorRectTexture(0xff662222))); + + return bar; + } +} diff --git a/src/main/java/top/leisuretimedock/dglabgame/config/ClientConfig.java b/src/main/java/top/leisuretimedock/dglabgame/config/ClientConfig.java new file mode 100644 index 0000000..71d6007 --- /dev/null +++ b/src/main/java/top/leisuretimedock/dglabgame/config/ClientConfig.java @@ -0,0 +1,39 @@ +package top.leisuretimedock.dglabgame.config; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.common.ForgeConfigSpec; + +@OnlyIn(Dist.CLIENT) +public class ClientConfig { + public static final ForgeConfigSpec SPEC; + + public static final ForgeConfigSpec.BooleanValue ENABLE_SSL; + public static final ForgeConfigSpec.IntValue CONNECTION_TIMEOUT; + public static final ForgeConfigSpec.ConfigValue> FEEDBACK_PRESETS; + + static { + ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); + + builder.push("connection"); + ENABLE_SSL = builder + .comment("Whether to use SSL/WSS for the WebSocket connection to the DGLab relay.", + "Requires a valid SSL certificate on the relay server side.") + .define("enableSSL", false); + CONNECTION_TIMEOUT = builder + .comment("Connection timeout in milliseconds for WebSocket handshake.") + .defineInRange("connectionTimeout", 5000, 1000, 30000); + builder.pop(); + + builder.push("feedback"); + FEEDBACK_PRESETS = builder + .comment("Preset feedback message labels.", + "These appear as quick-select buttons in the management GUI.") + .defineList("presets", + java.util.List.of("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"), + o -> o instanceof String && !((String) o).isBlank()); + builder.pop(); + + SPEC = builder.build(); + } +} diff --git a/src/main/java/top/leisuretimedock/dglabgame/config/CommonConfig.java b/src/main/java/top/leisuretimedock/dglabgame/config/CommonConfig.java new file mode 100644 index 0000000..543b400 --- /dev/null +++ b/src/main/java/top/leisuretimedock/dglabgame/config/CommonConfig.java @@ -0,0 +1,91 @@ +package top.leisuretimedock.dglabgame.config; + +import net.minecraftforge.common.ForgeConfigSpec; + +public class CommonConfig { + public static final ForgeConfigSpec SPEC; + + public static final ForgeConfigSpec.ConfigValue ADDRESS; + public static final ForgeConfigSpec.IntValue PORT; + + public static final ForgeConfigSpec.IntValue MAX_STRENGTH_A; + public static final ForgeConfigSpec.IntValue MAX_STRENGTH_B; + public static final ForgeConfigSpec.IntValue MIN_STRENGTH_A; + public static final ForgeConfigSpec.IntValue MIN_STRENGTH_B; + + public static final ForgeConfigSpec.IntValue MAX_FREQUENCY; + public static final ForgeConfigSpec.IntValue MIN_FREQUENCY; + public static final ForgeConfigSpec.IntValue MAX_PULSE_STRENGTH; + public static final ForgeConfigSpec.IntValue ANTI_SHAKE_DELAY; + + public static final ForgeConfigSpec.IntValue COUNTDOWN_SECONDS; + + public static final ForgeConfigSpec.IntValue SERVER_PORT; + public static final ForgeConfigSpec.IntValue MAX_CONNECTIONS; + + static { + ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); + + builder.push("websocket"); + ADDRESS = builder + .comment("WebSocket server address for the DGLab PowerBox relay.", + "Change this if the relay is running on a different machine.") + .define("address", "127.0.0.1", + o -> o instanceof String && !((String) o).isBlank()); + PORT = builder + .comment("WebSocket server port for the DGLab PowerBox relay.") + .defineInRange("port", 9000, 1, 65535); + builder.pop(); + + builder.push("strength_limits"); + MAX_STRENGTH_A = builder + .comment("Maximum allowed strength for channel A.") + .defineInRange("maxStrengthA", 200, 0, 200); + MAX_STRENGTH_B = builder + .comment("Maximum allowed strength for channel B.") + .defineInRange("maxStrengthB", 200, 0, 200); + MIN_STRENGTH_A = builder + .comment("Minimum allowed strength for channel A.") + .defineInRange("minStrengthA", 0, 0, 200); + MIN_STRENGTH_B = builder + .comment("Minimum allowed strength for channel B.") + .defineInRange("minStrengthB", 0, 0, 200); + builder.pop(); + + builder.push("pulse"); + MAX_FREQUENCY = builder + .comment("Maximum pulse frequency in Hz.") + .defineInRange("maxFrequency", 240, 10, 240); + MIN_FREQUENCY = builder + .comment("Minimum pulse frequency in Hz.") + .defineInRange("minFrequency", 10, 10, 240); + MAX_PULSE_STRENGTH = builder + .comment("Maximum pulse strength (0-100).") + .defineInRange("maxPulseStrength", 100, 0, 100); + ANTI_SHAKE_DELAY = builder + .comment("Anti-shake debounce delay in milliseconds.", + "Prevents rapid repeated pulse triggers within this window.") + .defineInRange("antiShakeDelay", 500, 0, 5000); + builder.pop(); + + builder.push("agreement"); + COUNTDOWN_SECONDS = builder + .comment("Countdown duration in seconds for the user agreement confirmation screen.", + "Set to 0 to allow instant confirmation (not recommended).") + .defineInRange("countdownSeconds", 5, 0, 60); + builder.pop(); + + builder.push("server"); + SERVER_PORT = builder + .comment("Port for the DGLab WebSocket server to listen on.", + "Restart the server for changes to take effect.") + .defineInRange("serverPort", 9000, 1, 65535); + MAX_CONNECTIONS = builder + .comment("Maximum number of concurrent WebSocket connections allowed.", + "Additional connections beyond this limit will be rejected.") + .defineInRange("maxConnections", 128, 1, 1024); + builder.pop(); + + SPEC = builder.build(); + } +} diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/capability/DGLabDataSyncManager.java b/src/main/java/top/leisuretimedock/dglabgame/core/capability/DGLabDataSyncManager.java index b89f6ab..68f4dec 100644 --- a/src/main/java/top/leisuretimedock/dglabgame/core/capability/DGLabDataSyncManager.java +++ b/src/main/java/top/leisuretimedock/dglabgame/core/capability/DGLabDataSyncManager.java @@ -1,15 +1,15 @@ package top.leisuretimedock.dglabgame.core.capability; +import net.minecraftforge.common.capabilities.Capability; import top.r3944realms.lib39.core.sync.CachedSyncManager; import java.util.HashMap; import java.util.Map; -import java.util.UUID; -public class DGLabDataSyncManager extends CachedSyncManager { - public static Map playerDungeonData = new HashMap<>(); +public class DGLabDataSyncManager extends CachedSyncManager, AbstractDGLabData> { + public static Map, AbstractDGLabData> playerDungeonData = new HashMap<>(); @Override - public Map getSyncMap() { + public Map, AbstractDGLabData> getSyncMap() { return playerDungeonData; } } diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/capability/EntityDGLabData.java b/src/main/java/top/leisuretimedock/dglabgame/core/capability/EntityDGLabData.java index 4aab0ea..db1dc99 100644 --- a/src/main/java/top/leisuretimedock/dglabgame/core/capability/EntityDGLabData.java +++ b/src/main/java/top/leisuretimedock/dglabgame/core/capability/EntityDGLabData.java @@ -34,4 +34,8 @@ public final class EntityDGLabData extends AbstractDGLabData { public int entityId() { return entity.getId(); } + + @Override + public void update() { + } } diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/command/DGLabCommands.java b/src/main/java/top/leisuretimedock/dglabgame/core/command/DGLabCommands.java new file mode 100644 index 0000000..d9deb3a --- /dev/null +++ b/src/main/java/top/leisuretimedock/dglabgame/core/command/DGLabCommands.java @@ -0,0 +1,410 @@ +package top.leisuretimedock.dglabgame.core.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.r3944realms.dg_lab.api.manager.Status; +import com.r3944realms.dg_lab.api.message.IPowerBoxMsg; +import com.r3944realms.dg_lab.api.message.argType.Channel; +import com.r3944realms.dg_lab.api.message.argType.ChangePolicy; +import com.r3944realms.dg_lab.api.message.data.PulseWaveListGenerator; +import com.r3944realms.dg_lab.api.websocket.message.MessageDirection; +import com.r3944realms.dg_lab.api.websocket.message.PowerBoxMessage; +import com.r3944realms.dg_lab.api.websocket.message.role.WebSocketClientRole; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import top.leisuretimedock.dglabgame.config.CommonConfig; +import top.leisuretimedock.dglabgame.core.manager.ClientManager; +import top.leisuretimedock.dglabgame.core.manager.DGLabManagerHolder; +import top.leisuretimedock.dglabgame.core.manager.ServerManager; + +import java.util.Optional; + +public class DGLabCommands { + private static final String CLIENT_ID = "dglabgame"; + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register( + Commands.literal("dglab") + .requires(source -> source.hasPermission(2)) + .then(serverNode()) + .then(clientNode()) + .then(strengthNode()) + .then(pulseNode()) + .then(clearNode()) + .then(feedbackNode()) + ); + } + + // ==================== Server Commands ==================== + + private static com.mojang.brigadier.builder.LiteralArgumentBuilder serverNode() { + return Commands.literal("server") + .then(Commands.literal("start") + .executes(ctx -> serverStart(ctx, CommonConfig.PORT.get())) + .then(Commands.argument("port", IntegerArgumentType.integer(1, 65535)) + .executes(ctx -> serverStart(ctx, IntegerArgumentType.getInteger(ctx, "port"))) + ) + ) + .then(Commands.literal("stop") + .executes(DGLabCommands::serverStop) + ) + .then(Commands.literal("status") + .executes(DGLabCommands::serverStatus) + ); + } + + private static int serverStart(CommandContext ctx, int port) { + Optional existing = DGLabManagerHolder.getServerManager(); + if (existing.isPresent() && existing.get().getStatus() != Status.STOPPED) { + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.server.already_running"), false + ); + return 0; + } + ServerManager mgr = DGLabManagerHolder.getOrCreateServerManager(); + mgr.start(); + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.server.started", port), true + ); + return 1; + } + + private static int serverStop(CommandContext ctx) { + Optional existing = DGLabManagerHolder.getServerManager(); + if (existing.isEmpty() || existing.get().getStatus() == Status.STOPPED) { + ctx.getSource().sendFailure( + Component.translatable("dglabgame.command.server.not_running") + ); + return 0; + } + existing.get().stop(); + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.server.stopped"), true + ); + return 1; + } + + private static int serverStatus(CommandContext ctx) { + Optional existing = DGLabManagerHolder.getServerManager(); + if (existing.isEmpty()) { + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.server.not_created"), false + ); + } else { + ServerManager mgr = existing.get(); + Status status = mgr.getStatus(); + int clients = mgr.getConnectedClients().size(); + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.server.status", status.name(), clients), false + ); + } + return 1; + } + + // ==================== Client Commands ==================== + + private static com.mojang.brigadier.builder.LiteralArgumentBuilder clientNode() { + return Commands.literal("client") + .then(Commands.literal("create") + .then(Commands.argument("name", StringArgumentType.word()) + .executes(ctx -> clientCreate(ctx, + StringArgumentType.getString(ctx, "name"), + CommonConfig.ADDRESS.get(), + CommonConfig.PORT.get())) + .then(Commands.argument("address", StringArgumentType.word()) + .executes(ctx -> clientCreate(ctx, + StringArgumentType.getString(ctx, "name"), + StringArgumentType.getString(ctx, "address"), + CommonConfig.PORT.get())) + .then(Commands.argument("port", IntegerArgumentType.integer(1, 65535)) + .executes(ctx -> clientCreate(ctx, + StringArgumentType.getString(ctx, "name"), + StringArgumentType.getString(ctx, "address"), + IntegerArgumentType.getInteger(ctx, "port"))) + ) + ) + ) + ) + .then(Commands.literal("remove") + .then(Commands.argument("name", StringArgumentType.word()) + .executes(DGLabCommands::clientRemove) + ) + ) + .then(Commands.literal("start") + .then(Commands.argument("name", StringArgumentType.word()) + .executes(DGLabCommands::clientStart) + ) + ) + .then(Commands.literal("stop") + .then(Commands.argument("name", StringArgumentType.word()) + .executes(DGLabCommands::clientStop) + ) + ) + .then(Commands.literal("list") + .executes(DGLabCommands::clientList) + ); + } + + private static int clientCreate(CommandContext ctx, String name, String address, int port) { + ClientManager mgr = DGLabManagerHolder.getClientManager(); + WebSocketClientRole role = WebSocketClientRole.of(CLIENT_ID + "_" + name); + mgr.createClient(name, role, address, port); + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.client.created", name, address, port), true + ); + return 1; + } + + private static int clientRemove(CommandContext ctx) { + String name = StringArgumentType.getString(ctx, "name"); + ClientManager mgr = DGLabManagerHolder.getClientManager(); + if (mgr.getClient(name).isEmpty()) { + ctx.getSource().sendFailure( + Component.translatable("dglabgame.command.client.not_found", name) + ); + return 0; + } + mgr.removeClient(name); + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.client.removed", name), true + ); + return 1; + } + + private static int clientStart(CommandContext ctx) { + String name = StringArgumentType.getString(ctx, "name"); + ClientManager mgr = DGLabManagerHolder.getClientManager(); + Optional client = mgr.getClient(name); + if (client.isEmpty()) { + ctx.getSource().sendFailure( + Component.translatable("dglabgame.command.client.not_found", name) + ); + return 0; + } + client.get().start(); + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.client.started", name), true + ); + return 1; + } + + private static int clientStop(CommandContext ctx) { + String name = StringArgumentType.getString(ctx, "name"); + ClientManager mgr = DGLabManagerHolder.getClientManager(); + Optional client = mgr.getClient(name); + if (client.isEmpty()) { + ctx.getSource().sendFailure( + Component.translatable("dglabgame.command.client.not_found", name) + ); + return 0; + } + client.get().stop(); + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.client.stopped", name), true + ); + return 1; + } + + private static int clientList(CommandContext ctx) { + ClientManager mgr = DGLabManagerHolder.getClientManager(); + var names = mgr.getClientNames(); + if (names.isEmpty()) { + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.client.none"), false + ); + } else { + String list = String.join(", ", names); + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.client.list", list), false + ); + } + return 1; + } + + // ==================== Device Commands ==================== + + private static com.mojang.brigadier.builder.LiteralArgumentBuilder strengthNode() { + return Commands.literal("strength") + .then(Commands.argument("channel", StringArgumentType.word()) + .then(Commands.argument("policy", StringArgumentType.word()) + .then(Commands.argument("value", IntegerArgumentType.integer(0, 200)) + .executes(DGLabCommands::strengthExec) + ) + ) + ); + } + + private static int strengthExec(CommandContext ctx) { + Channel channel = parseChannel(StringArgumentType.getString(ctx, "channel")); + if (channel == null) { + ctx.getSource().sendFailure(Component.translatable("dglabgame.command.invalid_channel")); + return 0; + } + ChangePolicy policy = parsePolicy(StringArgumentType.getString(ctx, "policy")); + if (policy == null) { + ctx.getSource().sendFailure(Component.translatable("dglabgame.command.invalid_policy")); + return 0; + } + int value = IntegerArgumentType.getInteger(ctx, "value"); + + IPowerBoxMsg.StrengthChange msg = new IPowerBoxMsg.StrengthChange(channel, policy, value); + int sent = sendToAll(msg); + if (sent == 0) { + ctx.getSource().sendFailure(Component.translatable("dglabgame.command.no_output")); + return 0; + } + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.strength.sent", channel.name(), policy.name(), value, sent), true + ); + return 1; + } + + private static com.mojang.brigadier.builder.LiteralArgumentBuilder pulseNode() { + return Commands.literal("pulse") + .then(Commands.argument("channel", StringArgumentType.word()) + .then(Commands.argument("f1", IntegerArgumentType.integer(10, 240)) + .then(Commands.argument("s1", IntegerArgumentType.integer(0, 100)) + .then(Commands.argument("f2", IntegerArgumentType.integer(10, 240)) + .then(Commands.argument("s2", IntegerArgumentType.integer(0, 100)) + .then(Commands.argument("f3", IntegerArgumentType.integer(10, 240)) + .then(Commands.argument("s3", IntegerArgumentType.integer(0, 100)) + .then(Commands.argument("f4", IntegerArgumentType.integer(10, 240)) + .then(Commands.argument("s4", IntegerArgumentType.integer(0, 100)) + .executes(DGLabCommands::pulseExec) + .then(Commands.argument("timer", IntegerArgumentType.integer(0, 60000)) + .executes(DGLabCommands::pulseExec) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ); + } + + private static int pulseExec(CommandContext ctx) { + Channel channel = parseChannel(StringArgumentType.getString(ctx, "channel")); + if (channel == null) { + ctx.getSource().sendFailure(Component.translatable("dglabgame.command.invalid_channel")); + return 0; + } + int f1 = IntegerArgumentType.getInteger(ctx, "f1"); + int s1 = IntegerArgumentType.getInteger(ctx, "s1"); + int f2 = IntegerArgumentType.getInteger(ctx, "f2"); + int s2 = IntegerArgumentType.getInteger(ctx, "s2"); + int f3 = IntegerArgumentType.getInteger(ctx, "f3"); + int s3 = IntegerArgumentType.getInteger(ctx, "s3"); + int f4 = IntegerArgumentType.getInteger(ctx, "f4"); + int s4 = IntegerArgumentType.getInteger(ctx, "s4"); + + var waveList = PulseWaveListGenerator.pulseWave( + new int[]{f1, f2, f3, f4}, + new int[]{s1, s2, s3, s4} + ); + + Integer timer = null; + try { + timer = IntegerArgumentType.getInteger(ctx, "timer"); + } catch (IllegalArgumentException ignored) { + } + + IPowerBoxMsg.Pulse msg = new IPowerBoxMsg.Pulse(channel, waveList, timer); + int sent = sendToAll(msg); + if (sent == 0) { + ctx.getSource().sendFailure(Component.translatable("dglabgame.command.no_output")); + return 0; + } + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.pulse.sent", channel.name(), sent), true + ); + return 1; + } + + private static com.mojang.brigadier.builder.LiteralArgumentBuilder clearNode() { + return Commands.literal("clear") + .then(Commands.argument("channel", StringArgumentType.word()) + .executes(DGLabCommands::clearExec) + ); + } + + private static int clearExec(CommandContext ctx) { + Channel channel = parseChannel(StringArgumentType.getString(ctx, "channel")); + if (channel == null) { + ctx.getSource().sendFailure(Component.translatable("dglabgame.command.invalid_channel")); + return 0; + } + + IPowerBoxMsg.Clear msg = new IPowerBoxMsg.Clear(channel); + int sent = sendToAll(msg); + if (sent == 0) { + ctx.getSource().sendFailure(Component.translatable("dglabgame.command.no_output")); + return 0; + } + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.clear.sent", channel.name(), sent), true + ); + return 1; + } + + private static com.mojang.brigadier.builder.LiteralArgumentBuilder feedbackNode() { + return Commands.literal("feedback") + .then(Commands.argument("value", IntegerArgumentType.integer(0, 10)) + .executes(DGLabCommands::feedbackExec) + ); + } + + private static int feedbackExec(CommandContext ctx) { + int value = IntegerArgumentType.getInteger(ctx, "value"); + + IPowerBoxMsg.Feedback msg = new IPowerBoxMsg.Feedback(value); + int sent = sendToAll(msg); + if (sent == 0) { + ctx.getSource().sendFailure(Component.translatable("dglabgame.command.no_output")); + return 0; + } + ctx.getSource().sendSuccess( + () -> Component.translatable("dglabgame.command.feedback.sent", value, sent), true + ); + return 1; + } + + // ==================== Send Helpers ==================== + + private static int sendToAll(IPowerBoxMsg msg) { + int sent = 0; + Optional serverMgr = DGLabManagerHolder.getServerManager(); + if (serverMgr.isPresent()) { + ServerManager mgr = serverMgr.get(); + if (mgr.getStatus() == Status.RUNNING) { + PowerBoxMessage pbm = msg.toPowerBoxMessage(CLIENT_ID, CLIENT_ID, + MessageDirection.DirectType.SERVER_TO_CLIENT); + mgr.sendToAll(pbm); + sent += mgr.getConnectedClients().size(); + } + } + return sent; + } + + private static Channel parseChannel(String s) { + return switch (s.toLowerCase()) { + case "a" -> Channel.A; + case "b" -> Channel.B; + default -> null; + }; + } + + private static ChangePolicy parsePolicy(String s) { + return switch (s.toLowerCase()) { + case "increase", "inc", "+" -> ChangePolicy.INCREASE; + case "decrease", "dec", "-" -> ChangePolicy.DECREASE; + case "goto", "set", "=" -> ChangePolicy.GOTO; + default -> null; + }; + } +} diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/event/ClientEventHandler.java b/src/main/java/top/leisuretimedock/dglabgame/core/event/ClientEventHandler.java index 114e8d9..c6c4b78 100644 --- a/src/main/java/top/leisuretimedock/dglabgame/core/event/ClientEventHandler.java +++ b/src/main/java/top/leisuretimedock/dglabgame/core/event/ClientEventHandler.java @@ -16,6 +16,7 @@ import net.minecraftforge.fml.loading.FMLEnvironment; import top.leisuretimedock.dglabgame.DGLabGame; import top.leisuretimedock.dglabgame.client.DLGKeyMapping; import top.leisuretimedock.dglabgame.client.gui.ConfirmGui; +import top.leisuretimedock.dglabgame.client.gui.DGLabManagementGui; import top.leisuretimedock.dglabgame.core.capability.DGLabDataProvider; public class ClientEventHandler { @@ -33,6 +34,15 @@ public class ClientEventHandler { } else player.displayClientMessage(Component.literal("You had accept use agreements").withStyle(ChatFormatting.RED).withStyle(ChatFormatting.BOLD), true); }); } + if (DLGKeyMapping.KEY_MANAGEMENT.isDown()) { + player.getCapability(DGLabDataProvider.DG_LAB_DATA_CAPABILITY).ifPresent(data -> { + if (data.hasAcceptUseAgreements()) { + new DGLabManagementGui().display(); + } else { + player.displayClientMessage(Component.translatable("dglabgame.user_agreement.reject_use").withStyle(ChatFormatting.RED), true); + } + }); + } } } @net.minecraftforge.fml.common.Mod.EventBusSubscriber(modid = DGLabGame.MOD_ID, bus = net.minecraftforge.fml.common.Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) @@ -40,6 +50,7 @@ public class ClientEventHandler { @SubscribeEvent public static void onRegisterKeyMappings (RegisterKeyMappingsEvent event) { event.register(DLGKeyMapping.KEY_TEST); + event.register(DLGKeyMapping.KEY_MANAGEMENT); } } diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/event/CommonEventHandler.java b/src/main/java/top/leisuretimedock/dglabgame/core/event/CommonEventHandler.java index d12a7f8..e0e94d8 100644 --- a/src/main/java/top/leisuretimedock/dglabgame/core/event/CommonEventHandler.java +++ b/src/main/java/top/leisuretimedock/dglabgame/core/event/CommonEventHandler.java @@ -4,10 +4,12 @@ import net.minecraft.resources.ResourceLocation; import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent; import net.minecraftforge.data.event.GatherDataEvent; import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import top.leisuretimedock.dglabgame.DGLabGame; import top.leisuretimedock.dglabgame.core.capability.DGLabDataProvider; import top.leisuretimedock.dglabgame.core.capability.DGLabDataSyncManager; +import top.leisuretimedock.dglabgame.core.command.DGLabCommands; import top.leisuretimedock.dglabgame.core.register.DLGCapabilities; import top.leisuretimedock.dglabgame.datagen.DLGDataGenEvent; import top.r3944realms.lib39.api.event.SyncManagerRegisterEvent; @@ -41,5 +43,10 @@ public class CommonEventHandler { public static void attachCapability(AttachCapabilitiesEvent event) { DLGCapabilities.attachCapability(event); } + + @SubscribeEvent + public static void onRegisterCommands(RegisterCommandsEvent event) { + DGLabCommands.register(event.getDispatcher()); + } } } diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/manager/ClientManager.java b/src/main/java/top/leisuretimedock/dglabgame/core/manager/ClientManager.java new file mode 100644 index 0000000..15638df --- /dev/null +++ b/src/main/java/top/leisuretimedock/dglabgame/core/manager/ClientManager.java @@ -0,0 +1,67 @@ +package top.leisuretimedock.dglabgame.core.manager; + +import com.r3944realms.dg_lab.api.websocket.message.role.WebSocketClientRole; +import com.r3944realms.dg_lab.manager.DGPBClientManager; +import com.r3944realms.dg_lab.websocket.PowerBoxWSClient; +import com.r3944realms.dg_lab.websocket.handler.client.DefaultClientOperation; +import com.r3944realms.dg_lab.websocket.sharedData.ClientPowerBoxSharedData; +import top.leisuretimedock.dglabgame.config.CommonConfig; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class ClientManager implements IManager> { + private final Map clients = new HashMap<>(); + + public DGPBClientManager createClient(String key, WebSocketClientRole role) { + return createClient(key, role, CommonConfig.ADDRESS.get(), CommonConfig.PORT.get()); + } + + public DGPBClientManager createClient(String key, WebSocketClientRole role, String address, int port) { + ClientPowerBoxSharedData sharedData = new ClientPowerBoxSharedData( + CommonConfig.ANTI_SHAKE_DELAY.get() + ); + sharedData.address = address; + sharedData.port = port; + + PowerBoxWSClient wsClient = new PowerBoxWSClient( + sharedData, role, new DefaultClientOperation(), + address, port + ); + DGPBClientManager client = new DGPBClientManager(wsClient); + clients.putIfAbsent(key, client); + return client; + } + + public java.util.Set getClientNames() { + return clients.keySet(); + } + + public void addClient(String key, DGPBClientManager client) { + clients.putIfAbsent(key, client); + } + + public void removeClient(String key) { + Optional.ofNullable(clients.remove(key)).ifPresent(DGPBClientManager::stop); + } + + public Optional getClient(String key) { + return Optional.ofNullable(clients.get(key)); + } + + @Override + public void startAll() { + clients.values().forEach(DGPBClientManager::start); + } + + @Override + public void stopAll() { + clients.values().forEach(DGPBClientManager::stop); + } + + @Override + public Optional> getInstance() { + return Optional.of(clients); + } +} diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/manager/DGLabManagerHolder.java b/src/main/java/top/leisuretimedock/dglabgame/core/manager/DGLabManagerHolder.java new file mode 100644 index 0000000..3fe8808 --- /dev/null +++ b/src/main/java/top/leisuretimedock/dglabgame/core/manager/DGLabManagerHolder.java @@ -0,0 +1,44 @@ +package top.leisuretimedock.dglabgame.core.manager; + +import com.r3944realms.dg_lab.api.websocket.message.role.WebSocketServerRole; +import com.r3944realms.dg_lab.manager.DGPBServerManager; +import com.r3944realms.dg_lab.websocket.PowerBoxWSServer; +import com.r3944realms.dg_lab.websocket.handler.server.DefaultServerOperation; +import com.r3944realms.dg_lab.websocket.sharedData.ServerPowerBoxSharedData; +import top.leisuretimedock.dglabgame.config.CommonConfig; + +import java.util.Optional; + +public class DGLabManagerHolder { + private static ServerManager serverManager; + private static final ClientManager clientManager = new ClientManager(); + + public static ClientManager getClientManager() { + return clientManager; + } + + public static Optional getServerManager() { + return Optional.ofNullable(serverManager); + } + + public static ServerManager getOrCreateServerManager() { + if (serverManager == null) { + ServerPowerBoxSharedData sharedData = new ServerPowerBoxSharedData(); + WebSocketServerRole role = WebSocketServerRole.of("dglabgame-server"); + PowerBoxWSServer server = new PowerBoxWSServer( + sharedData, role, new DefaultServerOperation(), + CommonConfig.SERVER_PORT.get() + ); + DGPBServerManager dgpbServerManager = new DGPBServerManager(server); + serverManager = new ServerManager(dgpbServerManager); + } + return serverManager; + } + + public static void setServerManager(ServerManager manager) { + if (serverManager != null) { + serverManager.stopAll(); + } + serverManager = manager; + } +} diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/manager/IManager.java b/src/main/java/top/leisuretimedock/dglabgame/core/manager/IManager.java new file mode 100644 index 0000000..ae8eb30 --- /dev/null +++ b/src/main/java/top/leisuretimedock/dglabgame/core/manager/IManager.java @@ -0,0 +1,9 @@ +package top.leisuretimedock.dglabgame.core.manager; + +import java.util.Optional; + +public interface IManager { + void startAll(); + void stopAll(); + Optional getInstance(); +} diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/manager/ServerManager.java b/src/main/java/top/leisuretimedock/dglabgame/core/manager/ServerManager.java new file mode 100644 index 0000000..34b7427 --- /dev/null +++ b/src/main/java/top/leisuretimedock/dglabgame/core/manager/ServerManager.java @@ -0,0 +1,75 @@ +package top.leisuretimedock.dglabgame.core.manager; + +import com.r3944realms.dg_lab.api.manager.Status; +import com.r3944realms.dg_lab.api.websocket.message.Message; +import com.r3944realms.dg_lab.api.websocket.sharedData.ISharedData; +import com.r3944realms.dg_lab.manager.DGPBServerManager; +import com.r3944realms.dg_lab.manager.IDGLabManager; +import com.r3944realms.dg_lab.websocket.sharedData.ServerPowerBoxSharedData; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.Set; + +public class ServerManager implements IManager, IDGLabManager { + private final DGPBServerManager serverManager; + + public ServerManager(@NotNull DGPBServerManager serverManager) { + this.serverManager = serverManager; + } + + @Override + public void start() { + serverManager.start(); + } + + @Override + public void stop() { + serverManager.stop(); + } + + @Override + public ISharedData getSharedData() { + return serverManager.getSharedData(); + } + + @Override + public Status getStatus() { + return serverManager.getStatus(); + } + + @Override + public void setStatus(Status status) { + serverManager.setStatus(status); + } + + @Override + public void startAll() { + start(); + } + + @Override + public void stopAll() { + stop(); + } + + @Override + public Optional getInstance() { + return Optional.of(serverManager); + } + + public void sendToAll(Message message) { + ISharedData sd = serverManager.getSharedData(); + if (sd instanceof ServerPowerBoxSharedData spsd) { + spsd.connections.keySet().forEach(id -> serverManager.send(id, message)); + } + } + + public Set getConnectedClients() { + ISharedData sd = serverManager.getSharedData(); + if (sd instanceof ServerPowerBoxSharedData spsd) { + return spsd.connections.keySet(); + } + return Set.of(); + } +} diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/network/DLGNetworkHandler.java b/src/main/java/top/leisuretimedock/dglabgame/core/network/DLGNetworkHandler.java index 3121f05..f7c54a4 100644 --- a/src/main/java/top/leisuretimedock/dglabgame/core/network/DLGNetworkHandler.java +++ b/src/main/java/top/leisuretimedock/dglabgame/core/network/DLGNetworkHandler.java @@ -5,6 +5,8 @@ import net.minecraftforge.network.NetworkRegistry; import net.minecraftforge.network.simple.SimpleChannel; import top.leisuretimedock.dglabgame.DGLabGame; import top.leisuretimedock.dglabgame.core.network.toServer.AcceptUserAgreementPacket; +import top.leisuretimedock.dglabgame.core.network.toServer.UpdateServerConfigPacket; +import top.leisuretimedock.dglabgame.core.network.toServer.UpdateStrengthConfigPacket; import java.util.concurrent.atomic.AtomicInteger; @@ -22,6 +24,16 @@ public class DLGNetworkHandler { .decoder(AcceptUserAgreementPacket::decode) .consumerMainThread(AcceptUserAgreementPacket::handle) .add(); + CHANNEL.messageBuilder(UpdateServerConfigPacket.class, getIndex(), NetworkDirection.PLAY_TO_SERVER) + .encoder(UpdateServerConfigPacket::encode) + .decoder(UpdateServerConfigPacket::decode) + .consumerMainThread(UpdateServerConfigPacket::handle) + .add(); + CHANNEL.messageBuilder(UpdateStrengthConfigPacket.class, getIndex(), NetworkDirection.PLAY_TO_SERVER) + .encoder(UpdateStrengthConfigPacket::encode) + .decoder(UpdateStrengthConfigPacket::decode) + .consumerMainThread(UpdateStrengthConfigPacket::handle) + .add(); } public static int getIndex() { diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/network/toServer/UpdateServerConfigPacket.java b/src/main/java/top/leisuretimedock/dglabgame/core/network/toServer/UpdateServerConfigPacket.java new file mode 100644 index 0000000..8369681 --- /dev/null +++ b/src/main/java/top/leisuretimedock/dglabgame/core/network/toServer/UpdateServerConfigPacket.java @@ -0,0 +1,33 @@ +package top.leisuretimedock.dglabgame.core.network.toServer; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkEvent; +import org.jetbrains.annotations.NotNull; +import top.leisuretimedock.dglabgame.config.CommonConfig; + +import java.util.function.Supplier; + +public record UpdateServerConfigPacket(int serverPort, int maxConnections) { + public void encode(FriendlyByteBuf buf) { + buf.writeVarInt(serverPort); + buf.writeVarInt(maxConnections); + } + + public static UpdateServerConfigPacket decode(FriendlyByteBuf buf) { + return new UpdateServerConfigPacket(buf.readVarInt(), buf.readVarInt()); + } + + public void handle(@NotNull Supplier contextSupplier) { + NetworkEvent.Context context = contextSupplier.get(); + context.enqueueWork(() -> { + ServerPlayer sender = context.getSender(); + if (sender != null && sender.hasPermissions(4)) { + CommonConfig.SERVER_PORT.set(serverPort); + CommonConfig.MAX_CONNECTIONS.set(maxConnections); + CommonConfig.SPEC.save(); + } + context.setPacketHandled(true); + }); + } +} diff --git a/src/main/java/top/leisuretimedock/dglabgame/core/network/toServer/UpdateStrengthConfigPacket.java b/src/main/java/top/leisuretimedock/dglabgame/core/network/toServer/UpdateStrengthConfigPacket.java new file mode 100644 index 0000000..c49a8f3 --- /dev/null +++ b/src/main/java/top/leisuretimedock/dglabgame/core/network/toServer/UpdateStrengthConfigPacket.java @@ -0,0 +1,37 @@ +package top.leisuretimedock.dglabgame.core.network.toServer; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkEvent; +import org.jetbrains.annotations.NotNull; +import top.leisuretimedock.dglabgame.config.CommonConfig; + +import java.util.function.Supplier; + +public record UpdateStrengthConfigPacket(int maxA, int minA, int maxB, int minB) { + public void encode(FriendlyByteBuf buf) { + buf.writeVarInt(maxA); + buf.writeVarInt(minA); + buf.writeVarInt(maxB); + buf.writeVarInt(minB); + } + + public static UpdateStrengthConfigPacket decode(FriendlyByteBuf buf) { + return new UpdateStrengthConfigPacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public void handle(@NotNull Supplier contextSupplier) { + NetworkEvent.Context context = contextSupplier.get(); + context.enqueueWork(() -> { + ServerPlayer sender = context.getSender(); + if (sender != null) { + CommonConfig.MAX_STRENGTH_A.set(maxA); + CommonConfig.MIN_STRENGTH_A.set(minA); + CommonConfig.MAX_STRENGTH_B.set(maxB); + CommonConfig.MIN_STRENGTH_B.set(minB); + CommonConfig.SPEC.save(); + } + context.setPacketHandled(true); + }); + } +} diff --git a/src/main/java/top/leisuretimedock/dglabgame/datagen/value/DLGLangKey.java b/src/main/java/top/leisuretimedock/dglabgame/datagen/value/DLGLangKey.java index cf33d37..39d7f1c 100644 --- a/src/main/java/top/leisuretimedock/dglabgame/datagen/value/DLGLangKey.java +++ b/src/main/java/top/leisuretimedock/dglabgame/datagen/value/DLGLangKey.java @@ -61,6 +61,15 @@ public enum DLGLangKey implements ILangKeyValueCollection { ) ); + addLang( + LangKeyValue.ofKey("dglabgame.user_agreement.reject_use", ModPartEnum.NAME, + "Not agreed to user guidelines, usage rejected", + "未同意用户使用准则,拒绝使用", + "未同意使用者準則,拒絕使用", + "未允用戶之約,拒用" + ) + ); + addLang( LangKeyValue.ofKey("dglabgame.user_agreement.line1", ModPartEnum.NAME, "1. This mod is developed by §e§lR3944Realms §r, completely free and open source.", @@ -139,6 +148,183 @@ public enum DLGLangKey implements ILangKeyValueCollection { // 6. 开发者不对任何人身伤害或财产损失负责。 // 6. Developers are not responsible for any personal injury or property damage. + initCommandKeys(); + } + + private void initCommandKeys() { + // Server commands + addLang(LangKeyValue.ofKey("dglabgame.command.server.started", ModPartEnum.NAME, + "DGLab WebSocket server started on port %s", + "DGLab WebSocket 服务器已在端口 %s 启动", + "DGLab WebSocket 伺服器已在連接埠 %s 啟動", + "DGLab WebSocket 伺服器已於埠 %s 啟" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.server.already_running", ModPartEnum.NAME, + "DGLab server is already running", + "DGLab 服务器已在运行中", + "DGLab 伺服器已在執行中", + "DGLab 伺服器已在運作中" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.server.stopped", ModPartEnum.NAME, + "DGLab server stopped", + "DGLab 服务器已关闭", + "DGLab 伺服器已關閉", + "DGLab 伺服器已停" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.server.not_running", ModPartEnum.NAME, + "DGLab server is not running", + "DGLab 服务器未在运行", + "DGLab 伺服器未在執行", + "DGLab 伺服器未啟" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.server.not_created", ModPartEnum.NAME, + "DGLab server has not been created yet", + "DGLab 服务器尚未创建", + "DGLab 伺服器尚未創建", + "DGLab 伺服器尚未建" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.server.status", ModPartEnum.NAME, + "Server status: %s, connected apps: %s", + "服务器状态: %s, 已连接应用: %s", + "伺服器狀態: %s, 已連線應用: %s", + "伺服器態: %s, 已連應用: %s" + )); + + // Client commands + addLang(LangKeyValue.ofKey("dglabgame.command.client.created", ModPartEnum.NAME, + "DGLab client \"%s\" created (%s:%s)", + "DGLab 客户端 \"%s\" 已创建 (%s:%s)", + "DGLab 客戶端 \"%s\" 已創建 (%s:%s)", + "DGLab 客戶 \"%s\" 已建 (%s:%s)" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.client.removed", ModPartEnum.NAME, + "DGLab client \"%s\" removed", + "DGLab 客户端 \"%s\" 已移除", + "DGLab 客戶端 \"%s\" 已移除", + "DGLab 客戶 \"%s\" 已除" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.client.started", ModPartEnum.NAME, + "DGLab client \"%s\" started", + "DGLab 客户端 \"%s\" 已启动", + "DGLab 客戶端 \"%s\" 已啟動", + "DGLab 客戶 \"%s\" 已啟" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.client.stopped", ModPartEnum.NAME, + "DGLab client \"%s\" stopped", + "DGLab 客户端 \"%s\" 已关闭", + "DGLab 客戶端 \"%s\" 已關閉", + "DGLab 客戶 \"%s\" 已停" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.client.not_found", ModPartEnum.NAME, + "Client \"%s\" not found", + "未找到客户端 \"%s\"", + "未找到客戶端 \"%s\"", + "未見客戶 \"%s\"" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.client.none", ModPartEnum.NAME, + "No DGLab clients registered", + "没有已注册的 DGLab 客户端", + "沒有已註冊的 DGLab 客戶端", + "無已註之 DGLab 客戶" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.client.list", ModPartEnum.NAME, + "Registered clients: %s", + "已注册的客户端: %s", + "已註冊的客戶端: %s", + "已註客戶: %s" + )); + + // Device commands + addLang(LangKeyValue.ofKey("dglabgame.command.invalid_channel", ModPartEnum.NAME, + "Invalid channel. Use A or B.", + "无效的通道。请使用 A 或 B。", + "無效的通道。請使用 A 或 B。", + "通道有誤,當用 A 或 B。" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.invalid_policy", ModPartEnum.NAME, + "Invalid policy. Use goto/set, increase/inc, or decrease/dec.", + "无效的策略。请使用 goto/set, increase/inc 或 decrease/dec。", + "無效的策略。請使用 goto/set, increase/inc 或 decrease/dec。", + "策略有誤,當用 goto/set、increase/inc 或 decrease/dec。" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.no_output", ModPartEnum.NAME, + "No active DGLab output available. Start the server or a client first.", + "没有可用的 DGLab 输出。请先启动服务器或客户端。", + "沒有可用的 DGLab 輸出。請先啟動伺服器或客戶端。", + "無可用之 DGLab 輸出。請先啟伺服器或客戶。" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.strength.sent", ModPartEnum.NAME, + "Strength [%s %s %s] sent to %s target(s)", + "强度 [%s %s %s] 已发送至 %s 个目标", + "強度 [%s %s %s] 已傳送至 %s 個目標", + "強度 [%s %s %s] 已送至 %s 標的" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.pulse.sent", ModPartEnum.NAME, + "Pulse waveform sent to channel %s (%s target(s))", + "脉冲波形已发送至通道 %s (%s 个目标)", + "脈衝波形已傳送至通道 %s (%s 個目標)", + "脈衝波形已送至通道 %s (%s 標的)" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.clear.sent", ModPartEnum.NAME, + "Clear command sent for channel %s (%s target(s))", + "清除指令已发送至通道 %s (%s 个目标)", + "清除指令已傳送至通道 %s (%s 個目標)", + "清除之令已送至通道 %s (%s 標的)" + )); + addLang(LangKeyValue.ofKey("dglabgame.command.feedback.sent", ModPartEnum.NAME, + "Feedback value %s sent to %s target(s)", + "反馈值 %s 已发送至 %s 个目标", + "回饋值 %s 已傳送至 %s 個目標", + "回應值 %s 已送至 %s 標的" + )); + + // Management GUI + addLang(LangKeyValue.ofKey("key.dglabgame.management", ModPartEnum.NAME, + "Open Management", "打开管理", "開啟管理", "啟管理" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.tab.qrcode", ModPartEnum.NAME, + "QR Code", "二维码", "QR碼", "二維碼" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.tab.strength", ModPartEnum.NAME, + "Strength", "强度", "強度", "強度" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.tab.waveform", ModPartEnum.NAME, + "Waveform", "波形", "波形", "波形" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.tab.feedback", ModPartEnum.NAME, + "Feedback", "反馈", "回饋", "回應" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.tab.server", ModPartEnum.NAME, + "Server", "服务器", "伺服器", "伺服器" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.qrcode.copy", ModPartEnum.NAME, + "Copy QR URL to clipboard", "复制QR URL到剪贴板", "複製QR URL到剪貼板", "複QR URL" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.strength.channel_a", ModPartEnum.NAME, + "Channel A", "通道A", "通道A", "通道甲" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.strength.channel_b", ModPartEnum.NAME, + "Channel B", "通道B", "通道B", "通道乙" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.waveform.channel", ModPartEnum.NAME, + "Channel:", "通道:", "通道:", "通道:" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.feedback.send", ModPartEnum.NAME, + "Send Feedback:", "发送反馈:", "發送回饋:", "發回應:" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.server.port", ModPartEnum.NAME, + "Port:", "端口:", "連接埠:", "埠:" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.server.max_conn", ModPartEnum.NAME, + "Max:", "最大:", "最大:", "至多:" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.server.status", ModPartEnum.NAME, + "Status: %s", "状态: %s", "狀態: %s", "態: %s" + )); + addLang(LangKeyValue.ofKey("gui.dglabgame.client.label", ModPartEnum.NAME, + "Client:", "客户端:", "客戶端:", "客戶:" + )); + } @Contract(pure = true) @Override diff --git a/src/main/resources/assets/dglabgame/ui/dg_lab_user_agreement.ui b/src/main/resources/assets/dglabgame/ui/dg_lab_user_agreement.ui index 9a839201067721882aabb2056232ec0428d32f84..6dd07af11ee6554eae1952b0623724221cdee751 100644 GIT binary patch delta 623 zcmYLGy>8P$9RFW!hx0XUk+7GPp<;sp33Y&WC?-v-wyIj%1OgFKboegk6OJ9(PE)h- z0F_#u`V=fwop=ETW+vW(CkW>_AM5Y`@%#GI{Qc?rIYCDTpd0{x-n_a(Yj?k{iT!R0 zJyzZ2?`KXL$X?cpg$Q{=Cfv_t9DxBVsh`AQ_~EqIOL?Y0XpCIeX>!1&2qpjqi-uA} zV4j?w>=Cev@xFS7Uc9t;e~@Wj1gkP_E)cigTX09wgb+?g?Rja@4R)m`{o$Kl4MXR zm()M9zBTvtap4OADK`p9$N;QK+LC2~7PO{a<=RzUwcTxVyg3@FTUV(1vh$=mm3gEs zkzvZ4zD(0NK?EMe7d#1RUfztyo5f9S@@Job$!JC`zzJn?n4Y&hPH?{I>W5o< z=;=V_;Bm`Sf87nWgq+tphCvmVS;CvAW=nwofIKqZ*9*17qzq0S7a ssL7kvq+=;xyrqoENN8#R3}%)OIMn8Sbr(5xMG_AOx}na!SnaR>2L@rPumAu6 delta 78 zcmX>Vb0v6!9v8zUW(LMe1_p+ri59(#wHuF3;Non73b{>==UzSe437e*98@G~G9#nt ZW-eYAMn;>-{(Oy-nHVEC-xbQ01^`^66ukfd