commit 49d30eb728c2df5ce4608d969ae604eca2bda99c Author: lnkosadmin Date: Fri Oct 31 13:53:49 2025 +0800 最初的提交 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 0000000..30d399d Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d4081da --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + 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