├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── root.gradle.kts ├── settings.gradle.kts ├── src └── main │ ├── java │ └── xyz │ │ └── bluspring │ │ └── unitytranslate │ │ └── mixin │ │ ├── AbstractWidgetAccessor.java │ │ └── AbstractWidgetMixin.java │ ├── kotlin │ └── xyz │ │ └── bluspring │ │ └── unitytranslate │ │ ├── Language.kt │ │ ├── PlatformProxy.kt │ │ ├── PlatformProxyImpl.kt │ │ ├── UnityTranslate.kt │ │ ├── client │ │ ├── UnityTranslateClient.kt │ │ ├── gui │ │ │ ├── EditTranscriptBoxesScreen.kt │ │ │ ├── LanguageSelectScreen.kt │ │ │ ├── OpenBrowserScreen.kt │ │ │ ├── RequestDownloadScreen.kt │ │ │ ├── TranscriptBox.kt │ │ │ └── UTConfigScreen.kt │ │ └── transcribers │ │ │ ├── SpeechTranscriber.kt │ │ │ ├── TranscriberType.kt │ │ │ ├── browser │ │ │ ├── BrowserApplication.kt │ │ │ └── BrowserSpeechTranscriber.kt │ │ │ ├── sphinx │ │ │ └── SphinxSpeechTranscriber.kt │ │ │ └── windows │ │ │ └── sapi5 │ │ │ └── WindowsSpeechApiTranscriber.kt │ │ ├── commands │ │ ├── UnityTranslateClientCommands.kt │ │ └── UnityTranslateCommands.kt │ │ ├── compat │ │ ├── talkballoons │ │ │ └── TalkBalloonsCompat.kt │ │ └── voicechat │ │ │ ├── PlasmoVoiceChatClientCompat.kt │ │ │ ├── PlasmoVoiceChatCompat.kt │ │ │ ├── SimpleVoiceChatCompat.kt │ │ │ └── UTVoiceChatCompat.kt │ │ ├── config │ │ ├── DependsOn.kt │ │ ├── FloatRange.kt │ │ ├── Hidden.kt │ │ ├── IntRange.kt │ │ └── UnityTranslateConfig.kt │ │ ├── duck │ │ └── ScrollableWidget.kt │ │ ├── events │ │ └── TranscriptEvents.kt │ │ ├── fabric │ │ ├── UnityTranslateFabric.kt │ │ ├── client │ │ │ └── UnityTranslateFabricClient.kt │ │ └── compat │ │ │ └── modmenu │ │ │ └── UTModMenuIntegration.kt │ │ ├── neoforge │ │ ├── ConfigScreenHelper.kt │ │ ├── NeoForgeEvents.kt │ │ └── UnityTranslateNeoForge.kt │ │ ├── network │ │ ├── PacketIds.kt │ │ ├── UTClientNetworking.kt │ │ └── UTServerNetworking.kt │ │ ├── transcript │ │ └── Transcript.kt │ │ ├── translator │ │ ├── LibreTranslateInstance.kt │ │ ├── LocalLibreTranslateInstance.kt │ │ ├── NativeLocalLibreTranslateInstance.kt │ │ ├── Translation.kt │ │ └── TranslatorManager.kt │ │ └── util │ │ ├── ClassLoaderProviderForkJoinWorkerThreadFactory.kt │ │ ├── Flags.kt │ │ ├── HttpHelper.kt │ │ └── nativeaccess │ │ ├── CudaAccess.kt │ │ ├── CudaState.kt │ │ ├── LwjglLoader.kt │ │ └── NativeAccess.kt │ └── resources │ ├── META-INF │ ├── mods.toml │ └── neoforge.mods.toml │ ├── assets │ └── unitytranslate │ │ ├── lang │ │ ├── en_us.json │ │ ├── es_es.json │ │ └── es_mx.json │ │ └── textures │ │ └── gui │ │ ├── close.png │ │ ├── sprites │ │ ├── arrow_down.png │ │ └── arrow_up.png │ │ ├── transcriber │ │ ├── browser.png │ │ ├── sphinx.png │ │ └── windows_sapi.png │ │ └── transcription_muted.png │ ├── fabric.mod.json │ ├── icon.png │ ├── pack.mcmeta │ ├── unitytranslate.mixins.json │ └── website │ ├── package.json │ ├── speech.css │ ├── speech.js │ ├── speech_recognition.html │ └── yarn.lock └── versions ├── 1.20.6-fabric └── src │ └── main │ └── kotlin │ └── xyz │ └── bluspring │ └── unitytranslate │ └── network │ └── payloads │ ├── MarkIncompletePayload.kt │ ├── SendTranscriptToClientPayload.kt │ ├── SendTranscriptToServerPayload.kt │ ├── ServerSupportPayload.kt │ ├── SetCurrentLanguagePayload.kt │ ├── SetUsedLanguagesPayload.kt │ └── TranslateSignPayload.kt └── mainProject /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Gradle CI 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up JDK 21 15 | uses: actions/setup-java@v3 16 | with: 17 | java-version: '21' 18 | distribution: 'temurin' 19 | # Taken from https://github.com/CaffeineMC/sodium-fabric/blob/1.19.3/dev/.github/workflows/gradle.yml 20 | - name: Cache/Uncache 21 | uses: actions/cache@v2 22 | with: 23 | path: | 24 | ~/.gradle/caches 25 | ~/.gradle/loom-cache 26 | ~/.gradle/wrapper 27 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle-wrapper.properties') }} 28 | restore-keys: | 29 | ${{ runner.os }}-gradle- 30 | - name: Build artifacts 31 | run: ./gradlew remapJar 32 | - name: Upload build artifacts 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: UnityTranslate Artifacts 36 | path: build/versions/UnityTranslate-*.jar -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish JDK 21 Mods 2 | on: 3 | release: 4 | types: 5 | - 'published' 6 | 7 | env: 8 | MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} 9 | CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} 10 | 11 | jobs: 12 | build: 13 | environment: UnityTranslate Publishing 14 | name: Publish Versions 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-java@v4 19 | with: 20 | distribution: temurin 21 | java-version: 21 22 | 23 | - name: Setup Gradle 24 | uses: gradle/actions/setup-gradle@v3 25 | with: 26 | cache-read-only: true 27 | 28 | - name: Execute Gradle build 29 | run: ./gradlew publishMod -Pdgt.publish.modrinth.token=${{ env.MODRINTH_TOKEN }} -Pdgt.publish.curseforge.apikey=${{ env.CURSEFORGE_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | 120 | src/main/resources/website/node_modules/ 121 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - Fix Forge-exclusive crash -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 BluSpring 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # UnityTranslate 4 | 5 | [Modrinth](https://modrinth.com/mod/unitytranslate) · [CurseForge](https://curseforge.com/minecraft/mc-mods/unitytranslate) · [Discord](https://discord.gg/ECGejX4WFA) · [Support the Developer!](https://ko-fi.com/bluspring) 6 | 7 | A Minecraft mod designed for free live translation, inspired by the QSMP. 8 | 9 | Created primarily for the [Unity Multiplayer](https://twitter.com/UnityUpdatesEN) server, and released publicly for the multilingual Minecraft community. 10 | 11 | ### Installation 12 | Official releases of UnityTranslate can be downloaded from the official [Modrinth](https://modrinth.com/mod/unitytranslate) and [CurseForge](https://curseforge.com/minecraft/mc-mods/unitytranslate) pages.
13 | For communicating with your multilingual friends, the [Simple Voice Chat](https://modrinth.com/plugin/simple-voice-chat) mod is recommended to be installed alongside UnityTranslate. 14 | 15 | You may also require [Microsoft Edge](https://www.microsoft.com/en-us/edge/download) or [Google Chrome](https://www.google.com/chrome/) to be able to use the voice transcription (speech-to-text) system. 16 | Other browsers are not guaranteed to support the [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API). 17 | 18 | ### Credits & Acknowledgements 19 | Thank you to [Argos Open Tech](https://www.argosopentech.com/) for developing [LibreTranslate](https://libretranslate.com) & [ArgosTranslate](https://github.com/argosopentech/argos-translate).
20 | Without LibreTranslate, this project would have been made significantly more expensive, and the translation system would 21 | have been impossible to develop for free. -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Defining XML parsers 2 | systemProp.javax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl 3 | systemProp.javax.xml.transform.TransformerFactory=com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl 4 | systemProp.javax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl 5 | 6 | # Done to increase the memory available to gradle. 7 | org.gradle.jvmargs=-Xmx8192m -Xms8192m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 8 | org.gradle.parallel.threads=8 9 | org.gradle.parallel=true 10 | org.gradle.daemon=false 11 | 12 | # Loom Setup 13 | dgt.loom.mappings=mojmap 14 | dgt.loom.mappings.flavor=parchment 15 | 16 | # Mod Properties 17 | mod.version = 0.2.0-beta 18 | mod.group = xyz.bluspring 19 | mod.name = UnityTranslate 20 | mod.id = unitytranslate 21 | 22 | # Dependencies 23 | 24 | ### Common 25 | # https://maven.maxhenkel.de/#/releases/de/maxhenkel/voicechat/voicechat-api 26 | voicechat_api_version=2.5.0 27 | # https://github.com/plasmoapp/plasmo-voice/releases 28 | plasmo_api_version=2.0.3 29 | 30 | # https://modrinth.com/mod/simple-voice-chat/versions?g=1.20.1 31 | voicechat_version=2.5.16 32 | 33 | # https://modrinth.com/plugin/plasmo-voice/versions?g=1.20.1 34 | plasmo_version=2.0.10 35 | 36 | kotlin_version=2.0.20 37 | kotlin_coroutines_version=1.8.1 38 | kotlin_serialization_version=1.7.2 39 | 40 | # https://central.sonatype.com/artifact/com.squareup.okhttp3/okhttp/4.12.0/versions 41 | okhttp_version=4.12.0 42 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnityMultiplayer/UnityTranslate/d7453d8815bf21569567e935a913fa42dfad5922/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /root.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("dev.deftu.gradle.multiversion-root") 3 | } 4 | 5 | preprocess { 6 | //val forge_1_21_01 = createNode("1.21.1-forge", 1_21_01, "srg") 7 | val neoforge_1_21_01 = createNode("1.21.1-neoforge", 1_21_01, "srg") 8 | val fabric_1_21_01 = createNode("1.21.1-fabric", 1_21_01, "yarn") 9 | 10 | //val forge_1_20_06 = createNode("1.20.6-forge", 1_20_04, "srg") 11 | val neoforge_1_20_06 = createNode("1.20.6-neoforge", 1_20_06, "srg") 12 | val fabric_1_20_06 = createNode("1.20.6-fabric", 1_20_06, "yarn") 13 | 14 | //val forge_1_20_04 = createNode("1.20.4-forge", 1_20_04, "srg") 15 | val neoforge_1_20_04 = createNode("1.20.4-neoforge", 1_20_04, "srg") 16 | val fabric_1_20_04 = createNode("1.20.4-fabric", 1_20_04, "yarn") 17 | 18 | val forge_1_20_01 = createNode("1.20.1-forge", 1_20_01, "srg") 19 | val fabric_1_20_01 = createNode("1.20.1-fabric", 1_20_01, "yarn") 20 | 21 | //forge_1_21_01.link(fabric_1_21_01) 22 | neoforge_1_21_01.link(fabric_1_21_01) 23 | fabric_1_21_01.link(fabric_1_20_06) 24 | 25 | //forge_1_20_06.link(fabric_1_20_06) 26 | neoforge_1_20_06.link(fabric_1_20_06) 27 | fabric_1_20_06.link(fabric_1_20_04) 28 | 29 | //forge_1_20_04.link(fabric_1_20_04) 30 | neoforge_1_20_04.link(fabric_1_20_04) 31 | fabric_1_20_04.link(fabric_1_20_01) 32 | 33 | forge_1_20_01.link(fabric_1_20_01) 34 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven("https://maven.fabricmc.net") 4 | maven("https://maven.architectury.dev") 5 | maven("https://maven.neoforged.net/releases") 6 | maven("https://maven.firstdark.dev/releases") 7 | maven("https://maven.deftu.dev/releases") 8 | maven("https://maven.deftu.dev/snapshots") 9 | maven("https://maven.minecraftforge.net") 10 | maven("https://repo.essential.gg/repository/maven-public") 11 | maven("https://server.bbkr.space/artifactory/libs-release/") 12 | 13 | gradlePluginPortal() 14 | mavenCentral() 15 | } 16 | 17 | plugins { 18 | kotlin("jvm") version("2.0.20") 19 | kotlin("plugin.serialization") version("2.0.20") 20 | id("dev.deftu.gradle.multiversion-root") version("2.9.1+alpha.3") 21 | } 22 | } 23 | 24 | val projectName: String = extra["mod.name"]?.toString()!! 25 | rootProject.name = projectName 26 | rootProject.buildFileName = "root.gradle.kts" 27 | 28 | listOf( 29 | "1.20.1-fabric", 30 | "1.20.1-forge", 31 | 32 | "1.20.4-fabric", 33 | "1.20.4-neoforge", 34 | //"1.20.4-forge", 35 | 36 | "1.20.6-fabric", 37 | "1.20.6-neoforge", 38 | //"1.20.6-forge", 39 | 40 | "1.21.1-fabric", 41 | "1.21.1-neoforge", 42 | //"1.21.1-forge", 43 | ).forEach { version -> 44 | include(":$version") 45 | project(":$version").apply { 46 | projectDir = file("versions/$version") 47 | buildFileName = "../../build.gradle.kts" 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/xyz/bluspring/unitytranslate/mixin/AbstractWidgetAccessor.java: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.mixin; 2 | 3 | import net.minecraft.client.gui.components.AbstractWidget; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.gen.Accessor; 6 | 7 | @Mixin(AbstractWidget.class) 8 | public interface AbstractWidgetAccessor { 9 | @Accessor 10 | void setHeight(int height); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/xyz/bluspring/unitytranslate/mixin/AbstractWidgetMixin.java: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.mixin; 2 | 3 | import net.minecraft.client.gui.components.AbstractWidget; 4 | import net.minecraft.network.chat.Component; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.Shadow; 7 | import org.spongepowered.asm.mixin.Unique; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | import xyz.bluspring.unitytranslate.duck.ScrollableWidget; 12 | 13 | @Mixin(AbstractWidget.class) 14 | public class AbstractWidgetMixin implements ScrollableWidget { 15 | @Shadow private int x; 16 | @Shadow private int y; 17 | 18 | @Unique private int initialX; 19 | @Unique private int initialY; 20 | 21 | @Inject(method = "", at = @At("TAIL")) 22 | private void unityTranslate$setInitialPositions(int x, int y, int width, int height, Component message, CallbackInfo ci) { 23 | this.unityTranslate$updateInitialPosition(); 24 | } 25 | 26 | 27 | @Override 28 | public int unityTranslate$getInitialX() { 29 | return this.initialX; 30 | } 31 | 32 | @Override 33 | public int unityTranslate$getInitialY() { 34 | return this.initialY; 35 | } 36 | 37 | @Override 38 | public void unityTranslate$updateInitialPosition() { 39 | this.initialX = this.x; 40 | this.initialY = this.y; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/Language.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate 2 | 3 | //#if MC >= 1.20.6 4 | //$$ import net.minecraft.network.RegistryFriendlyByteBuf 5 | //$$ import net.minecraft.network.codec.StreamCodec 6 | //$$ import net.minecraft.network.codec.StreamDecoder 7 | //$$ import net.minecraft.network.codec.StreamEncoder 8 | //#endif 9 | import net.minecraft.network.chat.Component 10 | import xyz.bluspring.unitytranslate.client.transcribers.TranscriberType 11 | 12 | enum class Language( 13 | val code: String, 14 | val supportedTranscribers: Map 15 | ) { 16 | // Any languages that don't have translation support in LibreTranslate should not be supported here. 17 | // Use this for reference for the Browser Transcriber: https://r12a.github.io/app-subtags/ 18 | ENGLISH("en", mapOf( 19 | TranscriberType.SPHINX to "en-us", 20 | TranscriberType.BROWSER to "en-US" 21 | )), 22 | SPANISH("es", mapOf( 23 | TranscriberType.SPHINX to "es-mx", 24 | TranscriberType.BROWSER to "es-013" 25 | )), 26 | PORTUGUESE("pt", mapOf( 27 | TranscriberType.SPHINX to "br-pt", 28 | TranscriberType.BROWSER to "pt-BR" 29 | )), 30 | FRENCH("fr", mapOf( 31 | TranscriberType.SPHINX to "fr-fr", 32 | TranscriberType.BROWSER to "fr" 33 | )), 34 | SWEDISH("sv", mapOf( 35 | TranscriberType.BROWSER to "sv" 36 | )), 37 | MALAY("ms", mapOf( 38 | TranscriberType.BROWSER to "ms" 39 | )), 40 | HEBREW("he", mapOf( 41 | TranscriberType.BROWSER to "he" 42 | )), 43 | ARABIC("ar", mapOf( 44 | TranscriberType.BROWSER to "ar" 45 | )), 46 | GERMAN("de", mapOf( 47 | TranscriberType.BROWSER to "de" 48 | )), 49 | RUSSIAN("ru", mapOf( 50 | TranscriberType.BROWSER to "ru" 51 | )), 52 | JAPANESE("ja", mapOf( 53 | TranscriberType.BROWSER to "ja" 54 | )), 55 | CHINESE("zh", mapOf( 56 | TranscriberType.BROWSER to "cmn" 57 | )), 58 | ITALIAN("it", mapOf( 59 | TranscriberType.BROWSER to "it" 60 | )), 61 | CHINESE_TRADITIONAL("zt", mapOf( 62 | TranscriberType.BROWSER to "cmn" // TODO: ??? 63 | )), 64 | CZECH("cs", mapOf( 65 | TranscriberType.BROWSER to "cs" 66 | )), 67 | DANISH("da", mapOf( 68 | TranscriberType.BROWSER to "da" 69 | )), 70 | DUTCH("nl", mapOf( 71 | TranscriberType.BROWSER to "nl" 72 | )), 73 | FINNISH("fi", mapOf( 74 | TranscriberType.BROWSER to "fi" 75 | )), 76 | GREEK("el", mapOf( 77 | TranscriberType.BROWSER to "el" 78 | )), 79 | HINDI("hi", mapOf( 80 | TranscriberType.BROWSER to "hi" 81 | )), 82 | HUNGARIAN("hu", mapOf( 83 | TranscriberType.BROWSER to "hu" 84 | )), 85 | INDONESIAN("id", mapOf( 86 | TranscriberType.BROWSER to "id" 87 | )), 88 | KOREAN("ko", mapOf( 89 | TranscriberType.BROWSER to "ko" 90 | )), 91 | NORWEGIAN("nb", mapOf( 92 | TranscriberType.BROWSER to "nb" 93 | )), 94 | POLISH("pl", mapOf( 95 | TranscriberType.BROWSER to "pl" 96 | )), 97 | TAGALOG("tl", mapOf( 98 | TranscriberType.BROWSER to "tl" 99 | )), 100 | THAI("th", mapOf( 101 | TranscriberType.BROWSER to "th" 102 | )), 103 | TURKISH("tr", mapOf( 104 | TranscriberType.BROWSER to "tr" 105 | )), 106 | UKRAINIAN("uk", mapOf( 107 | TranscriberType.BROWSER to "uk" 108 | )), 109 | BULGARIAN("bg", mapOf( 110 | TranscriberType.BROWSER to "bg" 111 | )), 112 | ALBANIAN("sq", mapOf( 113 | TranscriberType.BROWSER to "sq" 114 | )), 115 | AZERBAIJANI("az", mapOf( 116 | TranscriberType.BROWSER to "az" 117 | )), 118 | BENGALI("bn", mapOf( 119 | TranscriberType.BROWSER to "bn" 120 | )), 121 | CATALAN("ca", mapOf( 122 | TranscriberType.BROWSER to "ca" 123 | )), 124 | ESPERANTO("eo", mapOf( 125 | TranscriberType.BROWSER to "eo" 126 | )), 127 | ESTONIAN("et", mapOf( 128 | TranscriberType.BROWSER to "et" 129 | )), 130 | IRISH("ga", mapOf( 131 | TranscriberType.BROWSER to "ga" 132 | )), 133 | LATVIAN("lv", mapOf( 134 | TranscriberType.BROWSER to "lv" 135 | )), 136 | LITHUANIAN("lt", mapOf( 137 | TranscriberType.BROWSER to "lt" 138 | )), 139 | PERSIAN("fa", mapOf( 140 | TranscriberType.BROWSER to "fa" 141 | )), 142 | ROMANIAN("ro", mapOf( 143 | TranscriberType.BROWSER to "ro" 144 | )), 145 | SLOVAK("sk", mapOf( 146 | TranscriberType.BROWSER to "sk" 147 | )), 148 | SLOVENIAN("sl", mapOf( 149 | TranscriberType.BROWSER to "sl" 150 | )), 151 | URDU("ur", mapOf( 152 | TranscriberType.BROWSER to "ur" 153 | )); 154 | 155 | override fun toString(): String { 156 | return "$name ($code)" 157 | } 158 | 159 | val text = Component.translatable("unitytranslate.language.$code") 160 | 161 | companion object { 162 | //#if MC >= 1.20.6 163 | //$$ val STREAM_CODEC: StreamCodec = StreamCodec.of({ buf, language -> 164 | //$$ buf.writeEnum(language) 165 | //$$ }, { buf -> 166 | //$$ buf.readEnum(Language::class.java) 167 | //$$ }) 168 | //#endif 169 | 170 | fun findLibreLang(code: String): Language? { 171 | return Language.entries.firstOrNull { it.code == code } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/PlatformProxy.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate 2 | 3 | import dev.architectury.networking.NetworkManager 4 | import io.netty.buffer.Unpooled 5 | import net.minecraft.network.FriendlyByteBuf 6 | //#if MC >= 1.20.6 7 | //$$ import net.minecraft.network.protocol.common.custom.CustomPacketPayload 8 | //#endif 9 | import net.minecraft.resources.ResourceLocation 10 | import net.minecraft.server.level.ServerPlayer 11 | import net.minecraft.world.entity.player.Player 12 | 13 | //#if FORGE 14 | //$$ import net.minecraftforge.server.permission.events.PermissionGatherEvent 15 | //#elseif NEOFORGE 16 | //$$ import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent 17 | //#endif 18 | 19 | import java.nio.file.Path 20 | 21 | interface PlatformProxy { 22 | fun isModLoaded(id: String): Boolean 23 | fun isClient(): Boolean 24 | 25 | val modVersion: String 26 | val configDir: Path 27 | val gameDir: Path 28 | val isDev: Boolean 29 | 30 | fun createByteBuf(): FriendlyByteBuf { 31 | return FriendlyByteBuf(Unpooled.buffer()) 32 | } 33 | 34 | //#if MC >= 1.20.6 35 | //$$ fun sendPacketClient(payload: CustomPacketPayload) { 36 | //$$ NetworkManager.sendToServer(payload) 37 | //#else 38 | fun sendPacketClient(id: ResourceLocation, buf: FriendlyByteBuf) { 39 | NetworkManager.sendToServer(id, buf) 40 | //#endif 41 | } 42 | 43 | //#if MC >= 1.20.6 44 | //$$ fun sendPacketServer(player: ServerPlayer, payload: CustomPacketPayload) { 45 | //$$ NetworkManager.sendToPlayer(player, payload) 46 | //#else 47 | fun sendPacketServer(player: ServerPlayer, id: ResourceLocation, buf: FriendlyByteBuf) { 48 | NetworkManager.sendToPlayer(player, id, buf) 49 | //#endif 50 | } 51 | 52 | //#if FORGE-LIKE 53 | //$$ fun registerPermissions(event: PermissionGatherEvent.Nodes) 54 | //#endif 55 | 56 | fun hasTranscriptPermission(player: Player): Boolean 57 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/PlatformProxyImpl.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate 2 | 3 | //#if FABRIC 4 | import me.lucko.fabric.api.permissions.v0.Permissions 5 | import net.fabricmc.api.EnvType 6 | import net.fabricmc.loader.api.FabricLoader 7 | //#endif 8 | import net.minecraft.world.entity.player.Player 9 | import java.nio.file.Path 10 | //#if FORGE 11 | //$$ import net.minecraftforge.api.distmarker.Dist 12 | //$$ import net.minecraftforge.fml.ModList 13 | //$$ import net.minecraftforge.fml.loading.FMLLoader 14 | //$$ import net.minecraftforge.fml.loading.FMLPaths 15 | //$$ import net.minecraftforge.server.permission.PermissionAPI 16 | //$$ import net.minecraftforge.server.permission.events.PermissionGatherEvent 17 | //$$ import net.minecraftforge.server.permission.nodes.PermissionNode 18 | //$$ import net.minecraftforge.server.permission.nodes.PermissionTypes 19 | //#elseif NEOFORGE 20 | //$$ import net.neoforged.api.distmarker.Dist 21 | //$$ import net.neoforged.fml.ModList 22 | //$$ import net.neoforged.fml.loading.FMLLoader 23 | //$$ import net.neoforged.fml.loading.FMLPaths 24 | //$$ import net.neoforged.neoforge.server.permission.PermissionAPI 25 | //$$ import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent 26 | //$$ import net.neoforged.neoforge.server.permission.nodes.PermissionNode 27 | //$$ import net.neoforged.neoforge.server.permission.nodes.PermissionTypes 28 | //#endif 29 | 30 | class PlatformProxyImpl : PlatformProxy { 31 | override val isDev: Boolean 32 | //#if FABRIC 33 | get() = FabricLoader.getInstance().isDevelopmentEnvironment 34 | //#elseif FORGE-LIKE 35 | //$$ get() = !FMLLoader.isProduction() 36 | //#endif 37 | 38 | override val gameDir: Path 39 | //#if FABRIC 40 | get() = FabricLoader.getInstance().gameDir 41 | //#elseif FORGE-LIKE 42 | //$$ get() = FMLPaths.GAMEDIR.get() 43 | //#endif 44 | 45 | override val configDir: Path 46 | //#if FABRIC 47 | get() = FabricLoader.getInstance().configDir 48 | //#elseif FORGE-LIKE 49 | //$$ get() = FMLPaths.CONFIGDIR.get() 50 | //#endif 51 | 52 | override val modVersion: String 53 | //#if FABRIC 54 | get() = FabricLoader.getInstance().getModContainer(UnityTranslate.MOD_ID).orElseThrow().metadata.version.friendlyString 55 | //#elseif FORGE-LIKE 56 | //$$ get() = ModList.get().getModFileById(UnityTranslate.MOD_ID).versionString() 57 | //#endif 58 | 59 | override fun isModLoaded(id: String): Boolean { 60 | //#if FABRIC 61 | return FabricLoader.getInstance().isModLoaded(id) 62 | //#elseif FORGE-LIKE 63 | //$$ return ModList.get().isLoaded(id) 64 | //#endif 65 | } 66 | 67 | override fun isClient(): Boolean { 68 | //#if FABRIC 69 | return FabricLoader.getInstance().environmentType == EnvType.CLIENT 70 | //#elseif FORGE-LIKE 71 | //$$ return FMLLoader.getDist() == Dist.CLIENT 72 | //#endif 73 | } 74 | 75 | // how did Forge manage to overcomplicate permissions of all things 76 | 77 | //#if FORGE-LIKE 78 | //$$ val requestTranslationsNode = PermissionNode(UnityTranslate.MOD_ID, "request_translations", PermissionTypes.BOOLEAN, { _, _, _ -> true }) 79 | //$$ override fun registerPermissions(event: PermissionGatherEvent.Nodes) { 80 | //$$ event.addNodes(requestTranslationsNode) 81 | //$$ } 82 | //#endif 83 | 84 | override fun hasTranscriptPermission(player: Player): Boolean { 85 | //#if FABRIC 86 | return Permissions.check(player, "unitytranslate.request_translations", true) 87 | //#elseif FORGE-LIKE 88 | //$$ return PermissionAPI.getOfflinePermission(player.uuid, requestTranslationsNode) 89 | //#endif 90 | } 91 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/UnityTranslate.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate 2 | 3 | import dev.architectury.event.events.common.CommandRegistrationEvent 4 | import dev.architectury.event.events.common.LifecycleEvent 5 | import kotlinx.serialization.json.Json 6 | import net.minecraft.resources.ResourceLocation 7 | import org.slf4j.LoggerFactory 8 | import xyz.bluspring.unitytranslate.commands.UnityTranslateCommands 9 | import xyz.bluspring.unitytranslate.compat.voicechat.PlasmoVoiceChatCompat 10 | import xyz.bluspring.unitytranslate.compat.voicechat.UTVoiceChatCompat 11 | import xyz.bluspring.unitytranslate.config.UnityTranslateConfig 12 | import xyz.bluspring.unitytranslate.network.UTServerNetworking 13 | import xyz.bluspring.unitytranslate.translator.LocalLibreTranslateInstance 14 | import xyz.bluspring.unitytranslate.translator.TranslatorManager 15 | import java.io.File 16 | 17 | class UnityTranslate(val proxy: PlatformProxy = PlatformProxyImpl()) { 18 | init { 19 | instance = this 20 | configFile = File(proxy.configDir.toFile(), "unitytranslate.json") 21 | version = proxy.modVersion 22 | 23 | TranslatorManager.init() 24 | loadConfig() 25 | 26 | LifecycleEvent.SERVER_STOPPING.register { 27 | LocalLibreTranslateInstance.killOpenInstances() 28 | } 29 | 30 | CommandRegistrationEvent.EVENT.register { dispatcher, _, _ -> 31 | dispatcher.register(UnityTranslateCommands.ROOT) 32 | } 33 | 34 | UTServerNetworking.init() 35 | 36 | if (proxy.isModLoaded("plasmovoice")) { 37 | PlasmoVoiceChatCompat.init() 38 | } 39 | } 40 | 41 | companion object { 42 | const val MOD_ID = "unitytranslate" 43 | 44 | lateinit var instance: UnityTranslate 45 | lateinit var configFile: File 46 | lateinit var version: String 47 | val hasVoiceChat: Boolean 48 | get() { 49 | return UTVoiceChatCompat.hasVoiceChat 50 | } 51 | 52 | var config = UnityTranslateConfig() 53 | val logger = LoggerFactory.getLogger("UnityTranslate") 54 | 55 | private val json = Json { 56 | this.ignoreUnknownKeys = true 57 | this.prettyPrint = true 58 | this.encodeDefaults = true 59 | } 60 | 61 | @JvmStatic 62 | fun id(path: String): ResourceLocation { 63 | //#if MC >= 1.21 64 | //$$ return ResourceLocation.fromNamespaceAndPath(MOD_ID, path) 65 | //#else 66 | return ResourceLocation(MOD_ID, path) 67 | //#endif 68 | } 69 | 70 | fun saveConfig() { 71 | try { 72 | if (!configFile.exists()) 73 | configFile.createNewFile() 74 | 75 | val serialized = json.encodeToString(UnityTranslateConfig.serializer(), config) 76 | configFile.writeText(serialized) 77 | } catch (e: Exception) { 78 | logger.error("Failed to save UnityTranslate config!") 79 | e.printStackTrace() 80 | } 81 | } 82 | 83 | fun loadConfig() { 84 | if (!configFile.exists()) 85 | return 86 | 87 | try { 88 | config = json.decodeFromString(UnityTranslateConfig.serializer(), configFile.readText()) 89 | } catch (e: Exception) { 90 | logger.error("Failed to load UnityTranslate config, reverting to defaults.") 91 | e.printStackTrace() 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/UnityTranslateClient.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client 2 | 3 | import com.mojang.blaze3d.platform.InputConstants 4 | import dev.architectury.event.events.client.ClientGuiEvent 5 | import dev.architectury.event.events.client.ClientLifecycleEvent 6 | import dev.architectury.event.events.client.ClientPlayerEvent 7 | import dev.architectury.event.events.client.ClientTickEvent 8 | import dev.architectury.registry.client.keymappings.KeyMappingRegistry 9 | import net.minecraft.ChatFormatting 10 | import net.minecraft.client.KeyMapping 11 | import net.minecraft.client.Minecraft 12 | import net.minecraft.client.gui.GuiGraphics 13 | import net.minecraft.network.chat.CommonComponents 14 | import net.minecraft.network.chat.Component 15 | import xyz.bluspring.unitytranslate.UnityTranslate 16 | import xyz.bluspring.unitytranslate.client.gui.* 17 | import xyz.bluspring.unitytranslate.client.transcribers.SpeechTranscriber 18 | import xyz.bluspring.unitytranslate.client.transcribers.windows.sapi5.WindowsSpeechApiTranscriber 19 | import xyz.bluspring.unitytranslate.commands.UnityTranslateClientCommands 20 | import xyz.bluspring.unitytranslate.compat.talkballoons.TalkBalloonsCompat 21 | import xyz.bluspring.unitytranslate.events.TranscriptEvents 22 | import xyz.bluspring.unitytranslate.network.PacketIds 23 | import xyz.bluspring.unitytranslate.network.UTClientNetworking 24 | import xyz.bluspring.unitytranslate.transcript.Transcript 25 | //#if MC >= 1.20.6 26 | //$$ import xyz.bluspring.unitytranslate.network.payloads.SendTranscriptToServerPayload 27 | //#endif 28 | import xyz.bluspring.unitytranslate.translator.LocalLibreTranslateInstance 29 | import xyz.bluspring.unitytranslate.translator.TranslatorManager 30 | import java.util.concurrent.ConcurrentLinkedQueue 31 | import java.util.function.BiConsumer 32 | import java.util.function.Consumer 33 | 34 | class UnityTranslateClient { 35 | init { 36 | WindowsSpeechApiTranscriber.isSupported() // runs a check to load Windows Speech API. why write the code again anyway? 37 | setupCompat() 38 | UnityTranslateClientCommands.init() 39 | 40 | transcriber = UnityTranslate.config.client.transcriber.creator.invoke(UnityTranslate.config.client.language) 41 | setupTranscriber(transcriber) 42 | 43 | ClientGuiEvent.RENDER_HUD.register { guiGraphics, delta -> 44 | if (shouldRenderBoxes && UnityTranslate.config.client.enabled) { 45 | for (languageBox in languageBoxes) { 46 | //#if MC >= 1.21 47 | //$$ languageBox.render(guiGraphics, delta.realtimeDeltaTicks) 48 | //#else 49 | languageBox.render(guiGraphics, delta) 50 | //#endif 51 | } 52 | } 53 | } 54 | 55 | ClientTickEvent.CLIENT_POST.register { mc -> 56 | if (CONFIGURE_BOXES.consumeClick()) { 57 | mc.setScreen(EditTranscriptBoxesScreen(languageBoxes)) 58 | } 59 | 60 | if (TOGGLE_TRANSCRIPTION.consumeClick()) { 61 | shouldTranscribe = !shouldTranscribe 62 | mc.player?.displayClientMessage( 63 | Component.translatable("unitytranslate.transcript") 64 | .append(": ") 65 | .append(if (shouldTranscribe) CommonComponents.OPTION_ON else CommonComponents.OPTION_OFF), true 66 | ) 67 | } 68 | 69 | if (TOGGLE_BOXES.consumeClick() && mc.screen !is EditTranscriptBoxesScreen) { 70 | shouldRenderBoxes = !shouldRenderBoxes 71 | mc.player?.displayClientMessage( 72 | Component.translatable("unitytranslate.transcript_boxes") 73 | .append(": ") 74 | .append(if (shouldRenderBoxes) CommonComponents.OPTION_ON else CommonComponents.OPTION_OFF), 75 | true 76 | ) 77 | } 78 | 79 | if (SET_SPOKEN_LANGUAGE.consumeClick() && mc.screen == null) { 80 | mc.setScreen(LanguageSelectScreen(null, false)) 81 | } 82 | 83 | if (CLEAR_TRANSCRIPTS.consumeClick()) { 84 | for (box in languageBoxes) { 85 | box.transcripts.clear() 86 | } 87 | } 88 | 89 | if (OPEN_CONFIG_GUI.consumeClick()) { 90 | mc.setScreen(UTConfigScreen(null)) 91 | } 92 | 93 | /*if (TRANSLATE_SIGN.consumeClick()) { 94 | if (mc.player != null && mc.level != null) { 95 | val hitResult = mc.player?.pick(7.5, mc.frameTime, false) 96 | 97 | if (hitResult != null && hitResult is BlockHitResult) { 98 | val buf = UnityTranslate.instance.proxy.createByteBuf() 99 | buf.writeBlockPos(hitResult.blockPos) 100 | 101 | UnityTranslate.instance.proxy.sendPacketClient(PacketIds.TRANSLATE_SIGN, buf) 102 | } 103 | } 104 | }*/ 105 | 106 | // prune transcripts 107 | val currentTime = System.currentTimeMillis() 108 | for (box in languageBoxes) { 109 | if (box.transcripts.size > 50) { 110 | for (i in 0..(box.transcripts.size - 50)) { 111 | box.transcripts.remove() 112 | } 113 | } 114 | 115 | val clientConfig = UnityTranslate.config.client 116 | 117 | if (clientConfig.disappearingText) { 118 | for (transcript in box.transcripts) { 119 | if (currentTime >= (transcript.arrivalTime + (clientConfig.disappearingTextDelay * 1000L).toLong() + (clientConfig.disappearingTextFade * 1000L).toLong())) { 120 | box.transcripts.remove(transcript) 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | ClientLifecycleEvent.CLIENT_STOPPING.register { 128 | LocalLibreTranslateInstance.killOpenInstances() 129 | } 130 | 131 | UTClientNetworking.init() 132 | } 133 | 134 | fun setupTranscriber(transcriber: SpeechTranscriber) { 135 | transcriber.updater = BiConsumer { index, text -> 136 | if (!shouldTranscribe) 137 | return@BiConsumer 138 | 139 | val updateTime = System.currentTimeMillis() 140 | 141 | if (connectedServerHasSupport) { 142 | //#if MC >= 1.20.6 143 | //$$ UnityTranslate.instance.proxy.sendPacketClient(SendTranscriptToServerPayload(transcriber.language, text, index, updateTime)) 144 | //#else 145 | val buf = UnityTranslate.instance.proxy.createByteBuf() 146 | buf.writeEnum(transcriber.language) 147 | buf.writeUtf(text) 148 | buf.writeVarInt(index) 149 | buf.writeVarLong(updateTime) 150 | 151 | UnityTranslate.instance.proxy.sendPacketClient(PacketIds.SEND_TRANSCRIPT, buf) 152 | //#endif 153 | languageBoxes.firstOrNull { it.language == transcriber.language }?.updateTranscript(Minecraft.getInstance().player!!, text, transcriber.language, index, updateTime, false) 154 | 155 | if (languageBoxes.none { it.language == transcriber.language }) { 156 | TranscriptEvents.UPDATE.invoker().onTranscriptUpdate(Transcript(index, Minecraft.getInstance().player!!, text, transcriber.language, updateTime, false), transcriber.language) 157 | } 158 | } else { 159 | if (Minecraft.getInstance().player == null) 160 | return@BiConsumer 161 | 162 | for (box in languageBoxes) { 163 | if (box.language == transcriber.language) { 164 | box.updateTranscript(Minecraft.getInstance().player!!, text, transcriber.language, index, updateTime, false) 165 | 166 | continue 167 | } 168 | 169 | TranslatorManager.queueTranslation(text, transcriber.language, box.language, Minecraft.getInstance().player!!, index) 170 | .whenCompleteAsync { it, e -> 171 | if (e != null) 172 | return@whenCompleteAsync 173 | 174 | box.updateTranscript(Minecraft.getInstance().player!!, it, transcriber.language, index, updateTime, false) 175 | } 176 | } 177 | 178 | if (languageBoxes.none { it.language == transcriber.language }) { 179 | TranscriptEvents.UPDATE.invoker().onTranscriptUpdate(Transcript(index, Minecraft.getInstance().player!!, text, transcriber.language, updateTime, false), transcriber.language) 180 | } 181 | } 182 | } 183 | } 184 | 185 | private fun setupCompat() { 186 | if (isTalkBalloonsInstalled) { 187 | TalkBalloonsCompat.init() 188 | } 189 | } 190 | 191 | companion object { 192 | lateinit var transcriber: SpeechTranscriber 193 | 194 | var connectedServerHasSupport = false 195 | 196 | var shouldTranscribe = true 197 | set(value) { 198 | field = value 199 | transcriber.setMuted(!value) 200 | } 201 | 202 | var shouldRenderBoxes = true 203 | 204 | val languageBoxes: MutableList 205 | get() { 206 | return UnityTranslate.config.client.transcriptBoxes 207 | } 208 | 209 | val CONFIGURE_BOXES = (KeyMapping("unitytranslate.configure_boxes", -1, "UnityTranslate")) 210 | val TOGGLE_TRANSCRIPTION = (KeyMapping("unitytranslate.toggle_transcription", -1, "UnityTranslate")) 211 | val TOGGLE_BOXES = (KeyMapping("unitytranslate.toggle_boxes", -1, "UnityTranslate")) 212 | val SET_SPOKEN_LANGUAGE = (KeyMapping("unitytranslate.set_spoken_language", -1, "UnityTranslate")) 213 | val CLEAR_TRANSCRIPTS = (KeyMapping("unitytranslate.clear_transcripts", -1, "UnityTranslate")) 214 | //val TRANSLATE_SIGN = (KeyMapping("unitytranslate.translate_sign", InputConstants.KEY_F8, "UnityTranslate")) 215 | val OPEN_CONFIG_GUI = (KeyMapping("unitytranslate.open_config", InputConstants.KEY_F7, "UnityTranslate")) 216 | 217 | @JvmStatic 218 | fun registerKeys() { 219 | KeyMappingRegistry.register(CONFIGURE_BOXES) 220 | KeyMappingRegistry.register(TOGGLE_TRANSCRIPTION) 221 | KeyMappingRegistry.register(TOGGLE_BOXES) 222 | KeyMappingRegistry.register(SET_SPOKEN_LANGUAGE) 223 | KeyMappingRegistry.register(CLEAR_TRANSCRIPTS) 224 | //KeyMappingRegistry.register(TRANSLATE_SIGN) 225 | KeyMappingRegistry.register(OPEN_CONFIG_GUI) 226 | } 227 | 228 | val isTalkBalloonsInstalled = UnityTranslate.instance.proxy.isModLoaded("talk_balloons") 229 | 230 | fun displayMessage(component: Component, isError: Boolean = false) { 231 | val full = Component.empty() 232 | .append(Component.literal("[UnityTranslate]: ") 233 | .withStyle(if (isError) ChatFormatting.RED else ChatFormatting.YELLOW, ChatFormatting.BOLD) 234 | ) 235 | .append(component) 236 | 237 | Minecraft.getInstance().gui.chat.addMessage(full) 238 | } 239 | 240 | fun renderCreditText(guiGraphics: GuiGraphics) { 241 | val version = UnityTranslate.instance.proxy.modVersion 242 | val font = Minecraft.getInstance().font 243 | 244 | guiGraphics.drawString(font, "UnityTranslate v$version", 2, Minecraft.getInstance().window.guiScaledHeight - (font.lineHeight * 2) - 4, 0xAAAAAA) 245 | guiGraphics.drawString(font, Component.translatable("unitytranslate.credit.author"), 2, Minecraft.getInstance().window.guiScaledHeight - font.lineHeight - 2, 0xAAAAAA) 246 | } 247 | 248 | private val queuedForJoin = ConcurrentLinkedQueue>() 249 | 250 | init { 251 | ClientPlayerEvent.CLIENT_PLAYER_JOIN.register { _ -> 252 | for (consumer in queuedForJoin) { 253 | consumer.accept(Minecraft.getInstance()) 254 | } 255 | queuedForJoin.clear() 256 | } 257 | } 258 | 259 | fun openDownloadRequest() { 260 | queuedForJoin.add { mc -> 261 | if (mc.screen is OpenBrowserScreen) { 262 | mc.execute { 263 | mc.setScreen(RequestDownloadScreen().apply { 264 | parent = mc.screen 265 | }) 266 | } 267 | } else { 268 | mc.execute { 269 | mc.setScreen(RequestDownloadScreen()) 270 | } 271 | } 272 | } 273 | } 274 | } 275 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/gui/LanguageSelectScreen.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client.gui 2 | 3 | import net.minecraft.Util 4 | import net.minecraft.client.Minecraft 5 | import net.minecraft.client.gui.GuiGraphics 6 | import net.minecraft.client.gui.components.Button 7 | import net.minecraft.client.gui.components.ObjectSelectionList 8 | import net.minecraft.client.gui.screens.Screen 9 | import net.minecraft.network.chat.CommonComponents 10 | import net.minecraft.network.chat.Component 11 | import net.minecraft.util.FormattedCharSequence 12 | import xyz.bluspring.unitytranslate.Language 13 | import xyz.bluspring.unitytranslate.UnityTranslate 14 | import xyz.bluspring.unitytranslate.client.UnityTranslateClient 15 | 16 | class LanguageSelectScreen(val parent: Screen?, val isAddingBox: Boolean) : Screen(Component.translatable("options.language")) { 17 | private lateinit var list: LanguageSelectionList 18 | 19 | override fun init() { 20 | super.init() 21 | 22 | list = LanguageSelectionList() 23 | this.addRenderableWidget(list) 24 | this.addRenderableWidget( 25 | Button.builder(CommonComponents.GUI_DONE) { 26 | this.onDone() 27 | } 28 | .bounds(this.width / 2 - (Button.DEFAULT_WIDTH / 2), this.height - 38, Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT) 29 | .build() 30 | ) 31 | } 32 | 33 | override fun onClose() { 34 | Minecraft.getInstance().setScreen(parent) 35 | } 36 | 37 | override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { 38 | //#if MC >= 1.20.4 39 | //$$ this.renderBackground(guiGraphics, mouseX, mouseY, partialTick) 40 | //#else 41 | this.renderBackground(guiGraphics) 42 | //#endif 43 | super.render(guiGraphics, mouseX, mouseY, partialTick) 44 | 45 | guiGraphics.drawCenteredString(font, Component.translatable( 46 | if (isAddingBox) 47 | "unitytranslate.select_language" 48 | else 49 | "unitytranslate.set_spoken_language" 50 | ), this.width / 2, 15, 16777215) 51 | 52 | UnityTranslateClient.renderCreditText(guiGraphics) 53 | } 54 | 55 | private fun onDone() { 56 | val language = list.selected?.language 57 | 58 | if (language == null) { 59 | onClose() 60 | return 61 | } 62 | 63 | if (isAddingBox) { 64 | if (list.selected?.shouldBeDeactivated == true) { 65 | onClose() 66 | return 67 | } 68 | 69 | Minecraft.getInstance().execute { 70 | UnityTranslate.config.client.transcriptBoxes.add(TranscriptBox(0, 0, 150, 170, 120, language)) 71 | UnityTranslate.saveConfig() 72 | } 73 | } else { 74 | UnityTranslate.config.client.language = language 75 | UnityTranslateClient.transcriber.changeLanguage(language) 76 | UnityTranslate.saveConfig() 77 | } 78 | 79 | onClose() 80 | } 81 | 82 | private inner class LanguageSelectionList : ObjectSelectionList(Minecraft.getInstance(), 83 | this@LanguageSelectScreen.width, this@LanguageSelectScreen.height 84 | //#if MC >= 1.20.4 85 | //$$ - 75 86 | //#endif 87 | , 32, 88 | //#if MC <= 1.20.1 89 | this@LanguageSelectScreen.height - 65 + 4, 90 | //#endif 91 | 18 92 | ) { 93 | init { 94 | for (language in Language.entries.sortedBy { it.name }) { 95 | val entry = Entry(language) 96 | this.addEntry(entry) 97 | 98 | if (!isAddingBox && UnityTranslateClient.transcriber.language == language) { 99 | this.selected = entry 100 | } 101 | } 102 | } 103 | 104 | inner class Entry(val language: Language) : ObjectSelectionList.Entry() { 105 | internal val shouldBeDeactivated = isAddingBox && UnityTranslate.config.client.transcriptBoxes.any { it.language == language } 106 | private var lastClickTime: Long = 0L 107 | 108 | override fun render( 109 | guiGraphics: GuiGraphics, 110 | index: Int, top: Int, left: Int, 111 | width: Int, height: Int, 112 | mouseX: Int, mouseY: Int, 113 | hovering: Boolean, partialTick: Float 114 | ) { 115 | val color = if (shouldBeDeactivated) { 116 | 0x656565 117 | } else 0xFFFFFF 118 | 119 | guiGraphics.drawCenteredString(font, language.text, this@LanguageSelectScreen.width / 2, top + 1, color) 120 | 121 | if (!isAddingBox) { 122 | var x = this@LanguageSelectScreen.width / 2 + (font.width(language.text) / 2) + 4 123 | for (type in language.supportedTranscribers.keys) { 124 | if (!type.enabled) 125 | continue 126 | 127 | guiGraphics.blit(UnityTranslate.id("textures/gui/transcriber/${type.name.lowercase()}.png"), 128 | x, top - 1, 0f, 0f, 16, 16, 16, 16 129 | ) 130 | 131 | if (mouseX >= x && mouseX <= x + 16 && mouseY >= top - 1 && mouseY <= top - 1 + 16) { 132 | val lines = mutableListOf() 133 | 134 | lines.add(Component.translatable("unitytranslate.transcriber.type.${type.name.lowercase()}").visualOrderText) 135 | lines.add(Component.empty().visualOrderText) 136 | lines.addAll(font.split(Component.translatable("unitytranslate.transcriber.type.${type.name.lowercase()}.description"), (this@LanguageSelectScreen.width / 6).coerceAtLeast(150))) 137 | 138 | guiGraphics.renderTooltip(font, lines, mouseX, mouseY) 139 | } 140 | 141 | x += 20 142 | } 143 | } else if (shouldBeDeactivated) { 144 | val textWidth = font.width(language.text) 145 | val halfTextWidth = textWidth / 2 146 | val centerX = this@LanguageSelectScreen.width / 2 147 | 148 | if (mouseX >= centerX - halfTextWidth && mouseX <= centerX + halfTextWidth && mouseY >= top + 1 && mouseY <= top + 1 + font.lineHeight) { 149 | guiGraphics.renderTooltip(font, listOf( 150 | Component.translatable("unitytranslate.select_language.already_selected").visualOrderText 151 | ), mouseX, mouseY) 152 | } 153 | } 154 | } 155 | 156 | override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { 157 | if (shouldBeDeactivated) 158 | return false 159 | 160 | if (button == 0) { 161 | this@LanguageSelectionList.selected = this 162 | if (Util.getMillis() - this.lastClickTime < 250L) { 163 | this@LanguageSelectScreen.onDone() 164 | } 165 | 166 | this.lastClickTime = Util.getMillis() 167 | return true 168 | } else { 169 | this.lastClickTime = Util.getMillis() 170 | return false 171 | } 172 | } 173 | 174 | override fun getNarration(): Component { 175 | return Component.translatable("narrator.select", this.language.text) 176 | } 177 | } 178 | } 179 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/gui/OpenBrowserScreen.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client.gui 2 | 3 | import net.minecraft.Util 4 | import net.minecraft.client.gui.GuiGraphics 5 | import net.minecraft.client.gui.components.Button 6 | import net.minecraft.client.gui.components.Tooltip 7 | import net.minecraft.client.gui.screens.Screen 8 | import net.minecraft.client.resources.language.I18n 9 | import net.minecraft.network.chat.Component 10 | import net.minecraft.network.chat.FormattedText 11 | import xyz.bluspring.unitytranslate.UnityTranslate 12 | import xyz.bluspring.unitytranslate.client.UnityTranslateClient 13 | import xyz.bluspring.unitytranslate.config.UnityTranslateConfig 14 | 15 | class OpenBrowserScreen(val address: String) : Screen(Component.empty()) { 16 | override fun init() { 17 | super.init() 18 | 19 | addRenderableWidget( 20 | Button.builder(Component.translatable("unitytranslate.do_not_show_again")) { 21 | UnityTranslate.config.client.openBrowserWithoutPromptV2 = UnityTranslateConfig.TriState.FALSE 22 | UnityTranslate.saveConfig() 23 | 24 | UnityTranslateClient.displayMessage(Component.translatable("unitytranslate.disabled_browser_transcription", Component.keybind("unitytranslate.open_config"))) 25 | 26 | this.onClose() 27 | } 28 | .pos(this.width / 2 - (Button.DEFAULT_WIDTH / 2), this.height - 20 - Button.DEFAULT_HEIGHT - 5 - Button.DEFAULT_HEIGHT - 15) 29 | .tooltip(Tooltip.create(Component.translatable("unitytranslate.do_not_show_again.desc"))) 30 | .build() 31 | ) 32 | 33 | addRenderableWidget( 34 | Button.builder(Component.translatable("gui.copy_link_to_clipboard")) { 35 | this.minecraft!!.keyboardHandler.clipboard = address 36 | this.onClose() 37 | } 38 | .pos(this.width / 2 - (Button.DEFAULT_WIDTH / 2), this.height - 20 - Button.DEFAULT_HEIGHT - 5 - Button.DEFAULT_HEIGHT - 5 - Button.DEFAULT_HEIGHT - 15) 39 | .build() 40 | ) 41 | 42 | addRenderableWidget( 43 | Button.builder(Component.translatable("unitytranslate.open_browser.open_in_browser")) { 44 | UnityTranslate.config.client.openBrowserWithoutPromptV2 = UnityTranslateConfig.TriState.TRUE 45 | UnityTranslate.saveConfig() 46 | Util.getPlatform().openUri(address) 47 | this.onClose() 48 | } 49 | .pos(this.width / 2 - (Button.DEFAULT_WIDTH / 2), this.height - 20 - Button.DEFAULT_HEIGHT - 15) 50 | .tooltip(Tooltip.create(Component.translatable("unitytranslate.open_browser.open_in_browser.desc"))) 51 | .build() 52 | ) 53 | } 54 | 55 | override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { 56 | //#if MC >= 1.20.4 57 | //$$ this.renderBackground(guiGraphics, mouseX, mouseY, partialTick) 58 | //#else 59 | this.renderBackground(guiGraphics) 60 | //#endif 61 | super.render(guiGraphics, mouseX, mouseY, partialTick) 62 | 63 | val split = this.font.split(FormattedText.of(I18n.get("unitytranslate.open_browser.prompt")), this.width / 2) 64 | 65 | val start = (this.height / 2 - (10 * split.size)) 66 | for ((index, text) in split.withIndex()) { 67 | guiGraphics.drawCenteredString(this.font, text, this.width / 2, start + (this.font.lineHeight * index), 16777215) 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/gui/RequestDownloadScreen.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client.gui 2 | 3 | import net.minecraft.client.Minecraft 4 | import net.minecraft.client.gui.GuiGraphics 5 | import net.minecraft.client.gui.components.Button 6 | import net.minecraft.client.gui.components.Tooltip 7 | import net.minecraft.client.gui.screens.Screen 8 | import net.minecraft.network.chat.Component 9 | import xyz.bluspring.unitytranslate.UnityTranslate 10 | import xyz.bluspring.unitytranslate.translator.TranslatorManager 11 | 12 | class RequestDownloadScreen : Screen(Component.empty()) { 13 | var parent: Screen? = null 14 | 15 | override fun init() { 16 | super.init() 17 | 18 | this.addRenderableWidget(Button.builder(Component.translatable("unitytranslate.request_download.allow")) { 19 | TranslatorManager.installLibreTranslate() 20 | Minecraft.getInstance().setScreen(parent) 21 | } 22 | .pos(this.width / 2 - Button.SMALL_WIDTH - 20, this.height - 35) 23 | .width(Button.SMALL_WIDTH) 24 | .build()) 25 | 26 | this.addRenderableWidget(Button.builder(Component.translatable("unitytranslate.request_download.deny")) { 27 | Minecraft.getInstance().setScreen(parent) 28 | UnityTranslate.config.server.shouldRunTranslationServer = false 29 | UnityTranslate.saveConfig() 30 | } 31 | .pos(this.width / 2 + 20, this.height - 35) 32 | .width(Button.SMALL_WIDTH) 33 | .tooltip(Tooltip.create(Component.translatable("unitytranslate.request_download.deny.desc"))) 34 | .build()) 35 | } 36 | 37 | override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { 38 | //#if MC >= 1.20.4 39 | //$$ this.renderBackground(guiGraphics, mouseX, mouseY, partialTick) 40 | //#else 41 | this.renderBackground(guiGraphics) 42 | //#endif 43 | super.render(guiGraphics, mouseX, mouseY, partialTick) 44 | 45 | val lines = font.split(Component.translatable("unitytranslate.request_download"), this.width - 50) 46 | for ((index, line) in lines.withIndex()) { 47 | guiGraphics.drawCenteredString(font, line, this.width / 2, (this.height / 2 - (lines.size * (font.lineHeight + 2))).coerceAtLeast(13) + (index * (font.lineHeight + 2)), 16777215) 48 | } 49 | } 50 | 51 | override fun onClose() { 52 | super.onClose() 53 | 54 | Minecraft.getInstance().setScreen(parent) 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/gui/TranscriptBox.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client.gui 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.Transient 5 | import net.minecraft.ChatFormatting 6 | import net.minecraft.client.Minecraft 7 | import net.minecraft.client.gui.GuiGraphics 8 | import net.minecraft.network.chat.Component 9 | import net.minecraft.util.FastColor 10 | import net.minecraft.util.Mth 11 | import net.minecraft.world.entity.player.Player 12 | import xyz.bluspring.unitytranslate.Language 13 | import xyz.bluspring.unitytranslate.UnityTranslate 14 | import xyz.bluspring.unitytranslate.client.UnityTranslateClient 15 | import xyz.bluspring.unitytranslate.compat.voicechat.UTVoiceChatCompat 16 | import xyz.bluspring.unitytranslate.events.TranscriptEvents 17 | import xyz.bluspring.unitytranslate.transcript.Transcript 18 | import java.util.* 19 | import java.util.concurrent.ConcurrentLinkedQueue 20 | 21 | @Serializable 22 | data class TranscriptBox( 23 | var offsetX: Int, 24 | var offsetY: Int, 25 | var width: Int, 26 | var height: Int, 27 | var opacity: Int, 28 | 29 | var language: Language, 30 | 31 | var offsetXEdge: Boolean = false, 32 | var offsetYEdge: Boolean = false 33 | ) { 34 | var x: Int 35 | get() { 36 | return if (offsetXEdge) 37 | Minecraft.getInstance().window.guiScaledWidth - offsetX 38 | else 39 | offsetX 40 | } 41 | set(value) { 42 | if (value > Minecraft.getInstance().window.guiScaledWidth / 2 - (width / 2)) { 43 | offsetXEdge = true 44 | offsetX = Minecraft.getInstance().window.guiScaledWidth - value 45 | } else { 46 | offsetXEdge = false 47 | offsetX = value 48 | } 49 | } 50 | 51 | var y: Int 52 | get() { 53 | return if (offsetYEdge) 54 | Minecraft.getInstance().window.guiScaledHeight - offsetY 55 | else 56 | offsetY 57 | } 58 | set(value) { 59 | if (value > Minecraft.getInstance().window.guiScaledHeight / 2 - (height / 2)) { 60 | offsetYEdge = true 61 | offsetY = Minecraft.getInstance().window.guiScaledHeight - value 62 | } else { 63 | offsetYEdge = false 64 | offsetY = value 65 | } 66 | } 67 | 68 | @Transient 69 | val transcripts = ConcurrentLinkedQueue() 70 | 71 | fun render(guiGraphics: GuiGraphics, partialTick: Float) { 72 | guiGraphics.pose().pushPose() 73 | 74 | guiGraphics.pose().translate(0.0, 0.0, -255.0) 75 | guiGraphics.enableScissor(x, y, x + width, y + height) 76 | 77 | guiGraphics.fill(x, y, x + width, y + height, FastColor.ARGB32.color(opacity, 0, 0, 0)) 78 | guiGraphics.drawCenteredString(Minecraft.getInstance().font, Component.translatable("unitytranslate.transcript").append(" (${language.code.uppercase()})") 79 | .withStyle(ChatFormatting.UNDERLINE, ChatFormatting.BOLD), x + (width / 2), y + 5, 16777215) 80 | 81 | if (!UnityTranslateClient.shouldTranscribe) { 82 | guiGraphics.blit(TRANSCRIPT_MUTED, x + width - 20, y + 2, 0f, 0f, 16, 16, 16, 16) 83 | } 84 | 85 | guiGraphics.enableScissor(x, y + 15, x + width, y + height) 86 | 87 | val lines = transcripts.sortedByDescending { it.arrivalTime } 88 | 89 | val font = Minecraft.getInstance().font 90 | val scale = UnityTranslate.config.client.textScale / 100f 91 | val invScale = if (scale == 0f) 0f else 1f / scale 92 | 93 | var currentY = y + height - font.lineHeight 94 | 95 | for (transcript in lines) { 96 | val component = Component.empty() 97 | .append("<") 98 | .append(transcript.player.displayName) 99 | .append( 100 | Component.literal(" (${transcript.language.code.uppercase(Locale.ENGLISH)})") 101 | .withStyle(ChatFormatting.GREEN) 102 | ) 103 | .append("> ") 104 | .append( 105 | Component.literal(transcript.text) 106 | .apply { 107 | if (transcript.incomplete) { 108 | this.withStyle(ChatFormatting.GRAY, ChatFormatting.ITALIC) 109 | } 110 | } 111 | ) 112 | 113 | val currentTime = System.currentTimeMillis() 114 | val delay = (UnityTranslate.config.client.disappearingTextDelay * 1000L).toLong() 115 | if (UnityTranslate.config.client.disappearingText && currentTime >= transcript.arrivalTime + delay) { 116 | val fadeTime = (UnityTranslate.config.client.disappearingTextFade * 1000L).toLong() 117 | 118 | val fadeStart = transcript.arrivalTime + delay 119 | val fadeEnd = fadeStart + fadeTime 120 | val fadeAmount = ((fadeEnd - currentTime).toFloat() / fadeTime.toFloat()) 121 | 122 | val alpha = Mth.clamp(fadeAmount, 0f, 1f) 123 | guiGraphics.setColor(1f, 1f, 1f, alpha) 124 | } 125 | 126 | val split = font.split(component, ((width - 5) * invScale).toInt()).reversed() 127 | 128 | for (line in split) { 129 | guiGraphics.pose().pushPose() 130 | guiGraphics.pose().translate(x.toFloat(), currentY.toFloat(), 0f) 131 | guiGraphics.pose().scale(scale, scale, scale) 132 | guiGraphics.drawString(font, line, 4, 0, 16777215) 133 | currentY -= (font.lineHeight * scale).toInt() 134 | guiGraphics.pose().popPose() 135 | } 136 | 137 | guiGraphics.setColor(1f, 1f, 1f, 1f) 138 | 139 | currentY -= 4 140 | } 141 | 142 | guiGraphics.disableScissor() 143 | guiGraphics.disableScissor() 144 | 145 | guiGraphics.renderOutline(x, y, width, height, FastColor.ARGB32.color(100, 0, 0, 0)) 146 | 147 | guiGraphics.pose().popPose() 148 | } 149 | 150 | fun updateTranscript(source: Player, text: String, language: Language, index: Int, updateTime: Long, incomplete: Boolean) { 151 | if (!UTVoiceChatCompat.isPlayerAudible(source)) 152 | return 153 | 154 | if (this.transcripts.any { it.player.uuid == source.uuid && it.index == index }) { 155 | val transcript = this.transcripts.first { it.player.uuid == source.uuid && it.index == index } 156 | 157 | // it's possible for this to go out of order, let's avoid that 158 | if (transcript.lastUpdateTime > updateTime) 159 | return 160 | 161 | transcript.lastUpdateTime = updateTime 162 | transcript.text = text 163 | transcript.incomplete = incomplete 164 | transcript.arrivalTime = System.currentTimeMillis() 165 | 166 | TranscriptEvents.UPDATE.invoker().onTranscriptUpdate(transcript, this@TranscriptBox.language) 167 | 168 | return 169 | } 170 | 171 | this.transcripts.add(Transcript(index, source, text, language, updateTime, incomplete).apply { 172 | TranscriptEvents.UPDATE.invoker().onTranscriptUpdate(this, this@TranscriptBox.language) 173 | }) 174 | } 175 | 176 | companion object { 177 | val TRANSCRIPT_MUTED = UnityTranslate.id("textures/gui/transcription_muted.png") 178 | } 179 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/transcribers/SpeechTranscriber.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client.transcribers 2 | 3 | import net.minecraft.client.Minecraft 4 | import xyz.bluspring.unitytranslate.Language 5 | import xyz.bluspring.unitytranslate.UnityTranslate 6 | import xyz.bluspring.unitytranslate.network.PacketIds 7 | //#if MC >= 1.20.6 8 | //$$ import xyz.bluspring.unitytranslate.network.payloads.SetCurrentLanguagePayload 9 | //#endif 10 | import java.util.function.BiConsumer 11 | 12 | abstract class SpeechTranscriber(var language: Language) { 13 | var lastIndex = 0 14 | var currentOffset = 0 15 | 16 | lateinit var updater: BiConsumer 17 | 18 | abstract fun stop() 19 | open fun setMuted(muted: Boolean) {} 20 | 21 | open fun changeLanguage(language: Language) { 22 | this.language = language 23 | 24 | if (Minecraft.getInstance().player != null) { 25 | //#if MC >= 1.20.6 26 | //$$ UnityTranslate.instance.proxy.sendPacketClient(SetCurrentLanguagePayload(language)) 27 | //#else 28 | val buf = UnityTranslate.instance.proxy.createByteBuf() 29 | buf.writeEnum(language) 30 | 31 | UnityTranslate.instance.proxy.sendPacketClient(PacketIds.SET_CURRENT_LANGUAGE, buf) 32 | //#endif 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/transcribers/TranscriberType.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client.transcribers 2 | 3 | import xyz.bluspring.unitytranslate.Language 4 | import xyz.bluspring.unitytranslate.client.transcribers.browser.BrowserSpeechTranscriber 5 | import xyz.bluspring.unitytranslate.client.transcribers.sphinx.SphinxSpeechTranscriber 6 | import xyz.bluspring.unitytranslate.client.transcribers.windows.sapi5.WindowsSpeechApiTranscriber 7 | 8 | enum class TranscriberType(val creator: (Language) -> SpeechTranscriber, val enabled: Boolean = true) { 9 | SPHINX(::SphinxSpeechTranscriber, false), 10 | BROWSER(::BrowserSpeechTranscriber), 11 | WINDOWS_SAPI(::WindowsSpeechApiTranscriber, WindowsSpeechApiTranscriber.isSupported()) 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/transcribers/browser/BrowserApplication.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client.transcribers.browser 2 | 3 | import com.sun.net.httpserver.HttpServer 4 | import net.minecraft.client.resources.language.I18n 5 | import xyz.bluspring.unitytranslate.UnityTranslate 6 | 7 | object BrowserApplication { 8 | var socketPort: Int = 0 9 | 10 | val translationMap = mapOf( 11 | "TRANSCRIPT" to "unitytranslate.transcript", 12 | "INFO" to "unitytranslate.transcriber.browser.info", 13 | "TITLE" to "unitytranslate.transcriber.browser.title", 14 | "PAUSED" to "unitytranslate.transcriber.browser.paused", 15 | "PAUSED_DESC" to "unitytranslate.transcriber.browser.paused.desc", 16 | ) 17 | 18 | private fun applyTranslations(text: String): String { 19 | var current = text 20 | 21 | for ((id, key) in translationMap) { 22 | val translated = I18n.get(key) 23 | 24 | // Protection against translation keys 25 | if (translated.contains('<') || translated.contains('>')) { 26 | UnityTranslate.logger.error("UnityTranslate has detected HTML tag characters in translation key $key! To protect your system, UnityTranslate has forcefully crashed the transcriber (and potentially the game) to avoid any malicious actors.") 27 | UnityTranslate.logger.error("This may have occurred for one of the following reasons:") 28 | UnityTranslate.logger.error(" - A server resource pack added a malicious translation for $key") 29 | UnityTranslate.logger.error(" - Your currently applied resource pack contains a malicious translation for $key") 30 | UnityTranslate.logger.error(" - A world you have joined has a resource pack containing a malicious translation for $key") 31 | UnityTranslate.logger.error("") 32 | UnityTranslate.logger.error("Translated line: \"$translated\"") 33 | 34 | throw IllegalStateException("HTML tags detected in $key!") 35 | } 36 | 37 | current = current.replace("%I18N_$id%", translated) 38 | .replace("\$GITHUB$", "GitHub") 39 | .replace("\$BR$", "
") 40 | } 41 | 42 | return current 43 | } 44 | 45 | fun addHandler(server: HttpServer) { 46 | server.createContext("/") { ctx -> 47 | when (ctx.requestURI.path) { 48 | "/" -> { 49 | ctx.responseHeaders.set("Content-Type", "text/html; charset=utf-8") 50 | 51 | val byteArray = applyTranslations(this::class.java.getResource("/website/speech_recognition.html")!!.readText(Charsets.UTF_8)).toByteArray(Charsets.UTF_8) 52 | ctx.sendResponseHeaders(200, byteArray.size.toLong()) 53 | ctx.responseBody.write(byteArray) 54 | ctx.responseBody.close() 55 | } 56 | 57 | "/index.js" -> { 58 | ctx.responseHeaders.set("Content-Type", "text/javascript; charset=utf-8") 59 | 60 | val byteArray = applyTranslations(this::class.java.getResource("/website/speech.js")!!.readText(Charsets.UTF_8)) 61 | .replace("%SOCKET_PORT%", socketPort.toString()) 62 | .toByteArray(Charsets.UTF_8) 63 | ctx.sendResponseHeaders(200, byteArray.size.toLong()) 64 | ctx.responseBody.write(byteArray) 65 | ctx.responseBody.close() 66 | } 67 | 68 | "/speech.css" -> { 69 | ctx.responseHeaders.set("Content-Type", "text/css; charset=utf-8") 70 | 71 | val byteArray = applyTranslations(this::class.java.getResource("/website/speech.css")!!.readText(Charsets.UTF_8)) 72 | .toByteArray(Charsets.UTF_8) 73 | ctx.sendResponseHeaders(200, byteArray.size.toLong()) 74 | ctx.responseBody.write(byteArray) 75 | ctx.responseBody.close() 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/transcribers/browser/BrowserSpeechTranscriber.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client.transcribers.browser 2 | 3 | import com.google.gson.JsonObject 4 | import com.google.gson.JsonParser 5 | import com.sun.net.httpserver.HttpServer 6 | import dev.architectury.event.events.client.ClientPlayerEvent 7 | import net.minecraft.Util 8 | import net.minecraft.client.Minecraft 9 | import net.minecraft.network.chat.ClickEvent 10 | import net.minecraft.network.chat.Component 11 | import net.minecraft.util.HttpUtil 12 | import org.java_websocket.WebSocket 13 | import org.java_websocket.handshake.ClientHandshake 14 | import org.java_websocket.server.WebSocketServer 15 | import xyz.bluspring.unitytranslate.Language 16 | import xyz.bluspring.unitytranslate.UnityTranslate 17 | import xyz.bluspring.unitytranslate.client.UnityTranslateClient 18 | import xyz.bluspring.unitytranslate.client.gui.OpenBrowserScreen 19 | import xyz.bluspring.unitytranslate.client.gui.RequestDownloadScreen 20 | import xyz.bluspring.unitytranslate.client.transcribers.SpeechTranscriber 21 | import xyz.bluspring.unitytranslate.client.transcribers.TranscriberType 22 | import xyz.bluspring.unitytranslate.config.UnityTranslateConfig 23 | import java.net.InetSocketAddress 24 | 25 | class BrowserSpeechTranscriber(language: Language) : SpeechTranscriber(language) { 26 | val socketPort = HttpUtil.getAvailablePort() 27 | val server: HttpServer 28 | val socket = BrowserSocket() 29 | val serverPort = if (!HttpUtil.isPortAvailable(25117)) 30 | HttpUtil.getAvailablePort() 31 | else 32 | 25117 33 | 34 | init { 35 | BrowserApplication.socketPort = socketPort 36 | server = HttpServer.create(InetSocketAddress("0.0.0.0", serverPort), 0) 37 | BrowserApplication.addHandler(server) 38 | 39 | server.start() 40 | 41 | socket.isDaemon = true 42 | socket.start() 43 | 44 | ClientPlayerEvent.CLIENT_PLAYER_JOIN.register { _ -> 45 | openWebsite() 46 | } 47 | } 48 | 49 | fun openWebsite() { 50 | val mc = Minecraft.getInstance() 51 | 52 | if (socket.totalConnections <= 0 && UnityTranslate.config.client.enabled) { 53 | if (UnityTranslate.config.client.openBrowserWithoutPromptV2 == UnityTranslateConfig.TriState.TRUE) { 54 | Util.getPlatform().openUri("http://127.0.0.1:$serverPort") 55 | } else if (UnityTranslate.config.client.openBrowserWithoutPromptV2 == UnityTranslateConfig.TriState.DEFAULT) { 56 | Minecraft.getInstance().execute { 57 | if (mc.screen is RequestDownloadScreen) { 58 | (mc.screen as RequestDownloadScreen).parent = OpenBrowserScreen("http://127.0.0.1:$serverPort") 59 | } else { 60 | mc.setScreen(OpenBrowserScreen("http://127.0.0.1:$serverPort")) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | override fun stop() { 68 | server.stop(0) 69 | socket.stop(1000) 70 | } 71 | 72 | override fun changeLanguage(language: Language) { 73 | super.changeLanguage(language) 74 | this.socket.broadcast("set_language", JsonObject().apply { 75 | addProperty("language", language.supportedTranscribers[TranscriberType.BROWSER]) 76 | }) 77 | } 78 | 79 | override fun setMuted(muted: Boolean) { 80 | this.socket.broadcast("set_muted", JsonObject().apply { 81 | this.addProperty("muted", muted) 82 | }) 83 | } 84 | 85 | inner class BrowserSocket : WebSocketServer(InetSocketAddress("0.0.0.0", socketPort)) { 86 | var totalConnections = 0 87 | 88 | override fun onOpen(ws: WebSocket, handshake: ClientHandshake) { 89 | ws.sendData("set_language", JsonObject().apply { 90 | addProperty("language", language.supportedTranscribers[TranscriberType.BROWSER]) 91 | }) 92 | totalConnections++ 93 | 94 | UnityTranslateClient.displayMessage(Component.translatable("unitytranslate.transcriber.connected")) 95 | setMuted(!UnityTranslateClient.shouldTranscribe) 96 | } 97 | 98 | override fun onClose(ws: WebSocket, code: Int, reason: String, remote: Boolean) { 99 | totalConnections-- 100 | 101 | UnityTranslateClient.displayMessage(Component.translatable("unitytranslate.transcriber.disconnected") 102 | .withStyle { 103 | it.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, "http://127.0.0.1:${serverPort}")) 104 | }) 105 | } 106 | 107 | override fun onMessage(ws: WebSocket, message: String) { 108 | val msg = JsonParser.parseString(message).asJsonObject 109 | val data = if (msg.has("d")) msg.getAsJsonObject("d") else JsonObject() 110 | 111 | when (msg.get("op").asString) { 112 | "transcript" -> { 113 | val results = data.getAsJsonArray("results") 114 | val index = data.get("index").asInt 115 | 116 | val deserialized = mutableListOf>() 117 | for (result in results) { 118 | val d = result.asJsonObject 119 | deserialized.add(d.get("text").asString to d.get("confidence").asDouble) 120 | } 121 | 122 | if (deserialized.isEmpty()) { 123 | lastIndex = currentOffset + index 124 | return 125 | } 126 | 127 | val selected = deserialized.sortedByDescending { it.second }[0].first 128 | 129 | if (selected.isNotBlank()) { 130 | updater.accept(currentOffset + index, selected.trim()) 131 | } 132 | 133 | lastIndex = currentOffset + index 134 | } 135 | 136 | "reset" -> { 137 | currentOffset = lastIndex + 1 138 | } 139 | 140 | "error" -> { 141 | val type = data.get("type").asString 142 | 143 | UnityTranslateClient.displayMessage(Component.translatable("unitytranslate.transcriber.error") 144 | .append(Component.translatable("unitytranslate.transcriber.error.$type")), true) 145 | } 146 | } 147 | } 148 | 149 | override fun onError(ws: WebSocket, ex: Exception) { 150 | ex.printStackTrace() 151 | } 152 | 153 | override fun onStart() { 154 | UnityTranslate.logger.info("Started WebSocket server for Browser Transcriber mode at ${this.address}") 155 | } 156 | 157 | fun broadcast(op: String, data: JsonObject = JsonObject()) { 158 | super.broadcast(JsonObject().apply { 159 | this.addProperty("op", op) 160 | this.add("d", data) 161 | }.toString()) 162 | } 163 | 164 | fun WebSocket.sendData(op: String, data: JsonObject = JsonObject()) { 165 | this.send(JsonObject().apply { 166 | this.addProperty("op", op) 167 | this.add("d", data) 168 | }.toString()) 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/transcribers/sphinx/SphinxSpeechTranscriber.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client.transcribers.sphinx 2 | 3 | import xyz.bluspring.unitytranslate.Language 4 | import xyz.bluspring.unitytranslate.client.transcribers.SpeechTranscriber 5 | 6 | class SphinxSpeechTranscriber(language: Language) : SpeechTranscriber(language) { 7 | init { 8 | throw IllegalStateException("PocketSphinx transcription is currently not supported!") 9 | } 10 | 11 | override fun stop() { 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/client/transcribers/windows/sapi5/WindowsSpeechApiTranscriber.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.client.transcribers.windows.sapi5 2 | 3 | import net.minecraft.Util 4 | import org.lwjgl.system.APIUtil 5 | import org.lwjgl.system.SharedLibrary 6 | import xyz.bluspring.unitytranslate.Language 7 | import xyz.bluspring.unitytranslate.UnityTranslate 8 | import xyz.bluspring.unitytranslate.client.transcribers.SpeechTranscriber 9 | 10 | class WindowsSpeechApiTranscriber(language: Language) : SpeechTranscriber(language) { 11 | init { 12 | } 13 | 14 | // TODO: need to figure out how to use the Speech Recognition API without using JNI. 15 | 16 | override fun stop() { 17 | 18 | } 19 | 20 | companion object { 21 | lateinit var library: SharedLibrary 22 | 23 | private var hasTriedLoading = false 24 | 25 | fun isLibraryLoaded(): Boolean { 26 | return Companion::library.isInitialized 27 | } 28 | 29 | fun isSupported(): Boolean { 30 | if (Util.getPlatform() != Util.OS.WINDOWS) 31 | return false 32 | 33 | //tryLoadingLibrary() 34 | //return isLibraryLoaded() 35 | return false 36 | } 37 | 38 | private fun tryLoadingLibrary() { 39 | if (!hasTriedLoading && !isLibraryLoaded()) { 40 | try { 41 | library = APIUtil.apiCreateLibrary("sapi.dll") 42 | } catch (e: Throwable) { 43 | UnityTranslate.logger.error("Failed to load Windows Speech API!") 44 | e.printStackTrace() 45 | } 46 | 47 | hasTriedLoading = true 48 | } 49 | } 50 | 51 | 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/commands/UnityTranslateClientCommands.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.commands 2 | 3 | import dev.architectury.event.events.client.ClientCommandRegistrationEvent 4 | import net.minecraft.network.chat.Component 5 | import net.minecraft.network.chat.ComponentUtils 6 | import xyz.bluspring.unitytranslate.UnityTranslate 7 | import xyz.bluspring.unitytranslate.client.UnityTranslateClient 8 | import xyz.bluspring.unitytranslate.client.transcribers.browser.BrowserSpeechTranscriber 9 | import xyz.bluspring.unitytranslate.translator.LocalLibreTranslateInstance 10 | import xyz.bluspring.unitytranslate.translator.TranslatorManager 11 | 12 | object UnityTranslateClientCommands { 13 | val TRANSCRIBER = ClientCommandRegistrationEvent.literal("checktranscriber") 14 | .executes { 15 | if (UnityTranslateClient.transcriber is BrowserSpeechTranscriber) { 16 | (UnityTranslateClient.transcriber as BrowserSpeechTranscriber).openWebsite() 17 | } 18 | 19 | it.source.`arch$sendSuccess`({ Component.literal("Reopening browser transcriber if not opened") }, false) 20 | 21 | 1 22 | } 23 | 24 | val INFO = ClientCommandRegistrationEvent.literal("info") 25 | .executes { 26 | it.source.`arch$sendSuccess`({ 27 | ComponentUtils.formatList( 28 | listOf( 29 | Component.literal("UnityTranslate v${UnityTranslate.instance.proxy.modVersion}"), 30 | Component.literal("- Enabled: ${UnityTranslate.config.client.enabled}"), 31 | Component.literal("- Current transcriber: ${UnityTranslate.config.client.transcriber}"), 32 | Component.literal("- Spoken language: ${UnityTranslate.config.client.language}"), 33 | Component.empty(), 34 | Component.literal("- Server supports UnityTranslate: ${UnityTranslateClient.connectedServerHasSupport}"), 35 | Component.literal("- Supports local translation server: ${LocalLibreTranslateInstance.canRunLibreTranslate()}"), 36 | Component.literal("- Is local translation server running: ${LocalLibreTranslateInstance.hasStarted}"), 37 | Component.literal("- Supports CUDA: ${TranslatorManager.checkSupportsCuda()}"), 38 | ), Component.literal("\n") 39 | ) 40 | }, false) 41 | 42 | 1 43 | } 44 | 45 | val OPEN_BROWSER = ClientCommandRegistrationEvent.literal("openbrowser") 46 | .executes { 47 | if (UnityTranslateClient.transcriber is BrowserSpeechTranscriber) { 48 | (UnityTranslateClient.transcriber as BrowserSpeechTranscriber).openWebsite() 49 | } 50 | 51 | 1 52 | } 53 | 54 | val ROOT = ClientCommandRegistrationEvent.literal("unitytranslateclient") 55 | .then(TRANSCRIBER) 56 | .then(INFO) 57 | .then(OPEN_BROWSER) 58 | 59 | fun init() { 60 | ClientCommandRegistrationEvent.EVENT.register { dispatcher, ctx -> 61 | dispatcher.register(ROOT) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/commands/UnityTranslateCommands.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.commands 2 | 3 | import net.minecraft.commands.Commands 4 | import net.minecraft.network.chat.Component 5 | import net.minecraft.network.chat.ComponentUtils 6 | import xyz.bluspring.unitytranslate.UnityTranslate 7 | import xyz.bluspring.unitytranslate.translator.LocalLibreTranslateInstance 8 | import xyz.bluspring.unitytranslate.translator.TranslatorManager 9 | 10 | object UnityTranslateCommands { 11 | val INFO = Commands.literal("info") 12 | .executes { ctx -> 13 | ctx.source.sendSystemMessage(ComponentUtils.formatList(listOf( 14 | Component.literal("UnityTranslate v${UnityTranslate.instance.proxy.modVersion}"), 15 | Component.literal("- Total instances loaded: ${TranslatorManager.instances.size}"), 16 | Component.literal("- Queued translations: ${TranslatorManager.queuedTranslations.size}"), 17 | Component.empty(), 18 | Component.literal("- Supports local translation server: ${LocalLibreTranslateInstance.canRunLibreTranslate()}"), 19 | Component.literal("- Is local translation server running: ${LocalLibreTranslateInstance.hasStarted}"), 20 | Component.literal("- Supports CUDA: ${TranslatorManager.checkSupportsCuda()}"), 21 | ), Component.literal("\n"))) 22 | 23 | 1 24 | } 25 | 26 | val CLEAR_QUEUE = Commands.literal("clearqueue") 27 | .executes { ctx -> 28 | TranslatorManager.queuedTranslations.clear() 29 | ctx.source.sendSystemMessage(Component.literal("Forcefully cleared translation queue.")) 30 | 31 | 1 32 | } 33 | 34 | val DEBUG_RESTART = Commands.literal("debugreload") 35 | .executes { ctx -> 36 | TranslatorManager.installLibreTranslate() 37 | ctx.source.sendSystemMessage(Component.literal("Restarted timer!")) 38 | 39 | 1 40 | } 41 | 42 | val ROOT = Commands.literal("unitytranslate") 43 | .requires { it.hasPermission(3) } 44 | .then(INFO) 45 | .then(CLEAR_QUEUE) 46 | .then(DEBUG_RESTART) 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/compat/talkballoons/TalkBalloonsCompat.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.compat.talkballoons 2 | 3 | import com.cerbon.talk_balloons.TalkBalloons 4 | import com.cerbon.talk_balloons.util.mixin.IAbstractClientPlayer 5 | import xyz.bluspring.unitytranslate.UnityTranslate 6 | import xyz.bluspring.unitytranslate.events.TranscriptEvents 7 | import java.util.* 8 | import java.util.concurrent.ConcurrentSkipListMap 9 | 10 | object TalkBalloonsCompat { 11 | private val lastBalloonText = ConcurrentSkipListMap>() 12 | 13 | fun init() { 14 | TranscriptEvents.UPDATE.register { transcript, language -> 15 | if (language != UnityTranslate.config.client.language) 16 | return@register 17 | 18 | if (transcript.player !is IAbstractClientPlayer) 19 | return@register 20 | 21 | val uuid = transcript.player.uuid 22 | 23 | if (transcript.player.balloonMessages?.isNotEmpty() != true) { 24 | lastBalloonText.remove(uuid) 25 | } 26 | 27 | val text = transcript.text 28 | 29 | if (lastBalloonText.containsKey(uuid)) { 30 | val (index, lastText) = lastBalloonText[uuid]!! 31 | 32 | if (lastText == text) 33 | return@register 34 | 35 | if (index == transcript.index) { 36 | transcript.player.balloonMessages.removeIf { it == lastText } 37 | } 38 | } 39 | 40 | transcript.player.createBalloonMessage(text, TalkBalloons.config.balloonAge * 20) 41 | lastBalloonText[uuid] = transcript.index to text 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/compat/voicechat/PlasmoVoiceChatClientCompat.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.compat.voicechat 2 | 3 | import com.google.inject.Inject 4 | import su.plo.voice.api.addon.AddonInitializer 5 | import su.plo.voice.api.addon.AddonLoaderScope 6 | import su.plo.voice.api.addon.annotation.Addon 7 | import su.plo.voice.api.client.PlasmoVoiceClient 8 | 9 | @Addon( 10 | id = "pv-unitytranslate-compat-client", 11 | name = "UnityTranslate", 12 | version = "1.0.0", 13 | authors = [ "BluSpring" ], 14 | scope = AddonLoaderScope.CLIENT 15 | ) 16 | class PlasmoVoiceChatClientCompat : AddonInitializer { 17 | @Inject 18 | lateinit var voiceClient: PlasmoVoiceClient 19 | 20 | override fun onAddonInitialize() { 21 | instance = this 22 | } 23 | 24 | companion object { 25 | lateinit var instance: PlasmoVoiceChatClientCompat 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/compat/voicechat/PlasmoVoiceChatCompat.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.compat.voicechat 2 | 3 | import com.google.inject.Inject 4 | import net.minecraft.client.Minecraft 5 | import net.minecraft.server.level.ServerPlayer 6 | import net.minecraft.world.entity.player.Player 7 | import su.plo.voice.api.addon.AddonInitializer 8 | import su.plo.voice.api.addon.AddonLoaderScope 9 | import su.plo.voice.api.addon.annotation.Addon 10 | import su.plo.voice.api.client.PlasmoVoiceClient 11 | import su.plo.voice.api.client.event.connection.VoicePlayerUpdateEvent 12 | import su.plo.voice.api.event.EventSubscribe 13 | import su.plo.voice.api.server.PlasmoVoiceServer 14 | import su.plo.voice.api.server.audio.capture.ProximityServerActivationHelper 15 | import su.plo.voice.api.server.event.mute.PlayerVoiceMutedEvent 16 | import xyz.bluspring.unitytranslate.UnityTranslate 17 | import xyz.bluspring.unitytranslate.client.UnityTranslateClient 18 | 19 | @Addon( 20 | id = "pv-unitytranslate-compat", 21 | name = "UnityTranslate", 22 | version = "1.0.0", 23 | authors = [ "BluSpring" ], 24 | scope = AddonLoaderScope.ANY 25 | ) 26 | class PlasmoVoiceChatCompat : AddonInitializer { 27 | @Inject 28 | lateinit var voiceServer: PlasmoVoiceServer 29 | 30 | private var proximityHelper: ProximityServerActivationHelper? = null 31 | 32 | override fun onAddonInitialize() { 33 | instance = this 34 | } 35 | 36 | @EventSubscribe 37 | fun onVoiceUpdateEvent(event: VoicePlayerUpdateEvent) { 38 | if (event.player.playerId != Minecraft.getInstance().player?.uuid) 39 | return 40 | 41 | if (UnityTranslate.config.client.muteTranscriptWhenVoiceChatMuted) { 42 | if (event.player.isVoiceDisabled) { 43 | UnityTranslateClient.shouldTranscribe = false 44 | } else if (!event.player.isVoiceDisabled && !(event.player.isMuted || event.player.isMicrophoneMuted)) { 45 | UnityTranslateClient.shouldTranscribe = true 46 | } else if (!event.player.isVoiceDisabled && (event.player.isMuted || event.player.isMicrophoneMuted)) { 47 | UnityTranslateClient.shouldTranscribe = false 48 | } 49 | } 50 | } 51 | 52 | companion object { 53 | lateinit var instance: PlasmoVoiceChatCompat 54 | 55 | fun init() { 56 | PlasmoVoiceServer.getAddonsLoader().load(PlasmoVoiceChatCompat()) 57 | PlasmoVoiceClient.getAddonsLoader().load(PlasmoVoiceChatClientCompat()) 58 | } 59 | 60 | fun getNearbyPlayers(source: ServerPlayer): List { 61 | if (isPlayerMutedOrDeafened(source)) 62 | return listOf(source) 63 | 64 | val distance = instance.voiceServer.config?.voice()?.proximity()?.defaultDistance() ?: 5 65 | 66 | return source.serverLevel().getPlayers { 67 | (!isPlayerDeafened(it) && 68 | ((it.distanceToSqr(source) <= distance * distance && UTVoiceChatCompat.areBothSpectator(source, it)) || 69 | playerSharesGroup(it, source)) 70 | ) 71 | || it == source 72 | } 73 | } 74 | 75 | fun isPlayerAudible(source: Player): Boolean { 76 | val connection = PlasmoVoiceChatClientCompat.instance.voiceClient.serverConnection.orElse(null) ?: return false 77 | val vcPlayer = connection.getPlayerById(source.uuid).orElse(null) ?: return false 78 | 79 | return !vcPlayer.isMuted && !vcPlayer.isMicrophoneMuted && !vcPlayer.isVoiceDisabled 80 | } 81 | 82 | fun playerSharesGroup(player: ServerPlayer, other: ServerPlayer): Boolean { 83 | // TODO: Support pv-addon-groups 84 | return false 85 | } 86 | 87 | fun isPlayerMutedOrDeafened(player: ServerPlayer): Boolean { 88 | val vcPlayer = instance.voiceServer.playerManager.getPlayerById(player.uuid).orElse(null) ?: return true 89 | 90 | if (!vcPlayer.hasVoiceChat()) 91 | return true 92 | 93 | return vcPlayer.isVoiceDisabled || vcPlayer.isMicrophoneMuted 94 | } 95 | 96 | fun isPlayerDeafened(player: ServerPlayer): Boolean { 97 | val vcPlayer = instance.voiceServer.playerManager.getPlayerById(player.uuid).orElse(null) ?: return true 98 | 99 | if (!vcPlayer.hasVoiceChat()) 100 | return true 101 | 102 | return vcPlayer.isVoiceDisabled 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/compat/voicechat/SimpleVoiceChatCompat.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.compat.voicechat 2 | 3 | import de.maxhenkel.voicechat.api.ForgeVoicechatPlugin 4 | import de.maxhenkel.voicechat.api.Group 5 | import de.maxhenkel.voicechat.api.VoicechatClientApi 6 | import de.maxhenkel.voicechat.api.VoicechatPlugin 7 | import de.maxhenkel.voicechat.api.VoicechatServerApi 8 | import de.maxhenkel.voicechat.api.events.ClientVoicechatInitializationEvent 9 | import de.maxhenkel.voicechat.api.events.EventRegistration 10 | import de.maxhenkel.voicechat.api.events.MicrophoneMuteEvent 11 | import de.maxhenkel.voicechat.api.events.VoicechatDisableEvent 12 | import de.maxhenkel.voicechat.api.events.VoicechatServerStartedEvent 13 | import net.minecraft.server.level.ServerPlayer 14 | import net.minecraft.world.entity.player.Player 15 | import xyz.bluspring.unitytranslate.UnityTranslate 16 | import xyz.bluspring.unitytranslate.client.UnityTranslateClient 17 | 18 | @ForgeVoicechatPlugin 19 | class SimpleVoiceChatCompat : VoicechatPlugin { 20 | override fun getPluginId(): String { 21 | return UnityTranslate.MOD_ID 22 | } 23 | 24 | override fun registerEvents(registration: EventRegistration) { 25 | super.registerEvents(registration) 26 | 27 | registration.registerEvent(MicrophoneMuteEvent::class.java) { 28 | if (UnityTranslate.config.client.muteTranscriptWhenVoiceChatMuted) { 29 | UnityTranslateClient.shouldTranscribe = !it.isDisabled 30 | } 31 | } 32 | 33 | registration.registerEvent(VoicechatDisableEvent::class.java) { 34 | if (UnityTranslate.config.client.muteTranscriptWhenVoiceChatMuted) { 35 | if (it.isDisabled) { 36 | UnityTranslateClient.shouldTranscribe = false 37 | } else if (!it.isDisabled && !it.voicechat.isMuted) { 38 | UnityTranslateClient.shouldTranscribe = true 39 | } else if (!it.isDisabled && it.voicechat.isMuted) { 40 | UnityTranslateClient.shouldTranscribe = false 41 | } 42 | } 43 | } 44 | 45 | registration.registerEvent(VoicechatServerStartedEvent::class.java) { 46 | voiceChatServer = it.voicechat 47 | } 48 | 49 | registration.registerEvent(ClientVoicechatInitializationEvent::class.java) { 50 | voiceChatClient = it.voicechat 51 | } 52 | } 53 | 54 | companion object { 55 | lateinit var voiceChatServer: VoicechatServerApi 56 | lateinit var voiceChatClient: VoicechatClientApi 57 | 58 | fun getNearbyPlayers(source: ServerPlayer): List { 59 | if (isPlayerDeafened(source)) 60 | return listOf(source) 61 | 62 | return source.serverLevel().getPlayers { 63 | (!isPlayerDeafened(it) && 64 | ((it.distanceToSqr(source) <= voiceChatServer.voiceChatDistance * voiceChatServer.voiceChatDistance && UTVoiceChatCompat.areBothSpectator(source, it)) || 65 | playerSharesGroup(source, it)) 66 | ) 67 | || it == source 68 | } 69 | } 70 | 71 | fun playerSharesGroup(player: ServerPlayer, other: ServerPlayer): Boolean { 72 | val firstGroup = voiceChatServer.getConnectionOf(player.uuid)?.group ?: return false 73 | if (firstGroup.type == Group.Type.OPEN) 74 | return true 75 | 76 | val secondGroup = voiceChatServer.getConnectionOf(other.uuid)?.group ?: return false 77 | if (secondGroup.type == Group.Type.ISOLATED && firstGroup.id != secondGroup.id) 78 | return false 79 | 80 | return firstGroup.id == secondGroup.id 81 | } 82 | 83 | fun isPlayerAudible(player: Player): Boolean { 84 | // FIXME: SVC doesn't provide an easy way of detecting this... 85 | return true 86 | } 87 | 88 | fun isPlayerDeafened(player: ServerPlayer): Boolean { 89 | return voiceChatServer.getConnectionOf(player.uuid)?.isDisabled == true 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/compat/voicechat/UTVoiceChatCompat.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.compat.voicechat 2 | 3 | import net.minecraft.client.Minecraft 4 | import net.minecraft.server.level.ServerPlayer 5 | import net.minecraft.world.entity.player.Player 6 | import net.minecraft.world.level.GameType 7 | import xyz.bluspring.unitytranslate.UnityTranslate 8 | 9 | object UTVoiceChatCompat { 10 | val usesSimpleVoiceChat: Boolean 11 | get() { 12 | return UnityTranslate.instance.proxy.isModLoaded("voicechat") 13 | } 14 | 15 | val usesPlasmoVoice: Boolean 16 | get() { 17 | return UnityTranslate.instance.proxy.isModLoaded("plasmovoice") 18 | } 19 | 20 | val hasVoiceChat: Boolean 21 | get() { 22 | return usesSimpleVoiceChat || usesPlasmoVoice 23 | } 24 | 25 | fun getNearbyPlayers(source: ServerPlayer): List { 26 | return if (usesSimpleVoiceChat) 27 | SimpleVoiceChatCompat.getNearbyPlayers(source) 28 | else if (usesPlasmoVoice) 29 | PlasmoVoiceChatCompat.getNearbyPlayers(source) 30 | else 31 | listOf(source) 32 | } 33 | 34 | fun isPlayerDeafened(player: ServerPlayer): Boolean { 35 | return if (usesSimpleVoiceChat) 36 | SimpleVoiceChatCompat.isPlayerDeafened(player) 37 | else if (usesPlasmoVoice) 38 | PlasmoVoiceChatCompat.isPlayerDeafened(player) 39 | else 40 | false 41 | } 42 | 43 | fun isPlayerAudible(player: Player): Boolean { 44 | if (player == Minecraft.getInstance().player) 45 | return true 46 | 47 | return if (usesSimpleVoiceChat) 48 | SimpleVoiceChatCompat.isPlayerAudible(player) 49 | else if (usesPlasmoVoice) 50 | PlasmoVoiceChatCompat.isPlayerAudible(player) 51 | else false 52 | } 53 | 54 | fun areBothSpectator(player: ServerPlayer, other: ServerPlayer): Boolean { 55 | if (player.gameMode.gameModeForPlayer == GameType.SPECTATOR && other.gameMode.gameModeForPlayer == GameType.SPECTATOR) 56 | return true 57 | else if (player.gameMode.gameModeForPlayer == GameType.SPECTATOR && other.gameMode.gameModeForPlayer != GameType.SPECTATOR) 58 | return false 59 | else if (player.gameMode.gameModeForPlayer != GameType.SPECTATOR && other.gameMode.gameModeForPlayer == GameType.SPECTATOR) 60 | return true 61 | 62 | return true 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/config/DependsOn.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.config 2 | 3 | @Retention(AnnotationRetention.RUNTIME) 4 | annotation class DependsOn( 5 | val configName: String 6 | ) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/config/FloatRange.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.config 2 | 3 | @Retention(AnnotationRetention.RUNTIME) 4 | annotation class FloatRange( 5 | val from: Float, 6 | val to: Float, 7 | val increment: Float 8 | ) 9 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/config/Hidden.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.config 2 | 3 | annotation class Hidden() 4 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/config/IntRange.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.config 2 | 3 | @Retention(AnnotationRetention.RUNTIME) 4 | annotation class IntRange( 5 | val from: Int, 6 | val to: Int, 7 | val increment: Int 8 | ) 9 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/config/UnityTranslateConfig.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.config 2 | 3 | import kotlinx.serialization.Serializable 4 | import xyz.bluspring.unitytranslate.Language 5 | import xyz.bluspring.unitytranslate.client.gui.TranscriptBox 6 | import xyz.bluspring.unitytranslate.client.transcribers.TranscriberType 7 | 8 | @Serializable 9 | data class UnityTranslateConfig( 10 | val client: ClientConfig = ClientConfig(), 11 | val server: CommonConfig = CommonConfig() 12 | ) { 13 | @Serializable 14 | data class ClientConfig( 15 | var enabled: Boolean = true, 16 | var openBrowserWithoutPromptV2: TriState = TriState.DEFAULT, 17 | var muteTranscriptWhenVoiceChatMuted: Boolean = true, 18 | 19 | @get:IntRange(from = 10, to = 300, increment = 10) 20 | var textScale: Int = 100, 21 | 22 | val transcriptBoxes: MutableList = mutableListOf(), 23 | 24 | @get:Hidden 25 | val transcriber: TranscriberType = TranscriberType.BROWSER, 26 | 27 | @get:Hidden 28 | var language: Language = Language.ENGLISH, 29 | 30 | var disappearingText: Boolean = true, 31 | @get:DependsOn("disappearingText") 32 | @get:FloatRange(from = 0.2f, to = 60.0f, increment = 0.1f) 33 | var disappearingTextDelay: Float = 20.0f, 34 | @get:DependsOn("disappearingText") 35 | @get:FloatRange(from = 0.0f, to = 5.0f, increment = 0.1f) 36 | var disappearingTextFade: Float = 0.5f 37 | ) 38 | 39 | @Serializable 40 | data class CommonConfig( 41 | var translatePriority: MutableList = mutableListOf( 42 | TranslationPriority.CLIENT_GPU, // highest priority, prioritize using CUDA on the client-side. 43 | //TranslationPriority.SERVER_GPU, // if supported, use CUDA on the server-side. // TODO: make this not fucking require LWJGL 44 | TranslationPriority.SERVER_CPU, // otherwise, translate on the CPU. 45 | TranslationPriority.OFFLOADED, // use alternative servers if available 46 | TranslationPriority.CLIENT_CPU, // worst case scenario, use client CPU. 47 | ), 48 | 49 | @get:DependsOn("shouldRunTranslationServer") 50 | var shouldUseCuda: Boolean = true, 51 | 52 | var shouldRunTranslationServer: Boolean = true, 53 | @get:DependsOn("shouldRunTranslationServer") 54 | @get:IntRange(from = 1, to = 128, increment = 1) 55 | var libreTranslateThreads: Int = 4, 56 | 57 | var offloadServers: MutableList = mutableListOf( 58 | OffloadedLibreTranslateServer("https://libretranslate.devos.gay"), 59 | OffloadedLibreTranslateServer("https://trans.zillyhuhn.com"), 60 | ), 61 | 62 | // Interval for when the batch translations will be sent. 63 | // This is done so redundant translations don't go through, 64 | // which puts unnecessary stress on the translation instances. 65 | @get:FloatRange(from = 0.5f, to = 5.0f, increment = 0.1f) 66 | var batchTranslateInterval: Float = 0.5f, // 500ms 67 | ) 68 | 69 | @Serializable 70 | data class OffloadedLibreTranslateServer( 71 | var url: String, // follows http://127.0.0.1:5000 - the /translate endpoint will be appended at the end automatically. 72 | var authKey: String? = null, 73 | var weight: Int = 100, 74 | var maxConcurrentTranslations: Int = 20 75 | ) 76 | 77 | enum class TriState { 78 | TRUE, FALSE, DEFAULT 79 | } 80 | 81 | enum class TranslationPriority(val usesCuda: TriState) { 82 | SERVER_GPU(TriState.TRUE), 83 | SERVER_CPU(TriState.FALSE), 84 | CLIENT_GPU(TriState.TRUE), 85 | CLIENT_CPU(TriState.FALSE), 86 | OFFLOADED(TriState.DEFAULT) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/duck/ScrollableWidget.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.duck 2 | 3 | interface ScrollableWidget { 4 | @Suppress("INAPPLICABLE_JVM_NAME") 5 | @get:JvmName("unityTranslate\$getInitialX") 6 | val initialX: Int 7 | 8 | @Suppress("INAPPLICABLE_JVM_NAME") 9 | @get:JvmName("unityTranslate\$getInitialY") 10 | val initialY: Int 11 | 12 | @Suppress("INAPPLICABLE_JVM_NAME") 13 | @JvmName("unityTranslate\$updateInitialPosition") 14 | fun updateInitialPosition() 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/events/TranscriptEvents.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.events 2 | 3 | import dev.architectury.event.Event 4 | import dev.architectury.event.EventFactory 5 | import xyz.bluspring.unitytranslate.Language 6 | import xyz.bluspring.unitytranslate.transcript.Transcript 7 | 8 | interface TranscriptEvents { 9 | fun interface Update { 10 | fun onTranscriptUpdate(transcript: Transcript, language: Language) 11 | } 12 | 13 | companion object { 14 | val UPDATE: Event = EventFactory.createLoop(Update::class.java) 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/fabric/UnityTranslateFabric.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.fabric 2 | 3 | //#if FABRIC 4 | import net.fabricmc.api.ModInitializer 5 | import xyz.bluspring.unitytranslate.UnityTranslate 6 | 7 | class UnityTranslateFabric : ModInitializer { 8 | override fun onInitialize() { 9 | instance = UnityTranslate() 10 | } 11 | 12 | companion object { 13 | lateinit var instance: UnityTranslate 14 | } 15 | } 16 | //#endif -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/fabric/client/UnityTranslateFabricClient.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.fabric.client 2 | 3 | //#if FABRIC 4 | import net.fabricmc.api.ClientModInitializer 5 | import xyz.bluspring.unitytranslate.client.UnityTranslateClient 6 | 7 | class UnityTranslateFabricClient : ClientModInitializer { 8 | override fun onInitializeClient() { 9 | UnityTranslateClient() 10 | UnityTranslateClient.registerKeys() 11 | } 12 | } 13 | //#endif -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/fabric/compat/modmenu/UTModMenuIntegration.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.fabric.compat.modmenu 2 | 3 | //#if FABRIC 4 | import com.terraformersmc.modmenu.api.ConfigScreenFactory 5 | import com.terraformersmc.modmenu.api.ModMenuApi 6 | import xyz.bluspring.unitytranslate.client.gui.UTConfigScreen 7 | 8 | class UTModMenuIntegration : ModMenuApi { 9 | override fun getModConfigScreenFactory(): ConfigScreenFactory<*> { 10 | return ConfigScreenFactory { 11 | UTConfigScreen(it) 12 | } 13 | } 14 | } 15 | //#endif -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/neoforge/ConfigScreenHelper.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.neoforge 2 | 3 | //#if FORGE-LIKE 4 | 5 | //#if FORGE 6 | //$$ import net.minecraftforge.fml.ModLoadingContext 7 | //#elseif NEOFORGE 8 | //$$ import net.neoforged.fml.ModLoadingContext 9 | //#endif 10 | 11 | //#if MC >= 1.20.6 12 | //$$ import net.neoforged.neoforge.client.gui.IConfigScreenFactory 13 | //#else 14 | //#if FORGE 15 | //$$ import net.minecraftforge.client.ConfigScreenHandler 16 | //#else 17 | //$$ import net.neoforged.neoforge.client.ConfigScreenHandler 18 | //#endif 19 | //#endif 20 | 21 | //$$ import xyz.bluspring.unitytranslate.client.gui.UTConfigScreen 22 | 23 | //#endif 24 | 25 | object ConfigScreenHelper { 26 | fun createConfigScreen() { 27 | //#if FORGE-LIKE 28 | //#if MC >= 1.20.6 29 | //$$ ModLoadingContext.get().registerExtensionPoint(IConfigScreenFactory::class.java) { 30 | //$$ IConfigScreenFactory { mc, prev -> 31 | //#else 32 | //$$ ModLoadingContext.get().registerExtensionPoint(ConfigScreenHandler.ConfigScreenFactory::class.java) { 33 | //$$ ConfigScreenHandler.ConfigScreenFactory { mc, prev -> 34 | //#endif 35 | //$$ UTConfigScreen(prev) 36 | //$$ } 37 | //$$ } 38 | //#endif 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/neoforge/NeoForgeEvents.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.neoforge 2 | 3 | //#if NEOFORGE 4 | //$$ import net.neoforged.bus.api.SubscribeEvent 5 | //$$ import net.neoforged.neoforge.common.NeoForge 6 | //$$ import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent 7 | //#elseif FORGE 8 | //$$ import net.minecraftforge.common.MinecraftForge 9 | //$$ import net.minecraftforge.eventbus.api.SubscribeEvent 10 | //$$ import net.minecraftforge.server.permission.events.PermissionGatherEvent 11 | //#endif 12 | 13 | //#if FORGE-LIKE 14 | //$$ import xyz.bluspring.unitytranslate.UnityTranslate 15 | 16 | //$$ object NeoForgeEvents { 17 | //$$ fun init() { 18 | //#if FORGE 19 | //$$ MinecraftForge.EVENT_BUS.register(this) 20 | //#elseif NEOFORGE 21 | //$$ NeoForge.EVENT_BUS.register(this) 22 | //#endif 23 | //$$ } 24 | //$$ 25 | //$$ @SubscribeEvent 26 | //$$ fun onPermissionsGather(event: PermissionGatherEvent.Nodes) { 27 | //$$ UnityTranslate.instance.proxy.registerPermissions(event) 28 | //$$ } 29 | //$$ } 30 | //#endif -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/neoforge/UnityTranslateNeoForge.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.neoforge 2 | 3 | //#if FORGE-LIKE 4 | 5 | //#if FORGE 6 | //$$ import net.minecraftforge.api.distmarker.Dist 7 | //$$ import net.minecraftforge.api.distmarker.OnlyIn 8 | //$$ import net.minecraftforge.common.MinecraftForge 9 | //$$ import net.minecraftforge.client.event.RegisterKeyMappingsEvent 10 | //$$ import net.minecraftforge.eventbus.api.SubscribeEvent 11 | //$$ import net.minecraftforge.fml.ModLoadingContext 12 | //$$ import net.minecraftforge.fml.common.Mod 13 | //$$ import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent 14 | //$$ import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext 15 | //$$ import net.minecraftforge.server.permission.events.PermissionGatherEvent 16 | //#elseif NEOFORGE 17 | //$$ import net.neoforged.api.distmarker.Dist 18 | //$$ import net.neoforged.api.distmarker.OnlyIn 19 | //$$ import net.neoforged.bus.api.SubscribeEvent 20 | //$$ import net.neoforged.fml.ModLoadingContext 21 | //$$ import net.neoforged.fml.common.Mod 22 | //$$ import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent 23 | //$$ import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent 24 | //#endif 25 | 26 | //#if MC >= 1.20.4 27 | //$$ import thedarkcolour.kotlinforforge.neoforge.forge.MOD_BUS 28 | //#endif 29 | 30 | //$$ import xyz.bluspring.unitytranslate.UnityTranslate 31 | //$$ import xyz.bluspring.unitytranslate.client.UnityTranslateClient 32 | //$$ 33 | //$$ @Mod(UnityTranslate.MOD_ID) 34 | //$$ class UnityTranslateNeoForge { 35 | //$$ init { 36 | //$$ UnityTranslate() 37 | //#if MC >= 1.20.4 38 | //$$ MOD_BUS.register(this) 39 | //#else 40 | //$$ FMLJavaModLoadingContext.get().modEventBus.register(this) 41 | //#endif 42 | //$$ NeoForgeEvents.init() 43 | //$$ } 44 | //$$ 45 | //$$ @OnlyIn(Dist.CLIENT) 46 | //$$ @SubscribeEvent 47 | //$$ fun onClientLoading(ev: FMLClientSetupEvent) { 48 | //$$ UnityTranslateClient() 49 | //$$ 50 | //$$ ConfigScreenHelper.createConfigScreen() 51 | //$$ } 52 | //$$ 53 | //$$ @OnlyIn(Dist.CLIENT) 54 | //$$ @SubscribeEvent 55 | //$$ fun onClientKeybinds(ev: RegisterKeyMappingsEvent) { 56 | //$$ UnityTranslateClient.registerKeys() 57 | //$$ } 58 | //$$ } 59 | 60 | //#endif -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/network/PacketIds.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.network 2 | 3 | //#if MC >= 1.20.6 4 | //$$ import net.minecraft.network.RegistryFriendlyByteBuf 5 | //$$ import net.minecraft.network.codec.StreamCodec 6 | //$$ import net.minecraft.network.protocol.common.custom.CustomPacketPayload 7 | //$$ import net.minecraft.network.protocol.common.custom.CustomPacketPayload.TypeAndCodec 8 | //$$ import xyz.bluspring.unitytranslate.network.payloads.* 9 | //#endif 10 | import xyz.bluspring.unitytranslate.UnityTranslate 11 | 12 | object PacketIds { 13 | //#if MC >= 1.20.6 14 | //$$ val SERVER_SUPPORT = create("server_support", ServerSupportPayload.CODEC) 15 | //$$ val SEND_TRANSCRIPT_TO_CLIENT = create("send_transcript_client", SendTranscriptToClientPayload.CODEC) 16 | //$$ val SEND_TRANSCRIPT_TO_SERVER = create("send_transcript_server", SendTranscriptToServerPayload.CODEC) 17 | //$$ val SET_USED_LANGUAGES = create("set_used_languages", SetUsedLanguagesPayload.CODEC) 18 | //$$ val MARK_INCOMPLETE = create("mark_incomplete", MarkIncompletePayload.CODEC) 19 | //$$ val TRANSLATE_SIGN = create("translate_sign", TranslateSignPayload.CODEC) 20 | //$$ val SET_CURRENT_LANGUAGE = create("set_current_language", SetCurrentLanguagePayload.CODEC) 21 | //$$ 22 | //$$ fun init() { 23 | //$$ } 24 | //$$ 25 | //$$ private fun create(id: String, codec: StreamCodec): TypeAndCodec { 26 | //$$ return TypeAndCodec(CustomPacketPayload.Type(UnityTranslate.id(id)), codec) 27 | //$$ } 28 | //#else 29 | val SERVER_SUPPORT = UnityTranslate.id("server_support") 30 | val SEND_TRANSCRIPT = UnityTranslate.id("send_transcript") 31 | val SET_USED_LANGUAGES = UnityTranslate.id("set_used_languages") 32 | val MARK_INCOMPLETE = UnityTranslate.id("mark_incomplete") 33 | val TRANSLATE_SIGN = UnityTranslate.id("translate_sign") 34 | val SET_CURRENT_LANGUAGE = UnityTranslate.id("set_current_language") 35 | //#endif 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/network/UTClientNetworking.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.network 2 | 3 | //#if MC >= 1.20.6 4 | //$$ import net.minecraft.network.RegistryFriendlyByteBuf 5 | //$$ import net.minecraft.network.protocol.common.custom.CustomPacketPayload 6 | //$$ import net.minecraft.network.protocol.common.custom.CustomPacketPayload.TypeAndCodec 7 | //#endif 8 | //#if MC >= 1.20.6 9 | //$$ import xyz.bluspring.unitytranslate.network.payloads.SetCurrentLanguagePayload 10 | //$$ import xyz.bluspring.unitytranslate.network.payloads.SetUsedLanguagesPayload 11 | //#endif 12 | import dev.architectury.event.events.client.ClientPlayerEvent 13 | import dev.architectury.networking.NetworkManager 14 | import net.minecraft.client.Minecraft 15 | import xyz.bluspring.unitytranslate.Language 16 | import xyz.bluspring.unitytranslate.UnityTranslate 17 | import xyz.bluspring.unitytranslate.client.UnityTranslateClient 18 | import xyz.bluspring.unitytranslate.compat.voicechat.UTVoiceChatCompat 19 | import xyz.bluspring.unitytranslate.events.TranscriptEvents 20 | import xyz.bluspring.unitytranslate.transcript.Transcript 21 | import java.util.* 22 | 23 | object UTClientNetworking { 24 | fun init() { 25 | val proxy = UnityTranslate.instance.proxy 26 | 27 | //#if MC >= 1.20.6 28 | //$$ registerReceiver(PacketIds.SERVER_SUPPORT) { buf, ctx -> 29 | //#else 30 | NetworkManager.registerReceiver(NetworkManager.Side.S2C, PacketIds.SERVER_SUPPORT) { buf, ctx -> 31 | //#endif 32 | UnityTranslateClient.connectedServerHasSupport = true 33 | } 34 | 35 | ClientPlayerEvent.CLIENT_PLAYER_JOIN.register { player -> 36 | Minecraft.getInstance().execute { 37 | //#if MC >= 1.20.6 38 | //$$ proxy.sendPacketClient(SetUsedLanguagesPayload(UnityTranslateClient.languageBoxes.map { it.language })) 39 | //#else 40 | if (UnityTranslateClient.languageBoxes.isEmpty()) 41 | return@execute 42 | 43 | val buf = proxy.createByteBuf() 44 | buf.writeEnumSet(EnumSet.copyOf(UnityTranslateClient.languageBoxes.map { it.language }), Language::class.java) 45 | 46 | proxy.sendPacketClient(PacketIds.SET_USED_LANGUAGES, buf) 47 | //#endif 48 | } 49 | 50 | Minecraft.getInstance().execute { 51 | //#if MC >= 1.20.6 52 | //$$ proxy.sendPacketClient(SetCurrentLanguagePayload(UnityTranslate.config.client.language)) 53 | //#else 54 | val buf = proxy.createByteBuf() 55 | buf.writeEnum(UnityTranslate.config.client.language) 56 | 57 | proxy.sendPacketClient(PacketIds.SET_CURRENT_LANGUAGE, buf) 58 | //#endif 59 | } 60 | } 61 | 62 | ClientPlayerEvent.CLIENT_PLAYER_QUIT.register { _ -> 63 | UnityTranslateClient.connectedServerHasSupport = false 64 | } 65 | 66 | //#if MC >= 1.20.6 67 | //$$ registerReceiver(PacketIds.SEND_TRANSCRIPT_TO_CLIENT) { buf, ctx -> 68 | //$$ val sourceId = buf.uuid 69 | //#else 70 | NetworkManager.registerReceiver(NetworkManager.Side.S2C, PacketIds.SEND_TRANSCRIPT) { buf, ctx -> 71 | val sourceId = buf.readUUID() 72 | //#endif 73 | val source = ctx.player.level().getPlayerByUUID(sourceId) ?: return@registerReceiver 74 | 75 | //#if MC >= 1.20.6 76 | //$$ val sourceLanguage = buf.language 77 | //$$ val index = buf.index 78 | //$$ val updateTime = buf.updateTime 79 | //#else 80 | val sourceLanguage = buf.readEnum(Language::class.java) 81 | val index = buf.readVarInt() 82 | val updateTime = buf.readVarLong() 83 | 84 | val totalLanguages = buf.readVarInt() 85 | //#endif 86 | 87 | val boxes = UnityTranslateClient.languageBoxes 88 | 89 | //#if MC >= 1.20.6 90 | //$$ for ((language, text) in buf.toSend) { 91 | //#else 92 | for (i in 0 until totalLanguages) { 93 | val language = buf.readEnum(Language::class.java) 94 | val text = buf.readUtf() 95 | //#endif 96 | 97 | if (language == UnityTranslateClient.transcriber.language && sourceId == ctx.player?.uuid) 98 | continue 99 | 100 | val box = boxes.firstOrNull { it.language == language } 101 | box?.updateTranscript(source, text, sourceLanguage, index, updateTime, false) 102 | 103 | if (box == null && UTVoiceChatCompat.isPlayerAudible(ctx.player)) { 104 | TranscriptEvents.UPDATE.invoker().onTranscriptUpdate(Transcript(index, source, text, language, updateTime, false), language) 105 | } 106 | } 107 | } 108 | 109 | //#if MC >= 1.20.6 110 | //$$ registerReceiver(PacketIds.MARK_INCOMPLETE) { buf, ctx -> 111 | //$$ val from = buf.from 112 | //$$ val to = buf.to 113 | //$$ val uuid = buf.uuid 114 | //$$ val index = buf.index 115 | //$$ val isIncomplete = buf.isIncomplete 116 | //#else 117 | NetworkManager.registerReceiver(NetworkManager.Side.S2C, PacketIds.MARK_INCOMPLETE) { buf, ctx -> 118 | val from = buf.readEnum(Language::class.java) 119 | val to = buf.readEnum(Language::class.java) 120 | val uuid = buf.readUUID() 121 | val index = buf.readVarInt() 122 | val isIncomplete = buf.readBoolean() 123 | //#endif 124 | 125 | val box = UnityTranslateClient.languageBoxes.firstOrNull { it.language == to } ?: return@registerReceiver 126 | box.transcripts.firstOrNull { it.language == from && it.player.uuid == uuid && it.index == index }?.incomplete = isIncomplete 127 | } 128 | } 129 | 130 | //#if MC >= 1.20.6 131 | //$$ private fun registerReceiver(type: TypeAndCodec, receiver: NetworkManager.NetworkReceiver) { 132 | //$$ NetworkManager.registerReceiver(NetworkManager.Side.S2C, type.type, type.codec, receiver) 133 | //$$ } 134 | //#endif 135 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/network/UTServerNetworking.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.network 2 | 3 | //#if MC >= 1.20.6 4 | //$$ import net.minecraft.network.RegistryFriendlyByteBuf 5 | //$$ import net.minecraft.network.protocol.common.custom.CustomPacketPayload 6 | //$$ import net.minecraft.network.protocol.common.custom.CustomPacketPayload.TypeAndCodec 7 | //$$ import xyz.bluspring.unitytranslate.network.payloads.MarkIncompletePayload 8 | //$$ import xyz.bluspring.unitytranslate.network.payloads.SendTranscriptToClientPayload 9 | //$$ import xyz.bluspring.unitytranslate.network.payloads.ServerSupportPayload 10 | //#endif 11 | import dev.architectury.event.events.common.PlayerEvent 12 | import dev.architectury.networking.NetworkManager 13 | import net.minecraft.ChatFormatting 14 | import net.minecraft.network.FriendlyByteBuf 15 | import net.minecraft.network.chat.Component 16 | import net.minecraft.server.level.ServerPlayer 17 | import net.minecraft.world.entity.player.Player 18 | import net.minecraft.world.level.block.SignBlock 19 | import net.minecraft.world.level.block.entity.SignBlockEntity 20 | import xyz.bluspring.unitytranslate.Language 21 | import xyz.bluspring.unitytranslate.UnityTranslate 22 | import xyz.bluspring.unitytranslate.UnityTranslate.Companion.hasVoiceChat 23 | import xyz.bluspring.unitytranslate.compat.voicechat.UTVoiceChatCompat 24 | import xyz.bluspring.unitytranslate.translator.TranslatorManager 25 | import java.util.* 26 | import java.util.concurrent.CompletableFuture 27 | import java.util.concurrent.ConcurrentHashMap 28 | import java.util.concurrent.ConcurrentLinkedDeque 29 | 30 | object UTServerNetworking { 31 | val proxy = UnityTranslate.instance.proxy 32 | val playerLanguages = ConcurrentHashMap() 33 | 34 | fun init() { 35 | //#if MC >= 1.20.6 36 | //$$ PacketIds.init() 37 | //#endif 38 | 39 | val usedLanguages = ConcurrentHashMap>() 40 | 41 | //#if MC >= 1.20.6 42 | //$$ registerReceiver(PacketIds.SET_USED_LANGUAGES) { buf, ctx -> 43 | //$$ val languages = EnumSet.copyOf(buf.languages) 44 | //#else 45 | NetworkManager.registerReceiver(NetworkManager.Side.C2S, PacketIds.SET_USED_LANGUAGES) { buf, ctx -> 46 | val languages = buf.readEnumSet(Language::class.java) 47 | //#endif 48 | usedLanguages[ctx.player.uuid] = languages 49 | } 50 | 51 | //#if MC >= 1.20.6 52 | //$$ registerReceiver(PacketIds.SEND_TRANSCRIPT_TO_SERVER) { buf, ctx -> 53 | //$$ val sourceLanguage = buf.sourceLanguage 54 | //$$ val text = buf.text 55 | //$$ val index = buf.index 56 | //$$ val updateTime = buf.updateTime 57 | //#else 58 | NetworkManager.registerReceiver(NetworkManager.Side.C2S, PacketIds.SEND_TRANSCRIPT) { buf, ctx -> 59 | val sourceLanguage = buf.readEnum(Language::class.java) 60 | val text = buf.readUtf() 61 | val index = buf.readVarInt() 62 | val updateTime = buf.readVarLong() 63 | //#endif 64 | 65 | if (!canPlayerRequestTranslations(ctx.player)) 66 | return@registerReceiver 67 | 68 | // TODO: probably make this better 69 | if (text.length > 1500) { 70 | ctx.player.displayClientMessage(Component.literal("Transcription too long! Current transcript discarded.").withStyle(ChatFormatting.RED), true) 71 | //#if MC >= 1.20.6 72 | //$$ proxy.sendPacketServer(ctx.player as ServerPlayer, MarkIncompletePayload(sourceLanguage, sourceLanguage, ctx.player.uuid, index, true)) 73 | //#else 74 | val markBuf = proxy.createByteBuf() 75 | markBuf.writeEnum(sourceLanguage) 76 | markBuf.writeEnum(sourceLanguage) 77 | markBuf.writeUUID(ctx.player.uuid) 78 | markBuf.writeVarInt(index) 79 | markBuf.writeBoolean(true) 80 | 81 | proxy.sendPacketServer(ctx.player as ServerPlayer, PacketIds.MARK_INCOMPLETE, markBuf) 82 | //#endif 83 | return@registerReceiver 84 | } 85 | 86 | val translations = ConcurrentHashMap() 87 | val translationsToSend = ConcurrentLinkedDeque() 88 | 89 | Language.entries.filter { usedLanguages.values.any { b -> b.contains(it) } }.map { 90 | Pair(it, if (sourceLanguage == it) 91 | CompletableFuture.supplyAsync { 92 | text 93 | } 94 | else 95 | TranslatorManager.queueTranslation(text, sourceLanguage, it, ctx.player, index)) 96 | } 97 | .forEach { (language, future) -> 98 | future.whenCompleteAsync { translated, e -> 99 | if (e != null) { 100 | //e.printStackTrace() 101 | return@whenCompleteAsync 102 | } 103 | 104 | translations[language] = translated 105 | translationsToSend.add(language) 106 | 107 | ctx.queue { 108 | if (translationsToSend.isNotEmpty()) { 109 | broadcastTranslations(ctx.player as ServerPlayer, sourceLanguage, index, updateTime, translationsToSend, translations) 110 | translationsToSend.clear() 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | //#if MC >= 1.20.6 118 | //$$ registerReceiver(PacketIds.SET_CURRENT_LANGUAGE) { buf, ctx -> 119 | //$$ val language = buf.language 120 | //#else 121 | NetworkManager.registerReceiver(NetworkManager.Side.C2S, PacketIds.SET_CURRENT_LANGUAGE) { buf, ctx -> 122 | val language = buf.readEnum(Language::class.java) 123 | //#endif 124 | val player = ctx.player 125 | 126 | playerLanguages[player.uuid] = language 127 | } 128 | 129 | //#if MC >= 1.20.6 130 | //$$ registerReceiver(PacketIds.TRANSLATE_SIGN) { buf, ctx -> 131 | //$$ val pos = buf.pos 132 | //#else 133 | NetworkManager.registerReceiver(NetworkManager.Side.C2S, PacketIds.TRANSLATE_SIGN) { buf, ctx -> 134 | val pos = buf.readBlockPos() 135 | //#endif 136 | 137 | if (!canPlayerRequestTranslations(ctx.player)) 138 | return@registerReceiver 139 | 140 | val player = ctx.player 141 | val level = player.level() 142 | 143 | val state = level.getBlockState(pos) 144 | 145 | if (state.block !is SignBlock) 146 | return@registerReceiver 147 | 148 | ctx.queue { 149 | val entity = level.getBlockEntity(pos) 150 | 151 | if (entity !is SignBlockEntity) 152 | return@queue 153 | 154 | val text = (if (entity.isFacingFrontText(player)) entity.frontText else entity.backText) 155 | .getMessages(false) 156 | .joinToString("\n") { it.string } 157 | 158 | val language = TranslatorManager.detectLanguage(text) ?: Language.ENGLISH 159 | val toLang = this.playerLanguages.getOrElse(player.uuid) { Language.ENGLISH } 160 | 161 | player.displayClientMessage(Component.empty() 162 | .append(Component.literal("[UnityTranslate]: ").withStyle(ChatFormatting.YELLOW, ChatFormatting.BOLD)) 163 | .append(Component.translatable("unitytranslate.transcribe_sign", language.text, toLang.text, TranslatorManager.translateLine(text, language, toLang))), 164 | false 165 | ) 166 | } 167 | } 168 | 169 | PlayerEvent.PLAYER_JOIN.register { player -> 170 | proxy.sendPacketServer(player, 171 | //#if MC >= 1.20.6 172 | //$$ ServerSupportPayload.EMPTY 173 | //#else 174 | PacketIds.SERVER_SUPPORT, proxy.createByteBuf() 175 | //#endif 176 | ) 177 | } 178 | 179 | PlayerEvent.PLAYER_QUIT.register { player -> 180 | usedLanguages.remove(player.uuid) 181 | } 182 | } 183 | 184 | //#if MC <= 1.20.4 185 | // turns out, Forge requires us to rebuild the buffer every time we send it to a player, 186 | // so unfortunately, we cannot reuse the buffer. 187 | private fun buildBroadcastPacket(source: ServerPlayer, sourceLanguage: Language, index: Int, updateTime: Long, toSend: Map): FriendlyByteBuf { 188 | val buf = proxy.createByteBuf() 189 | buf.writeUUID(source.uuid) 190 | buf.writeEnum(sourceLanguage) 191 | buf.writeVarInt(index) 192 | buf.writeVarLong(updateTime) 193 | 194 | buf.writeVarInt(toSend.size) 195 | 196 | for ((language, translated) in toSend) { 197 | buf.writeEnum(language) 198 | buf.writeUtf(translated) 199 | } 200 | 201 | return buf 202 | } 203 | //#endif 204 | 205 | private fun broadcastTranslations(source: ServerPlayer, sourceLanguage: Language, index: Int, updateTime: Long, translationsToSend: ConcurrentLinkedDeque, translations: ConcurrentHashMap) { 206 | val toSend = translations.filter { a -> translationsToSend.contains(a.key) } 207 | 208 | if (hasVoiceChat) { 209 | val nearby = UTVoiceChatCompat.getNearbyPlayers(source) 210 | 211 | for (player in nearby) { 212 | if (UTVoiceChatCompat.isPlayerDeafened(player) && player != source) 213 | continue 214 | 215 | //#if MC >= 1.20.6 216 | //$$ proxy.sendPacketServer(player, SendTranscriptToClientPayload(source.uuid, sourceLanguage, index, updateTime, toSend)) 217 | //#else 218 | val buf = buildBroadcastPacket(source, sourceLanguage, index, updateTime, toSend) 219 | proxy.sendPacketServer(player, PacketIds.SEND_TRANSCRIPT, buf) 220 | //#endif 221 | } 222 | } else { 223 | //#if MC >= 1.20.6 224 | //$$ proxy.sendPacketServer(source, SendTranscriptToClientPayload(source.uuid, sourceLanguage, index, updateTime, toSend)) 225 | //#else 226 | val buf = buildBroadcastPacket(source, sourceLanguage, index, updateTime, toSend) 227 | proxy.sendPacketServer(source, PacketIds.SEND_TRANSCRIPT, buf) 228 | //#endif 229 | } 230 | } 231 | 232 | fun canPlayerRequestTranslations(player: Player): Boolean { 233 | return UnityTranslate.instance.proxy.hasTranscriptPermission(player) 234 | } 235 | 236 | //#if MC >= 1.20.6 237 | //$$ private fun registerReceiver(type: TypeAndCodec, receiver: NetworkManager.NetworkReceiver) { 238 | //$$ NetworkManager.registerReceiver(NetworkManager.Side.C2S, type.type, type.codec, receiver) 239 | //$$ } 240 | //#endif 241 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/transcript/Transcript.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.transcript 2 | 3 | import net.minecraft.world.entity.player.Player 4 | import xyz.bluspring.unitytranslate.Language 5 | 6 | data class Transcript( 7 | val index: Int, 8 | val player: Player, 9 | var text: String, 10 | val language: Language, 11 | var lastUpdateTime: Long, 12 | var incomplete: Boolean, 13 | 14 | var arrivalTime: Long = System.currentTimeMillis() 15 | ) 16 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/translator/LibreTranslateInstance.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.translator 2 | 3 | import com.google.common.collect.HashMultimap 4 | import com.google.common.collect.Multimap 5 | import com.google.gson.JsonArray 6 | import com.google.gson.JsonObject 7 | import com.google.gson.JsonParser 8 | import net.minecraft.util.random.Weight 9 | import net.minecraft.util.random.WeightedEntry 10 | import xyz.bluspring.unitytranslate.Language 11 | import xyz.bluspring.unitytranslate.UnityTranslate 12 | import xyz.bluspring.unitytranslate.util.Flags 13 | import xyz.bluspring.unitytranslate.util.HttpHelper 14 | import java.net.URL 15 | 16 | open class LibreTranslateInstance(val url: String, private var weight: Int, val authKey: String? = null) : WeightedEntry { 17 | private var cachedSupportedLanguages = HashMultimap.create() 18 | var latency: Int = -1 19 | private set 20 | 21 | var currentlyTranslating = 0 22 | 23 | init { 24 | val startTime = System.currentTimeMillis() 25 | if (this.translate("Latency test for UnityTranslate", Language.ENGLISH, Language.SPANISH) == null) 26 | throw Exception("Failed to run latency test for LibreTranslate instance $url!") 27 | latency = (System.currentTimeMillis() - startTime).toInt() 28 | } 29 | 30 | val supportedLanguages: Multimap 31 | get() { 32 | if (cachedSupportedLanguages.isEmpty) { 33 | val array = JsonParser.parseString(URL("$url/languages").readText()).asJsonArray 34 | 35 | for (element in array) { 36 | val langData = element.asJsonObject 37 | val srcLang = Language.findLibreLang(langData.get("code").asString) ?: continue 38 | 39 | val targets = langData.getAsJsonArray("targets") 40 | for (target in targets) { 41 | val targetLang = Language.findLibreLang(target.asString) ?: continue 42 | cachedSupportedLanguages.put(srcLang, targetLang) 43 | } 44 | } 45 | } 46 | 47 | return cachedSupportedLanguages 48 | } 49 | 50 | fun supportsLanguage(from: Language, to: Language): Boolean { 51 | if (!supportedLanguages.containsKey(from)) { 52 | return false 53 | } 54 | 55 | val supportedTargets = supportedLanguages.get(from) 56 | 57 | return supportedTargets.contains(to) 58 | } 59 | 60 | fun batchTranslate(texts: List, from: Language, to: Language): List? { 61 | if (!supportsLanguage(from, to)) 62 | return null 63 | 64 | return try { 65 | batchTranslate(from.code, to.code, texts) 66 | } catch (e: Exception) { 67 | if (SHOULD_PRINT_ERRORS) 68 | e.printStackTrace() 69 | 70 | null 71 | } 72 | } 73 | 74 | open fun detectLanguage(text: String): Language? { 75 | val detected = HttpHelper.post("$url/detect", JsonObject().apply { 76 | addProperty("q", text) 77 | 78 | if (authKey?.isNotBlank() == true) 79 | addProperty("api_key", authKey) 80 | }).asJsonArray.sortedByDescending { it.asJsonObject.get("confidence").asDouble } 81 | 82 | val langCode = detected.firstOrNull()?.asJsonObject?.get("language")?.asString ?: return null 83 | val lang = Language.findLibreLang(langCode) 84 | 85 | if (lang == null) { 86 | UnityTranslate.logger.error("Failed to find language for LibreTranslate code $langCode!") 87 | } 88 | 89 | return lang 90 | } 91 | 92 | open fun batchTranslate(from: String, to: String, request: List): List { 93 | val translated = HttpHelper.post("$url/translate", JsonObject().apply { 94 | addProperty("source", from) 95 | addProperty("target", to) 96 | add("q", JsonArray().apply { 97 | for (s in request) { 98 | this.add(s) 99 | } 100 | }) 101 | 102 | if (authKey?.isNotBlank() == true) 103 | addProperty("api_key", authKey) 104 | }).asJsonObject.get("translatedText").asJsonArray 105 | 106 | return translated.map { it.asString } 107 | } 108 | 109 | fun translate(text: String, from: Language, to: Language): String? { 110 | if (!supportsLanguage(from, to)) 111 | return null 112 | 113 | return try { 114 | translate(from.code, to.code, text) 115 | } catch (e: Exception) { 116 | if (SHOULD_PRINT_ERRORS) 117 | e.printStackTrace() 118 | 119 | null 120 | } 121 | } 122 | 123 | open fun translate(from: String, to: String, request: String): String { 124 | return HttpHelper.post("$url/translate", JsonObject().apply { 125 | addProperty("source", from) 126 | addProperty("target", to) 127 | addProperty("q", request) 128 | addProperty("format", "text") 129 | 130 | if (authKey?.isNotBlank() == true) 131 | addProperty("api_key", authKey) 132 | }) 133 | .asJsonObject.get("translatedText").asString 134 | } 135 | 136 | override fun getWeight(): Weight { 137 | return Weight.of(weight) 138 | } 139 | 140 | companion object { 141 | const val MAX_CONCURRENT_TRANSLATIONS = 15 142 | val SHOULD_PRINT_ERRORS = Flags.PRINT_HTTP_ERRORS || UnityTranslate.instance.proxy.isDev 143 | } 144 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/translator/LocalLibreTranslateInstance.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.translator 2 | 3 | import dev.architectury.event.events.client.ClientLifecycleEvent 4 | import dev.architectury.event.events.common.LifecycleEvent 5 | import net.minecraft.Util 6 | import net.minecraft.network.chat.Component 7 | import net.minecraft.util.HttpUtil 8 | import net.minecraft.util.Mth 9 | import oshi.SystemInfo 10 | import xyz.bluspring.unitytranslate.UnityTranslate 11 | import xyz.bluspring.unitytranslate.client.UnityTranslateClient 12 | import xyz.bluspring.unitytranslate.util.Flags 13 | import java.io.File 14 | import java.net.URL 15 | import java.util.* 16 | import java.util.concurrent.CompletableFuture 17 | import java.util.function.Consumer 18 | import java.util.zip.ZipFile 19 | 20 | class LocalLibreTranslateInstance protected constructor(val process: Process, val port: Int) : LibreTranslateInstance("http://127.0.0.1:$port", 150) { 21 | init { 22 | info("Started local LibreTranslate instance on port $port.") 23 | 24 | LifecycleEvent.SERVER_STOPPING.register { 25 | process.destroy() 26 | } 27 | 28 | if (UnityTranslate.instance.proxy.isClient()) 29 | registerEventsClient() 30 | } 31 | 32 | private fun registerEventsClient() { 33 | ClientLifecycleEvent.CLIENT_STOPPING.register { 34 | process.destroy() 35 | } 36 | } 37 | 38 | companion object { 39 | const val DOWNLOAD_URL = "https://nightly.link/BluSpring/LibreTranslate/workflows/build/main/{PLATFORM}%20Artifacts%20%28{TYPE}%29.zip?completed=true" 40 | private var lastPid = -1L 41 | var hasStarted = false 42 | var currentInstance: LocalLibreTranslateInstance? = null 43 | 44 | val unityTranslateDir = File(UnityTranslate.instance.proxy.gameDir.toFile(), ".unitytranslate") 45 | 46 | fun canRunLibreTranslate(): Boolean { 47 | val systemInfo = SystemInfo() 48 | 49 | return (Runtime.getRuntime().availableProcessors() >= 2 || TranslatorManager.checkSupportsCuda()) && 50 | // Require a minimum of 2 GiB free for LibreTranslate 51 | ((systemInfo.hardware.memory.total - Runtime.getRuntime().maxMemory()) / 1048576L) >= 2048 52 | } 53 | 54 | // TODO: make translatable 55 | private fun warn(text: String) { 56 | if (UnityTranslate.instance.proxy.isClient()) { 57 | UnityTranslateClient.displayMessage(Component.literal(text), true) 58 | } else { 59 | UnityTranslate.logger.warn(text) 60 | } 61 | } 62 | 63 | private fun info(text: String) { 64 | if (UnityTranslate.instance.proxy.isClient()) { 65 | UnityTranslateClient.displayMessage(Component.literal(text), false) 66 | } else { 67 | UnityTranslate.logger.info(text) 68 | } 69 | } 70 | 71 | fun killOpenInstances() { 72 | if (lastPid == -1L) 73 | return 74 | 75 | ProcessHandle.of(lastPid) 76 | .ifPresent { 77 | info("Detected LibreTranslate instance ${lastPid}, killing.") 78 | it.destroyForcibly() 79 | 80 | lastPid = -1L 81 | } 82 | 83 | currentInstance = null 84 | } 85 | 86 | private fun clearDeadDirectories() { 87 | val files = unityTranslateDir.listFiles() 88 | 89 | if (files != null) { 90 | for (file in files) { 91 | if (!file.isDirectory) 92 | continue 93 | 94 | if (file.name.startsWith("_MEI")) { 95 | if (!file.deleteRecursively()) { 96 | warn("Failed to delete unused LibreTranslate directories, this may mean a dead LibreTranslate instance is running on your computer!") 97 | warn("Please try to terminate any \"libretranslate.exe\" processes that you see running, then restart your game.") 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | fun launchLibreTranslate(source: File, consumer: Consumer) { 105 | val port = if (HttpUtil.isPortAvailable(5000)) 5000 else HttpUtil.getAvailablePort() 106 | 107 | if (lastPid != -1L) { 108 | killOpenInstances() 109 | } 110 | 111 | clearDeadDirectories() 112 | 113 | if (!source.setExecutable(true, false) && !source.canExecute()) { 114 | UnityTranslate.logger.error("Unable to start local LibreTranslate instance! You may have to manually set the execute permission on the file yourself!") 115 | UnityTranslate.logger.error("File path: ${source.absolutePath}") 116 | return 117 | } 118 | 119 | val processBuilder = ProcessBuilder(listOf( 120 | source.absolutePath, 121 | "--update-models", 122 | "--port", 123 | "$port", 124 | "--threads", 125 | "${Mth.clamp(UnityTranslate.config.server.libreTranslateThreads, 1, Runtime.getRuntime().availableProcessors())}", 126 | "--disable-web-ui", 127 | "--disable-files-translation" 128 | )) 129 | 130 | processBuilder.directory(unityTranslateDir) 131 | 132 | val environment = processBuilder.environment() 133 | environment["PYTHONIOENCODING"] = "utf-8" 134 | environment["PYTHONLEGACYWINDOWSSTDIO"] = "utf-8" 135 | 136 | if (UnityTranslate.instance.proxy.isDev || Flags.ENABLE_LOGGING) { 137 | processBuilder 138 | .redirectOutput(ProcessBuilder.Redirect.INHERIT) 139 | .redirectError(ProcessBuilder.Redirect.INHERIT) 140 | } 141 | 142 | val process = processBuilder.start() 143 | lastPid = process.pid() 144 | 145 | info("LibreTranslate is starting... (PID: ${process.pid()})") 146 | 147 | val timer = Timer() 148 | 149 | process.onExit() 150 | .whenCompleteAsync { process, e -> 151 | e?.printStackTrace() 152 | 153 | if (!hasStarted) { 154 | timer.cancel() 155 | warn("LibreTranslate appears to have exited with code ${process.exitValue()}, not proceeding with local translator instance.") 156 | UnityTranslate.logger.error(process.inputReader(Charsets.UTF_8).readText()) 157 | UnityTranslate.logger.error(process.errorReader(Charsets.UTF_8).readText()) 158 | } 159 | 160 | hasStarted = false 161 | currentInstance = null 162 | } 163 | 164 | var attempts = 0 165 | timer.scheduleAtFixedRate(object : TimerTask() { 166 | override fun run() { 167 | try { 168 | val instance = LocalLibreTranslateInstance(process, port) 169 | 170 | currentInstance = instance 171 | consumer.accept(instance) 172 | 173 | timer.cancel() 174 | hasStarted = true 175 | } catch (_: Exception) { 176 | } 177 | } 178 | }, 2500L, 2500L) 179 | } 180 | 181 | fun isLibreTranslateInstalled(): Boolean { 182 | val supportsCuda = TranslatorManager.checkSupportsCuda() 183 | val platform = Util.getPlatform() 184 | 185 | if (platform != Util.OS.WINDOWS && platform != Util.OS.OSX && platform != Util.OS.LINUX) { 186 | return false 187 | } 188 | 189 | val file = File(unityTranslateDir, "libretranslate/libretranslate${if (supportsCuda) "_cuda" else ""}${if (platform == Util.OS.WINDOWS) ".exe" else ""}") 190 | return file.exists() 191 | } 192 | 193 | fun installLibreTranslate(): CompletableFuture { 194 | val supportsCuda = TranslatorManager.checkSupportsCuda() 195 | val platform = Util.getPlatform() 196 | 197 | if (platform != Util.OS.WINDOWS && platform != Util.OS.OSX && platform != Util.OS.LINUX) { 198 | throw IllegalStateException("Unsupported platform! (Detected platform: $platform)") 199 | } 200 | 201 | val file = File(unityTranslateDir, "libretranslate/libretranslate${if (supportsCuda) "_cuda" else ""}${if (platform == Util.OS.WINDOWS) ".exe" else ""}") 202 | if (!file.parentFile.exists()) 203 | file.parentFile.mkdirs() 204 | 205 | return CompletableFuture.supplyAsync { 206 | if (!file.exists()) { 207 | if (file.parentFile.usableSpace <= 15L * 1024L * 1024L * 1024L) { 208 | warn("Current drive doesn't have enough space for local LibreTranslate instance! Not installing LibreTranslate.") 209 | throw IndexOutOfBoundsException() 210 | } 211 | 212 | info("Downloading LibreTranslate instance for platform ${platform.name} (CUDA: $supportsCuda)") 213 | 214 | val download = URL(DOWNLOAD_URL 215 | .replace("{PLATFORM}", when (platform) { 216 | Util.OS.WINDOWS -> "Windows" 217 | Util.OS.OSX -> "MacOS" 218 | Util.OS.LINUX -> "Linux" 219 | else -> "" 220 | }) 221 | .replace("{TYPE}", if (supportsCuda) "CUDA" else "CPU") 222 | ) 223 | 224 | val archive = File(file.parentFile.parentFile, "LibreTranslate_temp.zip") 225 | 226 | if (archive.exists()) { 227 | if (file.parentFile.exists()) { 228 | file.parentFile.delete() 229 | } 230 | 231 | archive.delete() 232 | } 233 | 234 | archive.deleteOnExit() 235 | val fileStream = archive.outputStream() 236 | val downloadStream = download.openStream() 237 | downloadStream.transferTo(fileStream) 238 | 239 | fileStream.close() 240 | downloadStream.close() 241 | 242 | info("Extracting LibreTranslate instance...") 243 | 244 | val zip = ZipFile(archive) 245 | for (entry in zip.entries()) { 246 | val extracted = File(file.parentFile, entry.name) 247 | if (entry.isDirectory) 248 | extracted.mkdirs() 249 | else { 250 | extracted.parentFile.mkdirs() 251 | 252 | val stream = zip.getInputStream(entry) 253 | val extractStream = extracted.outputStream() 254 | 255 | stream.transferTo(extractStream) 256 | extractStream.close() 257 | stream.close() 258 | } 259 | } 260 | 261 | zip.close() 262 | 263 | info("Deleting temporary file...") 264 | archive.delete() 265 | 266 | info("LibreTranslate instance successfully installed!") 267 | } 268 | 269 | file 270 | } 271 | } 272 | } 273 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/translator/NativeLocalLibreTranslateInstance.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.translator 2 | 3 | class NativeLocalLibreTranslateInstance { 4 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/translator/Translation.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.translator 2 | 3 | import net.minecraft.world.entity.player.Player 4 | import xyz.bluspring.unitytranslate.Language 5 | import java.util.concurrent.CompletableFuture 6 | 7 | data class Translation( 8 | val id: String, // follows "playerID-transcriptIndex" 9 | val text: String, 10 | val fromLang: Language, 11 | val toLang: Language, 12 | val queueTime: Long, 13 | val future: CompletableFuture, 14 | val player: Player, 15 | val index: Int 16 | ) { 17 | var attempts = 0 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/util/ClassLoaderProviderForkJoinWorkerThreadFactory.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.util 2 | 3 | import java.util.concurrent.ForkJoinPool 4 | import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory 5 | import java.util.concurrent.ForkJoinWorkerThread 6 | 7 | // i think this might be the longest class name i've ever written. 8 | // For context as to why this is needed - https://stackoverflow.com/a/57551188 9 | // and why this? because Forge. that's why. 10 | // it's always Forge. 11 | class ClassLoaderProviderForkJoinWorkerThreadFactory(private val classLoader: ClassLoader) : ForkJoinWorkerThreadFactory { 12 | override fun newThread(pool: ForkJoinPool): ForkJoinWorkerThread { 13 | return ClassLoaderProviderForkJoinWorkerThread(pool, classLoader) 14 | } 15 | 16 | class ClassLoaderProviderForkJoinWorkerThread(pool: ForkJoinPool, classLoader: ClassLoader) : ForkJoinWorkerThread(pool) { 17 | init { 18 | this.contextClassLoader = classLoader 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/util/Flags.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.util 2 | 3 | object Flags { 4 | /** 5 | * Prints any translation HTTP exceptions to the log. 6 | */ 7 | val PRINT_HTTP_ERRORS = System.getProperty("unitytranslate.printHttpErrors", "false") == "true" 8 | 9 | /** 10 | * Allows LibreTranslate to pipe its console output to the log. 11 | */ 12 | val ENABLE_LOGGING = System.getProperty("unitytranslate.enableLogging", "false") == "true" 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/util/HttpHelper.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.util 2 | 3 | import com.google.gson.JsonElement 4 | import com.google.gson.JsonObject 5 | import com.google.gson.JsonParser 6 | import okhttp3.MediaType.Companion.toMediaType 7 | import okhttp3.OkHttpClient 8 | import okhttp3.Request 9 | import okhttp3.RequestBody.Companion.toRequestBody 10 | 11 | object HttpHelper { 12 | private val client = OkHttpClient() 13 | 14 | fun post(uri: String, body: JsonObject, headers: Map = mapOf()): JsonElement { 15 | val request = Request.Builder().apply { 16 | url(uri) 17 | post(body.toString().toRequestBody("application/json; charset=utf-8".toMediaType())) 18 | header("Accept", "application/json; charset=utf-8") 19 | header("Content-Type", "application/json; charset=utf-8") 20 | charset("UTF-8") 21 | 22 | for ((key, value) in headers) { 23 | header(key, value) 24 | } 25 | } 26 | .build() 27 | 28 | client.newCall(request).execute().use { response -> 29 | if (!response.isSuccessful) { 30 | throw Exception("Failed to load $uri (code: ${response.code})") 31 | } 32 | 33 | (response.body?.charStream() ?: throw Exception("Failed to load $uri (code: ${response.code})")).use { reader -> 34 | return JsonParser.parseReader(reader) 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/util/nativeaccess/CudaAccess.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.util.nativeaccess 2 | 3 | import net.minecraft.Util 4 | import org.jetbrains.annotations.ApiStatus.Internal 5 | import org.lwjgl.system.APIUtil 6 | import org.lwjgl.system.JNI 7 | import org.lwjgl.system.MemoryUtil 8 | import org.lwjgl.system.SharedLibrary 9 | import xyz.bluspring.unitytranslate.UnityTranslate 10 | 11 | /** 12 | * This class should NEVER be accessed by normal means, only by using the NativeAccess or the LwjglLoader objects. 13 | */ 14 | @Internal 15 | internal object CudaAccess { 16 | private var isLibraryLoaded = false 17 | private lateinit var library: SharedLibrary 18 | private var PFN_cuInit: Long = 0L 19 | private var PFN_cuDeviceGetCount: Long = 0L 20 | private var PFN_cuDeviceComputeCapability: Long = 0L 21 | 22 | private var PFN_cuGetErrorName: Long = 0L 23 | private var PFN_cuGetErrorString: Long = 0L 24 | 25 | private fun logCudaError(code: Int, at: String) { 26 | if (code == 0) 27 | return 28 | 29 | // TODO: these return ??? for some reason. 30 | // can we figure out why? 31 | 32 | val errorCode = if (PFN_cuGetErrorName != MemoryUtil.NULL) { 33 | val ptr = MemoryUtil.nmemAlloc(255) 34 | JNI.callPP(code, ptr, PFN_cuGetErrorName) 35 | MemoryUtil.memUTF16(ptr).apply { 36 | MemoryUtil.nmemFree(ptr) 37 | } 38 | } else "[CUDA ERROR NAME NOT FOUND]" 39 | 40 | val errorDesc = if (PFN_cuGetErrorString != MemoryUtil.NULL) { 41 | val ptr = MemoryUtil.nmemAlloc(255) 42 | JNI.callPP(code, ptr, PFN_cuGetErrorString) 43 | MemoryUtil.memUTF16(ptr).apply { 44 | MemoryUtil.nmemFree(ptr) 45 | } 46 | } else "[CUDA ERROR DESC NOT FOUND]" 47 | 48 | UnityTranslate.logger.error("CUDA error at $at: $code $errorCode ($errorDesc)") 49 | } 50 | 51 | @JvmStatic 52 | val cudaState: CudaState = isCudaSupported() 53 | 54 | private fun isCudaSupported(): CudaState { 55 | if (!isLibraryLoaded) { 56 | try { 57 | library = if (Util.getPlatform() == Util.OS.WINDOWS) { 58 | APIUtil.apiCreateLibrary("nvcuda.dll") 59 | } else if (Util.getPlatform() == Util.OS.LINUX) { 60 | APIUtil.apiCreateLibrary("libcuda.so") 61 | } else { 62 | return CudaState.LIBRARY_UNAVAILABLE 63 | } 64 | 65 | PFN_cuInit = library.getFunctionAddress("cuInit") 66 | PFN_cuDeviceGetCount = library.getFunctionAddress("cuDeviceGetCount") 67 | PFN_cuDeviceComputeCapability = library.getFunctionAddress("cuDeviceComputeCapability") 68 | PFN_cuGetErrorName = library.getFunctionAddress("cuGetErrorName") 69 | PFN_cuGetErrorString = library.getFunctionAddress("cuGetErrorString") 70 | 71 | if (PFN_cuInit == MemoryUtil.NULL || PFN_cuDeviceGetCount == MemoryUtil.NULL || PFN_cuDeviceComputeCapability == MemoryUtil.NULL) { 72 | //UnityTranslate.logger.info("CUDA results: $PFN_cuInit $PFN_cuDeviceGetCount $PFN_cuDeviceComputeCapability") 73 | return CudaState.FUNCTION_UNAVAILABLE 74 | } 75 | } catch (_: UnsatisfiedLinkError) { 76 | UnityTranslate.logger.warn("CUDA library failed to load! Not attempting to initialize CUDA functions.") 77 | return CudaState.LIBRARY_UNAVAILABLE 78 | } catch (e: Throwable) { 79 | UnityTranslate.logger.warn("An error occurred while searching for CUDA devices! You don't have to report this, don't worry.") 80 | e.printStackTrace() 81 | return CudaState.EXCEPTION_THROWN 82 | } 83 | 84 | isLibraryLoaded = true 85 | } 86 | 87 | val success = 0 88 | 89 | if (JNI.callI(0, PFN_cuInit).apply { 90 | logCudaError(this, "init") 91 | } != success) { 92 | return CudaState.CUDA_FAILED 93 | } 94 | 95 | val totalPtr = MemoryUtil.nmemAlloc(Int.SIZE_BYTES.toLong()) 96 | if (JNI.callPI(totalPtr, PFN_cuDeviceGetCount).apply { 97 | logCudaError(this, "get device count") 98 | } != success) { 99 | return CudaState.CUDA_FAILED 100 | } 101 | 102 | val totalCudaDevices = MemoryUtil.memGetInt(totalPtr) 103 | UnityTranslate.logger.info("Total CUDA devices: $totalCudaDevices") 104 | if (totalCudaDevices <= 0) { 105 | return CudaState.NO_CUDA_DEVICES 106 | } 107 | 108 | MemoryUtil.nmemFree(totalPtr) 109 | 110 | for (i in 0 until totalCudaDevices) { 111 | val minorPtr = MemoryUtil.nmemAlloc(Int.SIZE_BYTES.toLong()) 112 | val majorPtr = MemoryUtil.nmemAlloc(Int.SIZE_BYTES.toLong()) 113 | 114 | if (JNI.callPPI(majorPtr, minorPtr, i, PFN_cuDeviceComputeCapability).apply { 115 | logCudaError(this, "get device compute capability $i") 116 | } != success) { 117 | continue 118 | } 119 | 120 | val majorVersion = MemoryUtil.memGetInt(majorPtr) 121 | val minorVersion = MemoryUtil.memGetInt(minorPtr) 122 | 123 | MemoryUtil.nmemFree(majorPtr) 124 | MemoryUtil.nmemFree(minorPtr) 125 | 126 | UnityTranslate.logger.info("Found device with CUDA compute capability major $majorVersion minor $minorVersion.") 127 | 128 | return CudaState.AVAILABLE 129 | } 130 | 131 | return CudaState.NO_CUDA_DEVICES 132 | } 133 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/util/nativeaccess/CudaState.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.util.nativeaccess 2 | 3 | enum class CudaState(val message: String? = null) { 4 | AVAILABLE, 5 | LIBRARY_UNAVAILABLE("Failed to locate CUDA library (not using NVIDIA GPU?)"), 6 | EXCEPTION_THROWN("An error occurred while trying to enumerate devices"), 7 | FUNCTION_UNAVAILABLE("Failed to locate one or more CUDA functions"), 8 | CUDA_FAILED("A CUDA error occurred in one or more positions"), 9 | NO_CUDA_DEVICES("No CUDA-supported devices were found") 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/util/nativeaccess/LwjglLoader.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.util.nativeaccess 2 | 3 | import xyz.bluspring.unitytranslate.UnityTranslate 4 | 5 | object LwjglLoader { 6 | private var isLoaded = false 7 | 8 | fun tryLoadLwjgl() { 9 | if (isLoaded) 10 | return 11 | 12 | try { 13 | Class.forName("org.lwjgl.Version") 14 | UnityTranslate.logger.info("LWJGL was detected, not loading custom-bundled LWJGL.") 15 | } catch (_: Exception) { 16 | UnityTranslate.logger.info("LWJGL was not detected, attempting to load custom-bundled LWJGL.") 17 | 18 | try { 19 | loadBundledLwjgl() 20 | } catch (e: Exception) { 21 | UnityTranslate.logger.error("Failed to load custom-bundled LWJGL!") 22 | e.printStackTrace() 23 | } 24 | } 25 | 26 | isLoaded = true 27 | } 28 | 29 | private fun loadBundledLwjgl() { 30 | val dir = "/lwjgl" 31 | 32 | /*val version = LwjglLoader::class.java.getResource("$dir/version.txt")?.readText(Charsets.UTF_8) ?: throw IllegalStateException("Missing LWJGL version data!") 33 | val fileName = "lwjgl-$version-natives-${when (Util.getPlatform()) { 34 | Util.OS.WINDOWS -> "windows" 35 | Util.OS.OSX -> "macos" 36 | Util.OS.LINUX -> "linux" 37 | else -> throw IllegalStateException("Unsupported platform ${Util.getPlatform()}!") 38 | }}.jar" 39 | val nativesJar = LwjglLoader::class.java.getResource("$dir/$fileName") ?: throw IllegalStateException("Missing LWJGL natives for platform ${Util.getPlatform()}!") 40 | 41 | if (!LocalLibreTranslateInstance.unityTranslateDir.exists()) 42 | LocalLibreTranslateInstance.unityTranslateDir.mkdirs() 43 | 44 | val file = File(LocalLibreTranslateInstance.unityTranslateDir, fileName) 45 | 46 | if (!file.exists()) 47 | file.createNewFile() 48 | 49 | file.writeBytes(nativesJar.readBytes()) 50 | System.setProperty("org.lwjgl.librarypath", file.absolutePath)*/ 51 | 52 | // TODO: figure out how to load LWJGL on Forge. 53 | // The module class loader makes it absurdly difficult to add custom JARs to the classpath, 54 | // meaning we likely need to make a custom mod loader or language provider *just* to get LWJGL to load ON THE SERVER SIDE ONLY. 55 | 56 | // by the way, if you're wondering how to make this idea work in Fabric, it's just this: 57 | // FabricLauncherBase.getLauncher().addToClassPath(path) 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/xyz/bluspring/unitytranslate/util/nativeaccess/NativeAccess.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.util.nativeaccess 2 | 3 | object NativeAccess { 4 | fun isCudaSupported(): CudaState { 5 | LwjglLoader.tryLoadLwjgl() 6 | 7 | return CudaAccess.cudaState 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/mods.toml: -------------------------------------------------------------------------------- 1 | modLoader="${forge_loader}" 2 | loaderVersion="${forge_loader_version}" 3 | license="MIT" 4 | issueTrackerURL="https://github.com/BluSpring/UnityTranslate/issues" 5 | 6 | [[mods]] 7 | modId="unitytranslate" 8 | version="${mod_version}" 9 | displayName="UnityTranslate" 10 | logoFile="icon.png" 11 | credits="Thanks to Argos Open Tech for creating LibreTranslate, without it this mod would not have been possible." 12 | authors="BluSpring" 13 | description=''' 14 | A mod designed for free live translation, inspired by the QSMP. Mainly created for the Unity Multiplayer server, and for public release towards the multilingual Minecraft community. 15 | ''' 16 | 17 | [[dependencies.unitytranslate]] 18 | modId="${mod_loader_name}" 19 | ${FUCKING_REQUIRED} 20 | versionRange="[1,)" 21 | ordering="NONE" 22 | side="BOTH" 23 | 24 | [[dependencies.unitytranslate]] 25 | modId="minecraft" 26 | ${FUCKING_REQUIRED} 27 | versionRange="[${mc_version},)" 28 | ordering="NONE" 29 | side="BOTH" 30 | 31 | [[dependencies.unitytranslate]] 32 | modId="kotlinforforge" 33 | ${FUCKING_REQUIRED} 34 | versionRange="[${forge_kotlin_version},)" 35 | ordering="NONE" 36 | side="BOTH" 37 | 38 | [[dependencies.unitytranslate]] 39 | modId="architectury" 40 | ${FUCKING_REQUIRED} 41 | versionRange="[${architectury_version},)" 42 | ordering="NONE" 43 | side="BOTH" 44 | 45 | [[mixins]] 46 | config = "unitytranslate.mixins.json" -------------------------------------------------------------------------------- /src/main/resources/META-INF/neoforge.mods.toml: -------------------------------------------------------------------------------- 1 | modLoader="kotlinforforge" 2 | loaderVersion="[1,)" 3 | license="MIT" 4 | issueTrackerURL="https://github.com/BluSpring/UnityTranslate/issues" 5 | 6 | [[mods]] 7 | modId="unitytranslate" 8 | version="${mod_version}" 9 | displayName="UnityTranslate" 10 | logoFile="icon.png" 11 | credits="Thanks to Argos Open Tech for creating LibreTranslate, without it this mod would not have been possible." 12 | authors="BluSpring" 13 | description=''' 14 | A mod designed for free live translation, inspired by the QSMP. Mainly created for the Unity Multiplayer server, and for public release towards the multilingual Minecraft community. 15 | ''' 16 | 17 | [[dependencies.unitytranslate]] 18 | modId="neoforge" 19 | mandatory=true 20 | versionRange="[20.4,)" 21 | ordering="NONE" 22 | side="BOTH" 23 | 24 | [[dependencies.unitytranslate]] 25 | modId="minecraft" 26 | mandatory=true 27 | versionRange="[${mc_version},)" 28 | ordering="NONE" 29 | side="BOTH" 30 | 31 | [[dependencies.unitytranslate]] 32 | modId="kotlinforforge" 33 | mandatory=true 34 | versionRange="[${forge_kotlin_version},)" 35 | ordering="NONE" 36 | side="BOTH" 37 | 38 | [[dependencies.unitytranslate]] 39 | modId="architectury" 40 | mandatory=true 41 | versionRange="[${architectury_version},)" 42 | ordering="NONE" 43 | side="BOTH" 44 | 45 | [[mixins]] 46 | config = "unitytranslate.mixins.json" -------------------------------------------------------------------------------- /src/main/resources/assets/unitytranslate/lang/es_es.json: -------------------------------------------------------------------------------- 1 | { 2 | "unitytranslate.credit.author": "de BluSpring", 3 | 4 | "unitytranslate.open_browser.prompt": "Para que UnityTranslate transcriba su discurso en el modo Transcripción por navegador, su navegador debe permanecer abierto a un sitio web que se ejecute localmente. Puede cerrar su navegador una vez que ya no esté usando UnityTranslate.", 5 | "unitytranslate.do_not_show_again": "No me muestres de nuevo", 6 | "unitytranslate.open_browser.open_in_browser": "Abrir en el navegador", 7 | 8 | "unitytranslate.transcriber.connected": "Transcripción conectada al navegador.", 9 | "unitytranslate.transcriber.disconnected": "Desconectado de Browser Transcription, por favor haga clic aquí para volver a conectarse.", 10 | 11 | "unitytranslate.transcriber.error": "Se ha producido un error: ", 12 | "unitytranslate.transcriber.error.too_many_resets": "Se han producido demasiados reinicios del transcriptor en un corto espacio de tiempo, por favor reinicie su navegador.", 13 | "unitytranslate.transcriber.error.no_support": "Tu navegador no es compatible con la API de reconocimiento de voz web. Por favor, intente utilizar Google Chrome o Microsoft Edge en su lugar.", 14 | 15 | "unitytranslate.transcriber.type.sphinx": "PocketSphinx (desactivado)", 16 | "unitytranslate.transcriber.type.sphinx.description": "Utiliza el motor de reconocimiento de voz PocketSphinx para la transcripción de voz. Actualmente desactivado debido a su gran requerimiento de tamaño..", 17 | "unitytranslate.transcriber.type.browser": "Web Speech API", 18 | "unitytranslate.transcriber.type.browser.description": "Utiliza la Web Speech API para la transcripción de voz. Esto requiere abrir el navegador web para usarlo, y puede utilizar más recursos.\nnEs compatible principalmente con Google Chrome y Microsoft Edge.", 19 | "unitytranslate.transcriber.type.windows_sapi": "API de voz de Windows (desactivada)", 20 | "unitytranslate.transcriber.type.windows_sapi.description": "Utiliza la API de voz de Windows para la transcripción de voz. Esto es soportado nativamente en Windows, pero requiere que instales los paquetes de idioma para los idiomas que pretendes hablar. Actualmente desactivado debido a que requiere algo de investigación para configurarlo correctamente..", 21 | 22 | "unitytranslate.configure_boxes": "Configurar cuadros de transcripción", 23 | "unitytranslate.toggle_transcription": "Silenciar/activar la transcripción propia", 24 | "unitytranslate.toggle_boxes": "Mostrar/Ocultar cuadros de transcripción", 25 | "unitytranslate.set_spoken_language": "Establecer lengua hablada", 26 | "unitytranslate.select_language": "Seleccionar idioma", 27 | "unitytranslate.select_language.already_selected": "¡Esta opción ya está seleccionada!", 28 | "unitytranslate.clear_transcripts": "Transcripciones claras", 29 | 30 | "unitytranslate.transcript_boxes": "Cajas de transcripción", 31 | "unitytranslate.transcript": "Transcripción", 32 | 33 | "unitytranslate.language.en": "Inglés", 34 | "unitytranslate.language.es": "Español", 35 | "unitytranslate.language.pt": "Portugués", 36 | "unitytranslate.language.fr": "Francés", 37 | "unitytranslate.language.sv": "Sueco", 38 | "unitytranslate.language.ms": "Malayo", 39 | "unitytranslate.language.he": "Hebreo", 40 | "unitytranslate.language.ar": "Árabe", 41 | "unitytranslate.language.de": "Alemán", 42 | "unitytranslate.language.ru": "Ruso", 43 | "unitytranslate.language.ja": "Japonés", 44 | "unitytranslate.language.zh": "Chino", 45 | 46 | "gui.unitytranslate.config.client": "Configuración de cliente", 47 | "config.unitytranslate.client.enabled": "¿Mod Activado?", 48 | "config.unitytranslate.client.enabled.desc": "Activa o desactiva el mod.", 49 | "config.unitytranslate.client.language": "Lengua hablada", 50 | "config.unitytranslate.client.language.desc": "Selecciona el idioma hablado en ese momento.", 51 | "config.unitytranslate.client.muteTranscriptWhenVoiceChatMuted": "¿Silenciar la transcripción cuando el chat de voz está silenciado?", 52 | "config.unitytranslate.client.muteTranscriptWhenVoiceChatMuted.desc": "Si Simple Voice Chat / Plasmo Voice está silenciado, y esta opción está establecida en True, UnityTranslate no transcribirá audio mientras esté silenciado.", 53 | "config.unitytranslate.client.openBrowserWithoutPromptV2": "¿Abrir el navegador sin preguntar?", 54 | "config.unitytranslate.client.openBrowserWithoutPromptV2.desc": "Cuando se una a un mundo por primera vez en una sesión, UnityTranslate abrirá automáticamente el navegador por usted sin preguntarle.", 55 | "config.unitytranslate.client.textScale": "Texto Escala (%)", 56 | "config.unitytranslate.client.textScale.desc": "La escala del texto dentro de los cuadros de transcripción.", 57 | "config.unitytranslate.client.disappearingText": "¿Activar la desaparición del texto?", 58 | "config.unitytranslate.client.disappearingText.desc": "Si esta opción está activada, las transcripciones desaparecerán al terminar de ser pronunciadas, siguiendo el retardo del texto.", 59 | "config.unitytranslate.client.disappearingTextDelay": "Retraso de desaparición de texto (segundos)", 60 | "config.unitytranslate.client.disappearingTextDelay.desc": "Tiempo que transcurre desde que se pronuncia una transcripción hasta que desaparece el texto.", 61 | "config.unitytranslate.client.disappearingTextFade": "Desvanecimiento del texto (segundos)", 62 | "config.unitytranslate.client.disappearingTextFade.desc": "El tiempo de desvanecimiento de un texto de transcripción que desaparece.", 63 | 64 | "gui.unitytranslate.config.common": "Configuración común", 65 | "config.unitytranslate.common.batchTranslateInterval": "Intervalo de traducción por lotes (en segundos)", 66 | "config.unitytranslate.common.batchTranslateInterval.desc": "Para evitar sobrecargar las instancias de traducción, UnityTranslate envía las traducciones a un intervalo específico, lo que también ayuda a evitar traducciones redundantes.", 67 | "config.unitytranslate.common.offloadServers": "Servidores de descarga", 68 | "config.unitytranslate.common.offloadServers.desc": "Servidores de traducción adicionales que ejecutan LibreTranslate para reducir la carga de su ordenador.", 69 | "config.unitytranslate.common.offloadServers.website_url.desc": "La URL de un servidor LibreTranslate.", 70 | "config.unitytranslate.common.offloadServers.api_key.desc": "La clave API para el servidor LibreTranslate. Puede estar en blanco.", 71 | "config.unitytranslate.common.shouldRunTranslationServer": "¿Debería funcionar un servidor de traducción?", 72 | "config.unitytranslate.common.shouldRunTranslationServer.desc": "¿Debería UnityTranslate iniciar automáticamente un servidor local de LibreTranslate?", 73 | "config.unitytranslate.common.shouldUseCuda": "Debería usar CUDA?", 74 | "config.unitytranslate.common.shouldUseCuda.desc": "¿Debe el servidor local de LibreTranslate utilizar CUDA para traducciones aceleradas? Sólo se admiten tarjetas gráficas NVIDIA GTX 900 o posteriores; es posible que no se admitan tarjetas gráficas anteriores o de otros fabricantes.", 75 | "config.unitytranslate.common.translatePriority": "Prioridad de traducción", 76 | "config.unitytranslate.common.translatePriority.desc": "En qué orden se debe priorizar la traducción. La parte superior es la de mayor prioridad, mientras que la inferior es la de menor prioridad. Si una opción no está disponible, simplemente se omitirá. Esto se utiliza para reducir al máximo la tensión de todos los dispositivos en la traducción..", 77 | "config.unitytranslate.common.translatePriority.client_gpu": "Cliente (GPU)", 78 | "config.unitytranslate.common.translatePriority.client_gpu.desc": "Realiza las traducciones en el lado del cliente, utilizando la tarjeta gráfica del cliente. Reduce esta opción a una prioridad más baja si está causando problemas a tu experiencia de juego. Sólo es compatible con tarjetas gráficas NVIDIA GTX 900 o posteriores, cualquier tarjeta gráfica anterior o de diferentes fabricantes puede no ser compatible..", 79 | "config.unitytranslate.common.translatePriority.client_cpu": "Cliente (CPU)", 80 | "config.unitytranslate.common.translatePriority.client_cpu.desc": "Realiza las traducciones en el lado del cliente, utilizando la CPU del cliente.\NPuede hacer que su ordenador funcione más lento durante el proceso de traducción si tiene una CPU débil.\NCambie esta opción a una prioridad más baja si está causando problemas a su experiencia de juego..", 81 | "config.unitytranslate.common.translatePriority.server_gpu": "Servidor (GPU)", 82 | "config.unitytranslate.common.translatePriority.server_gpu.desc": "Realiza las traducciones en el lado del servidor, utilizando la tarjeta gráfica del servidor.Esta es la opción de mejor rendimiento en general, sin embargo, es la opción menos compatible debido a que la mayoría de los servidores dedicados no proporcionan una tarjeta gráfica a sus servidores.Sólo admite tarjetas gráficas NVIDIA GTX 900 o más recientes, cualquier tarjeta gráfica anterior o de diferentes proveedores puede no ser compatible..", 83 | "config.unitytranslate.common.translatePriority.server_cpu": "Servidor (CPU)", 84 | "config.unitytranslate.common.translatePriority.server_cpu.desc": "Realiza las traducciones en el lado del servidor, utilizando la CPU del servidor.\NPuede hacer que el servidor de Minecraft funcione más lento mientras se realizan las traducciones.\NCambia esta opción a una prioridad más baja si está causando problemas a tu experiencia de juego..", 85 | "config.unitytranslate.common.translatePriority.offloaded": "Descargada", 86 | "config.unitytranslate.common.translatePriority.offloaded.desc": "Realiza las traducciones en un servidor de traducción dedicado.\NPuede tener tiempos de traducción más demorados debido a un tráfico potencialmente mayor de usuarios, pero tiene la menor carga para el cliente y el servidor.", 87 | 88 | "unitytranslate.value.true": "Verdadero", 89 | "unitytranslate.value.false": "Falso", 90 | "unitytranslate.value.none": "(ninguno)", 91 | 92 | "unitytranslate.transcriber.browser.title": "Transcripción del navegador UnityTranslate", 93 | "unitytranslate.transcriber.browser.info": "Esta página web se utiliza para reconocer palabras pronunciadas en el habla sin necesidad de pagar por el servicio en la nube. Esto suele permitir obtener mejores resultados y una mayor compatibilidad lingüística, aunque también puede utilizar más recursos..", 94 | "unitytranslate.transcriber.browser.paused": "Transcripción en pausa", 95 | "unitytranslate.transcriber.browser.paused.desc": "Esto suele significar que la transcripción se ha bloqueado o que el juego se ha cerrado.%BR%Si sigues utilizando el modo de transcripción por navegador en UnityTranslate, actualiza la página.%BR%Si este problema persiste, informa del problema en %GITHUB%.." 96 | } -------------------------------------------------------------------------------- /src/main/resources/assets/unitytranslate/lang/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "unitytranslate.credit.author": "de BluSpring", 3 | 4 | "unitytranslate.open_browser.prompt": "Para que UnityTranslate transcriba su discurso en el modo Transcripción por navegador, su navegador debe permanecer abierto a un sitio web que se ejecute localmente. Puede cerrar su navegador una vez que ya no esté usando UnityTranslate.", 5 | "unitytranslate.do_not_show_again": "No me muestres de nuevo", 6 | "unitytranslate.open_browser.open_in_browser": "Abrir en el navegador", 7 | 8 | "unitytranslate.transcriber.connected": "Transcripción conectada al navegador.", 9 | "unitytranslate.transcriber.disconnected": "Desconectado de Browser Transcription, por favor haga clic aquí para volver a conectarse.", 10 | 11 | "unitytranslate.transcriber.error": "Se ha producido un error: ", 12 | "unitytranslate.transcriber.error.too_many_resets": "Se han producido demasiados reinicios del transcriptor en un corto espacio de tiempo, por favor reinicie su navegador.", 13 | "unitytranslate.transcriber.error.no_support": "Tu navegador no es compatible con la API de reconocimiento de voz web. Por favor, intente utilizar Google Chrome o Microsoft Edge en su lugar.", 14 | 15 | "unitytranslate.transcriber.type.sphinx": "PocketSphinx (desactivado)", 16 | "unitytranslate.transcriber.type.sphinx.description": "Utiliza el motor de reconocimiento de voz PocketSphinx para la transcripción de voz. Actualmente desactivado debido a su gran requerimiento de tamaño..", 17 | "unitytranslate.transcriber.type.browser": "Web Speech API", 18 | "unitytranslate.transcriber.type.browser.description": "Utiliza la Web Speech API para la transcripción de voz. Esto requiere abrir el navegador web para usarlo, y puede utilizar más recursos.\nnEs compatible principalmente con Google Chrome y Microsoft Edge.", 19 | "unitytranslate.transcriber.type.windows_sapi": "API de voz de Windows (desactivada)", 20 | "unitytranslate.transcriber.type.windows_sapi.description": "Utiliza la API de voz de Windows para la transcripción de voz. Esto es soportado nativamente en Windows, pero requiere que instales los paquetes de idioma para los idiomas que pretendes hablar. Actualmente desactivado debido a que requiere algo de investigación para configurarlo correctamente..", 21 | 22 | "unitytranslate.configure_boxes": "Configurar cuadros de transcripción", 23 | "unitytranslate.toggle_transcription": "Silenciar/activar la transcripción propia", 24 | "unitytranslate.toggle_boxes": "Mostrar/Ocultar cuadros de transcripción", 25 | "unitytranslate.set_spoken_language": "Establecer lengua hablada", 26 | "unitytranslate.select_language": "Seleccionar idioma", 27 | "unitytranslate.select_language.already_selected": "¡Esta opción ya está seleccionada!", 28 | "unitytranslate.clear_transcripts": "Transcripciones claras", 29 | 30 | "unitytranslate.transcript_boxes": "Cajas de transcripción", 31 | "unitytranslate.transcript": "Transcripción", 32 | 33 | "unitytranslate.language.en": "Inglés", 34 | "unitytranslate.language.es": "Español", 35 | "unitytranslate.language.pt": "Portugués", 36 | "unitytranslate.language.fr": "Francés", 37 | "unitytranslate.language.sv": "Sueco", 38 | "unitytranslate.language.ms": "Malayo", 39 | "unitytranslate.language.he": "Hebreo", 40 | "unitytranslate.language.ar": "Árabe", 41 | "unitytranslate.language.de": "Alemán", 42 | "unitytranslate.language.ru": "Ruso", 43 | "unitytranslate.language.ja": "Japonés", 44 | "unitytranslate.language.zh": "Chino", 45 | 46 | "gui.unitytranslate.config.client": "Configuración de cliente", 47 | "config.unitytranslate.client.enabled": "¿Mod Activado?", 48 | "config.unitytranslate.client.enabled.desc": "Activa o desactiva el mod.", 49 | "config.unitytranslate.client.language": "Lengua hablada", 50 | "config.unitytranslate.client.language.desc": "Selecciona el idioma hablado en ese momento.", 51 | "config.unitytranslate.client.muteTranscriptWhenVoiceChatMuted": "¿Silenciar la transcripción cuando el chat de voz está silenciado?", 52 | "config.unitytranslate.client.muteTranscriptWhenVoiceChatMuted.desc": "Si Simple Voice Chat / Plasmo Voice está silenciado, y esta opción está establecida en True, UnityTranslate no transcribirá audio mientras esté silenciado.", 53 | "config.unitytranslate.client.openBrowserWithoutPromptV2": "¿Abrir el navegador sin preguntar?", 54 | "config.unitytranslate.client.openBrowserWithoutPromptV2.desc": "Cuando se una a un mundo por primera vez en una sesión, UnityTranslate abrirá automáticamente el navegador por usted sin preguntarle.", 55 | "config.unitytranslate.client.textScale": "Texto Escala (%)", 56 | "config.unitytranslate.client.textScale.desc": "La escala del texto dentro de los cuadros de transcripción.", 57 | "config.unitytranslate.client.disappearingText": "¿Activar la desaparición del texto?", 58 | "config.unitytranslate.client.disappearingText.desc": "Si esta opción está activada, las transcripciones desaparecerán al terminar de ser pronunciadas, siguiendo el retardo del texto.", 59 | "config.unitytranslate.client.disappearingTextDelay": "Retraso de desaparición de texto (segundos)", 60 | "config.unitytranslate.client.disappearingTextDelay.desc": "Tiempo que transcurre desde que se pronuncia una transcripción hasta que desaparece el texto.", 61 | "config.unitytranslate.client.disappearingTextFade": "Desvanecimiento del texto (segundos)", 62 | "config.unitytranslate.client.disappearingTextFade.desc": "El tiempo de desvanecimiento de un texto de transcripción que desaparece.", 63 | 64 | "gui.unitytranslate.config.common": "Configuración común", 65 | "config.unitytranslate.common.batchTranslateInterval": "Intervalo de traducción por lotes (en segundos)", 66 | "config.unitytranslate.common.batchTranslateInterval.desc": "Para evitar sobrecargar las instancias de traducción, UnityTranslate envía las traducciones a un intervalo específico, lo que también ayuda a evitar traducciones redundantes.", 67 | "config.unitytranslate.common.offloadServers": "Servidores de descarga", 68 | "config.unitytranslate.common.offloadServers.desc": "Servidores de traducción adicionales que ejecutan LibreTranslate para reducir la carga de su ordenador.", 69 | "config.unitytranslate.common.offloadServers.website_url.desc": "La URL de un servidor LibreTranslate.", 70 | "config.unitytranslate.common.offloadServers.api_key.desc": "La clave API para el servidor LibreTranslate. Puede estar en blanco.", 71 | "config.unitytranslate.common.shouldRunTranslationServer": "¿Debería funcionar un servidor de traducción?", 72 | "config.unitytranslate.common.shouldRunTranslationServer.desc": "¿Debería UnityTranslate iniciar automáticamente un servidor local de LibreTranslate?", 73 | "config.unitytranslate.common.shouldUseCuda": "Debería usar CUDA?", 74 | "config.unitytranslate.common.shouldUseCuda.desc": "¿Debe el servidor local de LibreTranslate utilizar CUDA para traducciones aceleradas? Sólo se admiten tarjetas gráficas NVIDIA GTX 900 o posteriores; es posible que no se admitan tarjetas gráficas anteriores o de otros fabricantes.", 75 | "config.unitytranslate.common.translatePriority": "Prioridad de traducción", 76 | "config.unitytranslate.common.translatePriority.desc": "En qué orden se debe priorizar la traducción. La parte superior es la de mayor prioridad, mientras que la inferior es la de menor prioridad. Si una opción no está disponible, simplemente se omitirá. Esto se utiliza para reducir al máximo la tensión de todos los dispositivos en la traducción..", 77 | "config.unitytranslate.common.translatePriority.client_gpu": "Cliente (GPU)", 78 | "config.unitytranslate.common.translatePriority.client_gpu.desc": "Realiza las traducciones en el lado del cliente, utilizando la tarjeta gráfica del cliente. Reduce esta opción a una prioridad más baja si está causando problemas a tu experiencia de juego. Sólo es compatible con tarjetas gráficas NVIDIA GTX 900 o posteriores, cualquier tarjeta gráfica anterior o de diferentes fabricantes puede no ser compatible..", 79 | "config.unitytranslate.common.translatePriority.client_cpu": "Cliente (CPU)", 80 | "config.unitytranslate.common.translatePriority.client_cpu.desc": "Realiza las traducciones en el lado del cliente, utilizando la CPU del cliente.\NPuede hacer que su ordenador funcione más lento durante el proceso de traducción si tiene una CPU débil.\NCambie esta opción a una prioridad más baja si está causando problemas a su experiencia de juego..", 81 | "config.unitytranslate.common.translatePriority.server_gpu": "Servidor (GPU)", 82 | "config.unitytranslate.common.translatePriority.server_gpu.desc": "Realiza las traducciones en el lado del servidor, utilizando la tarjeta gráfica del servidor.Esta es la opción de mejor rendimiento en general, sin embargo, es la opción menos compatible debido a que la mayoría de los servidores dedicados no proporcionan una tarjeta gráfica a sus servidores.Sólo admite tarjetas gráficas NVIDIA GTX 900 o más recientes, cualquier tarjeta gráfica anterior o de diferentes proveedores puede no ser compatible..", 83 | "config.unitytranslate.common.translatePriority.server_cpu": "Servidor (CPU)", 84 | "config.unitytranslate.common.translatePriority.server_cpu.desc": "Realiza las traducciones en el lado del servidor, utilizando la CPU del servidor.\NPuede hacer que el servidor de Minecraft funcione más lento mientras se realizan las traducciones.\NCambia esta opción a una prioridad más baja si está causando problemas a tu experiencia de juego..", 85 | "config.unitytranslate.common.translatePriority.offloaded": "Descargada", 86 | "config.unitytranslate.common.translatePriority.offloaded.desc": "Realiza las traducciones en un servidor de traducción dedicado.\NPuede tener tiempos de traducción más demorados debido a un tráfico potencialmente mayor de usuarios, pero tiene la menor carga para el cliente y el servidor.", 87 | 88 | "unitytranslate.value.true": "Verdadero", 89 | "unitytranslate.value.false": "Falso", 90 | "unitytranslate.value.none": "(ninguno)", 91 | 92 | "unitytranslate.transcriber.browser.title": "Transcripción del navegador UnityTranslate", 93 | "unitytranslate.transcriber.browser.info": "Esta página web se utiliza para reconocer palabras pronunciadas en el habla sin necesidad de pagar por el servicio en la nube. Esto suele permitir obtener mejores resultados y una mayor compatibilidad lingüística, aunque también puede utilizar más recursos..", 94 | "unitytranslate.transcriber.browser.paused": "Transcripción en pausa", 95 | "unitytranslate.transcriber.browser.paused.desc": "Esto suele significar que la transcripción se ha bloqueado o que el juego se ha cerrado.%BR%Si sigues utilizando el modo de transcripción por navegador en UnityTranslate, actualiza la página.%BR%Si este problema persiste, informa del problema en %GITHUB%.." 96 | } -------------------------------------------------------------------------------- /src/main/resources/assets/unitytranslate/textures/gui/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnityMultiplayer/UnityTranslate/d7453d8815bf21569567e935a913fa42dfad5922/src/main/resources/assets/unitytranslate/textures/gui/close.png -------------------------------------------------------------------------------- /src/main/resources/assets/unitytranslate/textures/gui/sprites/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnityMultiplayer/UnityTranslate/d7453d8815bf21569567e935a913fa42dfad5922/src/main/resources/assets/unitytranslate/textures/gui/sprites/arrow_down.png -------------------------------------------------------------------------------- /src/main/resources/assets/unitytranslate/textures/gui/sprites/arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnityMultiplayer/UnityTranslate/d7453d8815bf21569567e935a913fa42dfad5922/src/main/resources/assets/unitytranslate/textures/gui/sprites/arrow_up.png -------------------------------------------------------------------------------- /src/main/resources/assets/unitytranslate/textures/gui/transcriber/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnityMultiplayer/UnityTranslate/d7453d8815bf21569567e935a913fa42dfad5922/src/main/resources/assets/unitytranslate/textures/gui/transcriber/browser.png -------------------------------------------------------------------------------- /src/main/resources/assets/unitytranslate/textures/gui/transcriber/sphinx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnityMultiplayer/UnityTranslate/d7453d8815bf21569567e935a913fa42dfad5922/src/main/resources/assets/unitytranslate/textures/gui/transcriber/sphinx.png -------------------------------------------------------------------------------- /src/main/resources/assets/unitytranslate/textures/gui/transcriber/windows_sapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnityMultiplayer/UnityTranslate/d7453d8815bf21569567e935a913fa42dfad5922/src/main/resources/assets/unitytranslate/textures/gui/transcriber/windows_sapi.png -------------------------------------------------------------------------------- /src/main/resources/assets/unitytranslate/textures/gui/transcription_muted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnityMultiplayer/UnityTranslate/d7453d8815bf21569567e935a913fa42dfad5922/src/main/resources/assets/unitytranslate/textures/gui/transcription_muted.png -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "unitytranslate", 4 | "version": "${mod_version}", 5 | "name": "UnityTranslate", 6 | "description": "A mod designed for free live translation, inspired by the QSMP. Mainly created for the Unity Multiplayer server, and for public release towards the multilingual Minecraft community.", 7 | "authors": [ 8 | "BluSpring" 9 | ], 10 | "contact": { 11 | "sources": "https://github.com/BluSpring/UnityTranslate", 12 | "issues": "https://github.com/BluSpring/UnityTranslate/issues" 13 | }, 14 | "license": "MIT", 15 | "icon": "icon.png", 16 | "environment": "*", 17 | "entrypoints": { 18 | "client": [ 19 | "xyz.bluspring.unitytranslate.fabric.client.UnityTranslateFabricClient" 20 | ], 21 | "main": [ 22 | "xyz.bluspring.unitytranslate.fabric.UnityTranslateFabric" 23 | ], 24 | "modmenu": [ 25 | "xyz.bluspring.unitytranslate.fabric.compat.modmenu.UTModMenuIntegration" 26 | ], 27 | "voicechat": [ 28 | "xyz.bluspring.unitytranslate.compat.voicechat.SimpleVoiceChatCompat" 29 | ] 30 | }, 31 | "mixins": [ 32 | "unitytranslate.mixins.json" 33 | ], 34 | "depends": { 35 | "fabricloader": ">=${loader_version}", 36 | "minecraft": "${mc_version}", 37 | "fabric-language-kotlin": ">=${fabric_kotlin_version}", 38 | "architectury": ">=${architectury_version}" 39 | }, 40 | "recommends": { 41 | "voicechat": "*" 42 | }, 43 | "suggests": { 44 | "talk_balloons": "*" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnityMultiplayer/UnityTranslate/d7453d8815bf21569567e935a913fa42dfad5922/src/main/resources/icon.png -------------------------------------------------------------------------------- /src/main/resources/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "description": "UnityTranslate resources", 4 | "pack_format": 15, 5 | "forge:server_data_pack_format": 12 6 | } 7 | } -------------------------------------------------------------------------------- /src/main/resources/unitytranslate.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "xyz.bluspring.unitytranslate.mixin", 5 | "compatibilityLevel": "${java_version}", 6 | "mixins": [ 7 | "AbstractWidgetAccessor" 8 | ], 9 | "client": [ 10 | "AbstractWidgetMixin" 11 | ], 12 | "injectors": { 13 | "defaultRequire": 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@types/webspeechapi": "^0.0.29" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/website/speech.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: sans-serif; 3 | } 4 | 5 | .pause-screen { 6 | background: rgba(0, 0, 0, 0.6); 7 | z-index: 99; 8 | width: 100vw; 9 | height: 100vh; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | display: none; 14 | } 15 | 16 | .visible { 17 | display: block; 18 | } 19 | 20 | .pause-screen h2 { 21 | text-align: center; 22 | color: white; 23 | text-shadow: 3px 3px 4px rgba(0, 0, 0, 0.6); 24 | font-size: 5em; 25 | } 26 | 27 | .pause-screen p { 28 | text-align: center; 29 | color: white; 30 | text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6); 31 | font-size: 2em; 32 | } 33 | 34 | .info { 35 | text-align: center; 36 | font-size: 2em; 37 | } 38 | 39 | .transcript { 40 | text-align: center; 41 | width: 70vw; 42 | margin: auto; 43 | } 44 | 45 | .transcript h2 { 46 | font-size: 2em; 47 | font-weight: bold; 48 | } 49 | 50 | .footer { 51 | position: fixed; 52 | bottom: 2rem; 53 | left: 0; 54 | right: 0; 55 | margin: auto; 56 | text-align: center; 57 | font-weight: 600; 58 | } -------------------------------------------------------------------------------- /src/main/resources/website/speech.js: -------------------------------------------------------------------------------- 1 | let ws = new WebSocket('ws://127.0.0.1:%SOCKET_PORT%'); 2 | 3 | window.SpeechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition; 4 | 5 | const pause = document.getElementById('pause'); 6 | 7 | /** 8 | * @type {SpeechRecognition} 9 | */ 10 | let transcriber; 11 | 12 | let lastReset = 0; 13 | let totalResetsBelow50ms = 0; 14 | let isErrored = false; 15 | let wasNoSpeech = false; 16 | let isCurrentlyMuted = false; 17 | 18 | ws.onopen = () => { 19 | console.log('Connected'); 20 | } 21 | 22 | function setupTranscriber(lang) { 23 | console.log(`Starting transcriber with lang ${lang}`); 24 | transcriber = new SpeechRecognition(); 25 | 26 | lastReset = Date.now(); 27 | 28 | ws.send(JSON.stringify({ 29 | op: 'reset' 30 | })); 31 | 32 | transcriber.lang = lang; 33 | transcriber.continuous = true; 34 | transcriber.interimResults = true; 35 | transcriber.maxAlternatives = 3; 36 | 37 | transcriber.onerror = (ev) => { 38 | console.error(ev.error); 39 | if (ev.error == 'no-speech') 40 | wasNoSpeech = true; 41 | } 42 | 43 | transcriber.onend = () => { 44 | if (isErrored) { 45 | pause.classList.add('visible'); 46 | return; 47 | } 48 | 49 | if (isCurrentlyMuted) { 50 | return; 51 | } 52 | 53 | console.log('Transcriber resetting...'); 54 | ws.send(JSON.stringify({ 55 | op: 'reset' 56 | })); 57 | transcriber.start(); 58 | 59 | if (wasNoSpeech) { 60 | wasNoSpeech = false; 61 | return; 62 | } 63 | 64 | if (Date.now() - lastReset >= 50) { 65 | if (++totalResetsBelow50ms >= 30) { 66 | ws.send(JSON.stringify({ 67 | op: "error", 68 | d: { 69 | type: "too_many_resets" 70 | } 71 | })); 72 | isErrored = true; 73 | } 74 | } else { 75 | totalResetsBelow50ms = 0; 76 | } 77 | lastReset = Date.now(); 78 | } 79 | 80 | transcriber.onresult = (ev) => { 81 | let results = []; 82 | const items = ev.results.item(ev.resultIndex); 83 | 84 | for (let j = 0; j < items.length; j++) { 85 | const data = items.item(j); 86 | results.push({ 87 | text: data.transcript, 88 | confidence: data.confidence 89 | }); 90 | } 91 | 92 | ws.send(JSON.stringify({ 93 | op: 'transcript', 94 | d: { 95 | results: results, 96 | index: ev.resultIndex 97 | } 98 | })); 99 | 100 | if (results.length > 0) 101 | document.getElementById('transcript').innerText = results[0].text; 102 | } 103 | 104 | transcriber.start(); 105 | } 106 | 107 | ws.onmessage = (ev) => { 108 | const data = JSON.parse(ev.data); 109 | 110 | switch (data.op) { 111 | case 'set_language': { 112 | const lang = data.d.language; 113 | 114 | if (!!transcriber) { 115 | transcriber.stop(); 116 | } 117 | 118 | if (!SpeechRecognition) { 119 | ws.send(JSON.stringify({ 120 | op: "error", 121 | d: { 122 | type: "no_support" 123 | } 124 | })); 125 | pause.classList.add('visible'); 126 | return; 127 | } 128 | 129 | document.getElementById('transcript_lang').innerText = `%I18N_TRANSCRIPT% (${lang})`; 130 | 131 | setupTranscriber(lang); 132 | 133 | break; 134 | } 135 | 136 | case 'set_muted': { 137 | const isMuted = data.d.muted; 138 | isCurrentlyMuted = isMuted; 139 | 140 | if (!isMuted && !!transcriber) { 141 | ws.send(JSON.stringify({ 142 | op: 'reset' 143 | })); 144 | transcriber.start(); 145 | } else if (!!transcriber) { 146 | transcriber.stop(); 147 | } 148 | } 149 | } 150 | } 151 | 152 | ws.onerror = e => { 153 | console.error('Failed to connect!'); 154 | console.error(e); 155 | } 156 | 157 | ws.onclose = () => { 158 | if (!!transcriber) { 159 | isErrored = true; 160 | transcriber.stop(); 161 | } 162 | } -------------------------------------------------------------------------------- /src/main/resources/website/speech_recognition.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %I18N_TITLE% 5 | 6 | 7 | 8 |

9 | %I18N_INFO% 10 |

11 | 12 |
13 |

%I18N_TRANSCRIPT%

14 |

15 |
16 | 17 | 18 | 19 |
20 |

%I18N_PAUSED%

21 |

22 | %I18N_PAUSED_DESC% 23 |

24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/website/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/webspeechapi@^0.0.29": 6 | version "0.0.29" 7 | resolved "https://registry.yarnpkg.com/@types/webspeechapi/-/webspeechapi-0.0.29.tgz#8f3c6b31b779df7a9bbac7f89acfce0c3bcb1972" 8 | integrity sha512-AYEhqEJLdR08YPBOwYa73IHTiGU4DdngbKbtZdW+bzuM7s8LzKBed0Fwgl/a3oMqMY227qOT+3Lpr5A0WSmm+A== 9 | -------------------------------------------------------------------------------- /versions/1.20.6-fabric/src/main/kotlin/xyz/bluspring/unitytranslate/network/payloads/MarkIncompletePayload.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.network.payloads 2 | 3 | import net.minecraft.core.UUIDUtil 4 | import net.minecraft.network.RegistryFriendlyByteBuf 5 | import net.minecraft.network.codec.ByteBufCodecs 6 | import net.minecraft.network.codec.StreamCodec 7 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 8 | import xyz.bluspring.unitytranslate.Language 9 | import xyz.bluspring.unitytranslate.network.PacketIds 10 | import java.util.* 11 | 12 | data class MarkIncompletePayload( 13 | val from: Language, 14 | val to: Language, 15 | val uuid: UUID, 16 | val index: Int, 17 | var isIncomplete: Boolean 18 | ) : CustomPacketPayload { 19 | override fun type(): CustomPacketPayload.Type { 20 | return PacketIds.MARK_INCOMPLETE.type 21 | } 22 | 23 | companion object { 24 | val CODEC: StreamCodec = StreamCodec.composite( 25 | Language.STREAM_CODEC, MarkIncompletePayload::from, 26 | Language.STREAM_CODEC, MarkIncompletePayload::to, 27 | UUIDUtil.STREAM_CODEC, MarkIncompletePayload::uuid, 28 | ByteBufCodecs.VAR_INT, MarkIncompletePayload::index, 29 | ByteBufCodecs.BOOL, MarkIncompletePayload::isIncomplete, 30 | 31 | ::MarkIncompletePayload 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /versions/1.20.6-fabric/src/main/kotlin/xyz/bluspring/unitytranslate/network/payloads/SendTranscriptToClientPayload.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.network.payloads 2 | 3 | import net.minecraft.core.UUIDUtil 4 | import net.minecraft.network.RegistryFriendlyByteBuf 5 | import net.minecraft.network.codec.ByteBufCodecs 6 | import net.minecraft.network.codec.StreamCodec 7 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 8 | import xyz.bluspring.unitytranslate.Language 9 | import xyz.bluspring.unitytranslate.network.PacketIds 10 | import java.util.* 11 | 12 | data class SendTranscriptToClientPayload( 13 | val uuid: UUID, 14 | val language: Language, 15 | val index: Int, 16 | val updateTime: Long, 17 | val toSend: Map 18 | ) : CustomPacketPayload { 19 | override fun type(): CustomPacketPayload.Type { 20 | return PacketIds.SEND_TRANSCRIPT_TO_CLIENT.type 21 | } 22 | 23 | companion object { 24 | val CODEC: StreamCodec = StreamCodec.composite( 25 | UUIDUtil.STREAM_CODEC, SendTranscriptToClientPayload::uuid, 26 | Language.STREAM_CODEC, SendTranscriptToClientPayload::language, 27 | ByteBufCodecs.VAR_INT, SendTranscriptToClientPayload::index, 28 | ByteBufCodecs.VAR_LONG, SendTranscriptToClientPayload::updateTime, 29 | StreamCodec.of({ buf, map -> 30 | buf.writeVarInt(map.size) 31 | 32 | for ((language, translated) in map) { 33 | buf.writeEnum(language) 34 | buf.writeUtf(translated) 35 | } 36 | }, { buf -> 37 | val map = mutableMapOf() 38 | val size = buf.readVarInt() 39 | 40 | for (i in 0 until size) { 41 | map[buf.readEnum(Language::class.java)] = buf.readUtf() 42 | } 43 | 44 | map 45 | }), SendTranscriptToClientPayload::toSend, 46 | 47 | ::SendTranscriptToClientPayload 48 | ) 49 | } 50 | } -------------------------------------------------------------------------------- /versions/1.20.6-fabric/src/main/kotlin/xyz/bluspring/unitytranslate/network/payloads/SendTranscriptToServerPayload.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.network.payloads 2 | 3 | import net.minecraft.network.RegistryFriendlyByteBuf 4 | import net.minecraft.network.codec.ByteBufCodecs 5 | import net.minecraft.network.codec.StreamCodec 6 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 7 | import xyz.bluspring.unitytranslate.Language 8 | import xyz.bluspring.unitytranslate.network.PacketIds 9 | 10 | data class SendTranscriptToServerPayload( 11 | val sourceLanguage: Language, 12 | val text: String, 13 | val index: Int, 14 | val updateTime: Long 15 | ) : CustomPacketPayload { 16 | override fun type(): CustomPacketPayload.Type { 17 | return PacketIds.SEND_TRANSCRIPT_TO_SERVER.type 18 | } 19 | 20 | companion object { 21 | val CODEC: StreamCodec = StreamCodec.composite( 22 | Language.STREAM_CODEC, SendTranscriptToServerPayload::sourceLanguage, 23 | ByteBufCodecs.STRING_UTF8, SendTranscriptToServerPayload::text, 24 | ByteBufCodecs.VAR_INT, SendTranscriptToServerPayload::index, 25 | ByteBufCodecs.VAR_LONG, SendTranscriptToServerPayload::updateTime, 26 | 27 | ::SendTranscriptToServerPayload 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /versions/1.20.6-fabric/src/main/kotlin/xyz/bluspring/unitytranslate/network/payloads/ServerSupportPayload.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.network.payloads 2 | 3 | import net.minecraft.network.RegistryFriendlyByteBuf 4 | import net.minecraft.network.codec.StreamCodec 5 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 6 | import xyz.bluspring.unitytranslate.network.PacketIds 7 | 8 | class ServerSupportPayload : CustomPacketPayload { 9 | override fun type(): CustomPacketPayload.Type { 10 | return PacketIds.SERVER_SUPPORT.type 11 | } 12 | 13 | companion object { 14 | val EMPTY = ServerSupportPayload() 15 | val CODEC: StreamCodec = StreamCodec.unit(EMPTY) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /versions/1.20.6-fabric/src/main/kotlin/xyz/bluspring/unitytranslate/network/payloads/SetCurrentLanguagePayload.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.network.payloads 2 | 3 | import net.minecraft.network.RegistryFriendlyByteBuf 4 | import net.minecraft.network.codec.StreamCodec 5 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 6 | import xyz.bluspring.unitytranslate.Language 7 | import xyz.bluspring.unitytranslate.network.PacketIds 8 | 9 | data class SetCurrentLanguagePayload( 10 | val language: Language 11 | ) : CustomPacketPayload { 12 | override fun type(): CustomPacketPayload.Type { 13 | return PacketIds.SET_CURRENT_LANGUAGE.type 14 | } 15 | 16 | companion object { 17 | val CODEC: StreamCodec = StreamCodec.composite( 18 | Language.STREAM_CODEC, SetCurrentLanguagePayload::language, 19 | ::SetCurrentLanguagePayload 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /versions/1.20.6-fabric/src/main/kotlin/xyz/bluspring/unitytranslate/network/payloads/SetUsedLanguagesPayload.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.network.payloads 2 | 3 | import net.minecraft.network.RegistryFriendlyByteBuf 4 | import net.minecraft.network.codec.ByteBufCodecs 5 | import net.minecraft.network.codec.StreamCodec 6 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 7 | import xyz.bluspring.unitytranslate.Language 8 | import xyz.bluspring.unitytranslate.network.PacketIds 9 | 10 | data class SetUsedLanguagesPayload( 11 | val languages: List 12 | ) : CustomPacketPayload { 13 | override fun type(): CustomPacketPayload.Type { 14 | return PacketIds.SET_USED_LANGUAGES.type 15 | } 16 | 17 | companion object { 18 | val CODEC: StreamCodec = StreamCodec.composite( 19 | ByteBufCodecs.list() 20 | .apply(Language.STREAM_CODEC), SetUsedLanguagesPayload::languages, 21 | 22 | ::SetUsedLanguagesPayload 23 | ) 24 | } 25 | } -------------------------------------------------------------------------------- /versions/1.20.6-fabric/src/main/kotlin/xyz/bluspring/unitytranslate/network/payloads/TranslateSignPayload.kt: -------------------------------------------------------------------------------- 1 | package xyz.bluspring.unitytranslate.network.payloads 2 | 3 | import net.minecraft.core.BlockPos 4 | import net.minecraft.network.RegistryFriendlyByteBuf 5 | import net.minecraft.network.codec.StreamCodec 6 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload 7 | import xyz.bluspring.unitytranslate.network.PacketIds 8 | 9 | data class TranslateSignPayload( 10 | val pos: BlockPos 11 | ) : CustomPacketPayload { 12 | override fun type(): CustomPacketPayload.Type { 13 | return PacketIds.TRANSLATE_SIGN.type 14 | } 15 | 16 | companion object { 17 | val CODEC: StreamCodec = StreamCodec.composite( 18 | BlockPos.STREAM_CODEC, TranslateSignPayload::pos, 19 | ::TranslateSignPayload 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /versions/mainProject: -------------------------------------------------------------------------------- 1 | 1.20.1-fabric --------------------------------------------------------------------------------