diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a18779b..beb44fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,14 @@ -name: Build and Archive +name: Build on: push: branches: - main + pull_request: + +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true jobs: build: @@ -11,22 +16,35 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: - distribution: "adopt" - java-version: "17" + distribution: temurin + java-version: "21" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 - name: Make gradlew executable run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew build + run: ./gradlew clean build --no-daemon + + - name: Collect release artifacts + run: | + mkdir -p dist + find modules -path "*/build/libs/*.jar" \ + ! -name "*-sources.jar" \ + ! -name "*-thin.jar" \ + ! -path "*/core/build/libs/*" \ + -exec cp {} dist/ \; - name: Archive artifacts uses: actions/upload-artifact@v4 with: - name: geyser-voice-artifact - path: build/libs/ + name: geyservoice-build + path: dist/*.jar + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21ee9c6..e0fbcc2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,35 +9,56 @@ on: status: required: true description: "Status (beta, stable)" + publish_modrinth: + required: true + description: "Publish to Modrinth" + default: "true" env: VERSION: ${{ github.event.inputs.tag }}-${{ github.event.inputs.status }} +concurrency: + group: release-${{ github.event.inputs.tag }} + cancel-in-progress: false + jobs: build: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: - distribution: "adopt" - java-version: "17" + distribution: temurin + java-version: "21" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 - name: Make gradlew executable run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew build -PVERSION="${{ env.VERSION }}" + run: ./gradlew clean build -PVERSION="${{ env.VERSION }}" --no-daemon + + - name: Collect release artifacts + run: | + mkdir -p release-artifacts + find modules -path "*/build/libs/*.jar" \ + ! -name "*-sources.jar" \ + ! -name "*-thin.jar" \ + ! -path "*/core/build/libs/*" \ + -exec cp {} release-artifacts/ \; - name: Archive artifacts uses: actions/upload-artifact@v4 with: - name: geyser-voice-artifact - path: build/libs/ + name: geyservoice-release + path: release-artifacts/*.jar + if-no-files-found: error publish_release: name: Publish release @@ -49,47 +70,47 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Download artifacts uses: actions/download-artifact@v4 with: - name: geyser-voice-artifact - path: build/libs/ + name: geyservoice-release + path: release-artifacts - name: Create release if: github.event.inputs.status == 'stable' - uses: ncipollo/release-action@v1.13.0 + uses: ncipollo/release-action@v1.14.0 with: prerelease: false tag: ${{ github.event.inputs.tag }} - artifacts: | - build/libs/GeyserVoice-*.jar - env: - GITHUB_REPOSITORY: AvionBlock/GeyserVoice + artifacts: release-artifacts/*.jar + artifactErrorsFailBuild: true + allowUpdates: true + replacesArtifacts: true - name: Create pre-release if: github.event.inputs.status != 'stable' - uses: ncipollo/release-action@v1.13.0 + uses: ncipollo/release-action@v1.14.0 with: prerelease: true tag: ${{ github.event.inputs.tag }} - artifacts: | - build/libs/GeyserVoice-*.jar - env: - GITHUB_REPOSITORY: AvionBlock/GeyserVoice + artifacts: release-artifacts/*.jar + artifactErrorsFailBuild: true + allowUpdates: true + replacesArtifacts: true - name: Create Modrinth release + if: github.event.inputs.publish_modrinth == 'true' uses: dsx137/modrinth-release-action@main env: - MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} with: - name: GeyserVoice ${{ github.event.inputs.tag }} - project_id: WtPu56Wa - loaders: paper, spigot, bukkit, purpur, velocity, bungeecord - game_versions: 1.20.2:1.21.10 - version_number: ${{ github.event.inputs.tag }} - featured: ${{ github.event.inputs.status == 'stable' }} - version_type: ${{ github.event.inputs.status == 'stable' && 'release' || 'beta' }} - files: | - ./build/libs/GeyserVoice-*.jar + name: GeyserVoice ${{ github.event.inputs.tag }} + project_id: WtPu56Wa + loaders: paper, purpur, velocity, bungeecord + game_versions: 1.21.11:26.1 + version_number: ${{ github.event.inputs.tag }} + featured: ${{ github.event.inputs.status == 'stable' }} + version_type: ${{ github.event.inputs.status == 'stable' && 'release' || 'beta' }} + files: release-artifacts/*.jar diff --git a/.gitignore b/.gitignore index ccfa976..77d08ec 100644 --- a/.gitignore +++ b/.gitignore @@ -236,4 +236,4 @@ nbdist/ # End of https://www.gitignore.io/api/git,java,maven,eclipse,netbeans,jetbrains+all,visualstudiocode ### Gradle ### -.gradle \ No newline at end of file +.gradle* \ No newline at end of file diff --git a/README.md b/README.md index 06833be..dfa48cb 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,26 @@ ## GeyserVoice - Minecraft Proximity Voice Chat Plugin -GeyserVoice is a Java plugin designed to enhance the multiplayer gaming experience on Minecraft servers by integrating the [VoiceCraft](https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat/) Proximity Chat application. The plugin facilitates cross-platform communication, allowing players on both Java and Bedrock editions to seamlessly engage in proximity-based voice conversations. +GeyserVoice is a Java-side bridge for VoiceCraft built around `McApi TCP`. It now ships dedicated runtimes for `Paper`, `Velocity`, and `BungeeCord`, with direct Paper mode and proxy-relay mode for multi-server networks. ### Features -- Cross-Platform Communication: - GeyserVoice bridges the gap between Minecraft Java Edition and Bedrock Edition, enabling players on different platforms to communicate through the VoiceCraft Proximity Chat system. - -- Immersive Proximity Chat: - Experience a more immersive and realistic gameplay environment with proximity-based voice chat. Engage in conversations with nearby players, enhancing teamwork and coordination. +- `McApi TCP` transport only for VoiceCraft communication +- managed VoiceCraft runtime download/startup on direct Paper servers +- proxy relay mode for `Velocity` and `BungeeCord` +- server-side positioning updates from each Paper backend through the proxy ### How It Works -Installation: Simply install the GeyserVoice plugin on your Minecraft Java server. Make sure to follow the setup instructions to integrate it seamlessly with the VoiceCraft Proximity Chat application. +Direct Paper mode: +- install GeyserVoice on the Paper server +- let the plugin download/start VoiceCraft locally if desired +- the Paper plugin connects to VoiceCraft over `McApi TCP` + +Proxy mode: +- install GeyserVoice on each Paper backend and on the proxy +- enable `config.server-behind-proxy: true` on the Paper backends +- the proxy plugin owns the VoiceCraft connection +- backend Paper servers stream player snapshots to the proxy through plugin messages ### Getting Started @@ -26,6 +34,8 @@ We welcome contributions from the community to improve and expand the functional GeyserVoice is licensed under the MIT License. Feel free to use, modify, and distribute the plugin in accordance with the terms of the license. -### Proxy server support +### Status -GeyserVoice also supports usage with Velocity and Bungeecord networks. Just install the .jar on your proxy server and on your paper server(s). Be sure to edit the config of the paper server(s) to set `server-behind-proxy` to `true` and then reload using `voice reload`. P.s. You don't need to set the server address, port and keys on the paper server(s), this is only needed on the proxy server. +Current supported runtime paths: +- `Paper -> McApi TCP -> VoiceCraft` +- `Paper -> Proxy relay -> McApi TCP -> VoiceCraft` diff --git a/build.gradle b/build.gradle index fce807e..a4cefc4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,111 +1,67 @@ plugins { - id 'java' - id 'io.github.goooler.shadow' version '8.1.8' - id 'net.kyori.blossom' version '2.1.0' + id 'base' } -group = 'io.greitan' -version = project.hasProperty('VERSION') ? project.VERSION : 'UNKNOWN' -description = "Plugin that adds support for using VoiceCraft on Java servers." +group = 'team.avion' +version = project.hasProperty('VERSION') ? project.VERSION : '0.1.0-SNAPSHOT' +description = 'Plugin that adds support for using VoiceCraft on Java servers.' -repositories { - mavenCentral() - maven { - name = "CodeMC" - url = uri("https://repo.codemc.io/repository/maven-public/") - } - maven { - name = "PaperMC" - url = "https://repo.papermc.io/repository/maven-public/" - } - maven { - name = "PlaceholderAPI" - url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/' - } - maven { - name = "Jitpack" - url = 'https://jitpack.io' - } - maven { - name = "Bungeecord" - url 'https://oss.sonatype.org/content/repositories/snapshots' - } +ext { + pluginName = 'GeyserVoice' + pluginUrl = 'https://geyservoice.avion.team' + pluginAuthor = 'lil-jon-crunk' + javaTarget = JavaVersion.VERSION_21 + voiceCraft = [ + githubRepository : 'AvionBlock/VoiceCraft', + release : 'latest', + installDirectory : 'voicecraft-runtime', + executableBaseName : 'VoiceCraft.Server', + protocolMajor : 1, + protocolMinor : 6, + protocolPatch : 0 + ] } -dependencies { - compileOnly 'dev.folia:folia-api:1.20.2-R0.1-SNAPSHOT' - compileOnly 'io.papermc.paper:paper-api:1.20.2-R0.1-SNAPSHOT' - compileOnly 'com.velocitypowered:velocity-api:3.3.0-SNAPSHOT' - annotationProcessor 'com.velocitypowered:velocity-api:3.3.0-SNAPSHOT' - compileOnly 'net.md-5:bungeecord-api:1.20-R0.2' - - compileOnly 'me.clip:placeholderapi:2.11.6' - compileOnly 'org.projectlombok:lombok:1.18.30' - annotationProcessor 'org.projectlombok:lombok:1.18.30' - - implementation('com.fasterxml.jackson.core:jackson-databind:2.15.2') - implementation('com.fasterxml.jackson.core:jackson-core:2.15.2') - implementation("com.github.simplix-softworks:simplixstorage:3.2.3") -} +allprojects { + group = rootProject.group + version = rootProject.version -sourceSets { - main { - blossom { - javaSources { - property("name", "GeyserVoice") - property("version", version.toString()) - property("description", description) - property("url", "https://uninode.de") - } - resources { - property("name", "GeyserVoice") - property("version", version.toString()) - property("description", description) - property("url", "https://uninode.de") - } + repositories { + mavenCentral() + maven { + name = "PaperMC" + url = uri("https://repo.papermc.io/repository/maven-public/") + } + maven { + name = "PlaceholderAPI" + url = uri("https://repo.extendedclip.com/releases/") + } + maven { + name = "Bungeecord" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") } } } -tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' -} - -tasks.withType(Javadoc) { - options.encoding = 'UTF-8' - options.charSet = 'UTF-8' -} +subprojects { + apply plugin: 'java-library' -tasks.withType(Test) { - systemProperty 'file.encoding', 'UTF-8' -} - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} + java { + sourceCompatibility = rootProject.ext.javaTarget + targetCompatibility = rootProject.ext.javaTarget + withSourcesJar() + } -shadowJar { - archiveClassifier.set('all') - relocate 'com.tcoded.folialib', 'io.greitan.avion.folialib' -} + tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + } -task deleteUnusableJar(type: Delete) { - doLast { - file("build/libs").listFiles().each { File file -> - if (file.name.endsWith(".jar") && !file.name.contains("-all")) { - file.delete() - } - } + tasks.withType(Javadoc).configureEach { + options.encoding = 'UTF-8' + options.charSet = 'UTF-8' } -} -task copyToTestServer(type: Copy) { - from shadowJar - into "D:\\JavaTest\\plugins" + tasks.withType(Test).configureEach { + systemProperty 'file.encoding', 'UTF-8' + } } -copyToTestServer.dependsOn shadowJar - -build.finalizedBy shadowJar -shadowJar.finalizedBy deleteUnusableJar -if (project.hasProperty('DEV')) shadowJar.finalizedBy copyToTestServer diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9..d997cfc 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df97d72..c61a118 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6..739907d 100644 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -173,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -206,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/modules/bungeecord/build.gradle b/modules/bungeecord/build.gradle new file mode 100644 index 0000000..d1b7d93 --- /dev/null +++ b/modules/bungeecord/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'com.gradleup.shadow' version '9.4.1' +} + +description = 'BungeeCord adapter for GeyserVoice.' + +dependencies { + implementation project(':core') + compileOnly 'net.md-5:bungeecord-api:26.1-R0.1-SNAPSHOT' + compileOnly 'org.projectlombok:lombok:1.18.44' + annotationProcessor 'org.projectlombok:lombok:1.18.44' +} + +processResources { + inputs.properties([ + name : rootProject.ext.pluginName, + version : project.version, + description: rootProject.description, + url : rootProject.ext.pluginUrl, + author : rootProject.ext.pluginAuthor + ]) + + filesMatching('bungee.yml') { + expand( + name: rootProject.ext.pluginName, + version: project.version, + description: rootProject.description, + url: rootProject.ext.pluginUrl, + author: rootProject.ext.pluginAuthor + ) + } +} + +tasks.jar { + archiveBaseName.set("${rootProject.ext.pluginName}-bungeecord") + archiveClassifier.set('thin') +} + +tasks.shadowJar { + archiveBaseName.set("${rootProject.ext.pluginName}-bungeecord") + archiveClassifier.set('') +} + +tasks.build { + dependsOn tasks.shadowJar +} diff --git a/modules/bungeecord/src/main/java/team/avion/bungeecord/GeyserVoice.java b/modules/bungeecord/src/main/java/team/avion/bungeecord/GeyserVoice.java new file mode 100644 index 0000000..7d28792 --- /dev/null +++ b/modules/bungeecord/src/main/java/team/avion/bungeecord/GeyserVoice.java @@ -0,0 +1,392 @@ +package team.avion.bungeecord; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import lombok.Getter; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.scheduler.ScheduledTask; +import net.md_5.bungee.config.Configuration; +import net.md_5.bungee.config.ConfigurationProvider; +import net.md_5.bungee.config.YamlConfiguration; +import team.avion.bungeecord.commands.VoiceCommand; +import team.avion.bungeecord.listeners.PlayerJoinHandler; +import team.avion.bungeecord.listeners.PlayerQuitHandler; +import team.avion.bungeecord.listeners.PluginMessageHandler; +import team.avion.bungeecord.tasks.PositionsTask; +import team.avion.bungeecord.utils.BungeecordLogger; +import team.avion.bungeecord.utils.Language; +import team.avion.common.BaseGeyserVoice; +import team.avion.common.config.ConfigTemplateWriter; +import team.avion.common.localization.PluginLocalization; +import team.avion.proxy.VoiceCraftProxySessionManager; + +public class GeyserVoice extends Plugin implements BaseGeyserVoice { + private static final String TRANSPORT_HOST_PATH = "config.voicecraft.transport.host"; + private static final String TRANSPORT_PORT_PATH = "config.voicecraft.transport.port"; + private static final String TRANSPORT_LOGIN_TOKEN_PATH = "config.voicecraft.transport.login-token"; + private static final String LEGACY_HOST_PATH = "config.voicecraft.host"; + private static final String LEGACY_PORT_PATH = "config.voicecraft.port"; + private static final String LEGACY_LOGIN_TOKEN_PATH = "config.voicecraft.login-token"; + + private static @Getter Configuration config; + private static @Getter GeyserVoice instance; + + private @Getter boolean isConnected = false; + private @Getter String host = ""; + private @Getter int port = 0; + private @Getter String loginToken = ""; + private final @Getter Map playerBinds = new HashMap<>(); + private @Getter String token = ""; + private String lang; + private final @Getter PluginMessageHandler messageHandler = new PluginMessageHandler(this); + private final @Getter VoiceCraftProxySessionManager sessionManager; + + private @Getter ScheduledTask taskRunner; + + public final BungeecordLogger Logger = new BungeecordLogger(); + + public GeyserVoice() { + this.sessionManager = new VoiceCraftProxySessionManager(Logger); + } + + @Override + public void onEnable() { + instance = this; + + ensureLocalizedConfigExists(); + reloadConfig(); + lang = resolveConfiguredLanguage(); + Language.init(this); + + getProxy().registerChannel(PluginMessageHandler.CHANNEL); + getProxy().getPluginManager().registerListener(this, messageHandler); + getProxy().getPluginManager().registerCommand(this, new VoiceCommand(this, lang)); + getProxy().getPluginManager().registerListener(this, new PlayerJoinHandler(this, lang)); + getProxy().getPluginManager().registerListener(this, new PlayerQuitHandler(this, lang)); + + reload(); + } + + @Override + public void onDisable() { + getProxy().unregisterChannel(PluginMessageHandler.CHANNEL); + if (taskRunner != null) { + taskRunner.cancel(); + } + sessionManager.close(); + } + + @Override + public void reload() { + reloadConfig(); + lang = resolveConfiguredLanguage(); + Logger.info(Language.getMessage(lang, "plugin-config-loaded")); + Logger.info(Language.getMessage(lang, "plugin-command-executor")); + + host = getTransportHost(); + port = getTransportPort(); + loginToken = getTransportLoginToken(); + int proximityDistance = getConfig().getInt("config.voice.proximity-distance", 30); + boolean proximityToggle = getConfig().getBoolean("config.voice.proximity-toggle", true); + boolean voiceEffects = getConfig().getBoolean("config.voice.voice-effects", true); + sessionManager.configure(host, port, loginToken, proximityDistance, proximityToggle, voiceEffects); + + if (getConfig().getBoolean("config.auto-reconnect", true)) { + isConnected = reconnect(true); + } + + int positionTaskInterval = getConfig().getInt("config.voice.position-update-interval-ticks", 1); + if (taskRunner != null) { + taskRunner.cancel(); + } + taskRunner = getProxy().getScheduler().schedule(this, new PositionsTask(this, lang), 1, + 50L * positionTaskInterval, TimeUnit.MILLISECONDS); + + updateSettings(proximityDistance, proximityToggle, voiceEffects); + } + + @Override + public Boolean connect(String host, int port, String loginToken) { + if (host == null || loginToken == null) { + Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); + return false; + } + + getConfig().set(TRANSPORT_HOST_PATH, host); + getConfig().set(TRANSPORT_PORT_PATH, port); + getConfig().set(TRANSPORT_LOGIN_TOKEN_PATH, loginToken); + saveConfig(); + reload(); + return isConnected; + } + + @Override + public Boolean reconnect(Boolean force) { + if (isConnected && !force) { + return true; + } + if (isConnected) { + disconnect("Reconnecting to another server."); + } + + if (Objects.nonNull(host) && Objects.nonNull(loginToken)) { + boolean connected = sessionManager.connect(); + String sessionToken = connected ? sessionManager.getSessionToken() : null; + if (sessionToken != null && !sessionToken.isBlank()) { + Logger.info(Language.getMessage(lang, "plugin-connect-connected")); + isConnected = true; + token = sessionToken; + } else { + Logger.warn(Language.getMessage(lang, "plugin-connect-failed")); + } + return isConnected; + } + + Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); + return false; + } + + @Override + public void disconnect(String reason) { + if (!isConnected) { + return; + } + + if (sessionManager.isConnected()) { + sessionManager.disconnect(); + } + isConnected = false; + token = ""; + playerBinds.clear(); + + String disconnectMessage = Language.getMessage(lang, "plugin-connection-disconnect").replace("$reason", reason); + Logger.info(disconnectMessage); + if (getConfig().getBoolean("config.voice.send-voicecraft-disconnect-message", true)) { + getProxy().broadcast(new ComponentBuilder(disconnectMessage).color(ChatColor.YELLOW).create()); + } + } + + @Override + public void disconnect() { + disconnect("N.A."); + } + + public Boolean bind(int playerKey, ProxiedPlayer player, int tries) { + if (!isConnected) { + return false; + } + if (playerBinds.getOrDefault(player.getName(), false)) { + return true; + } + + boolean bound = sessionManager.bindPlayer(playerKey, player.getUniqueId().toString(), player.getName()); + if (!bound && tries == 0 && !sessionManager.isConnected()) { + isConnected = reconnect(true); + return bind(playerKey, player, 1); + } + if (!bound) { + messageHandler.sendPlayerBindSync(player); + return false; + } + + playerBinds.put(player.getName(), true); + messageHandler.sendPlayerBindSync(player); + Logger.info(Language.getMessage(lang, "player-binded").replace("$player", player.getName())); + if (getConfig().getBoolean("config.voice.send-bind-message", true)) { + getProxy().broadcast(new ComponentBuilder(player.getName()).bold(true) + .append(new ComponentBuilder(Language.getMessage(lang, "player-binded").replace("$player", "")).color(ChatColor.DARK_GREEN).create()) + .create()); + } + return true; + } + + public Boolean bind(int playerKey, ProxiedPlayer player) { + return bind(playerKey, player, 0); + } + + @Override + public Boolean bindFake(int playerKey, String name, int tries) { + if (!isConnected) { + return false; + } + boolean bound = sessionManager.bindFakePlayer(playerKey, "fake:" + playerKey, name); + if (!bound && tries == 0 && !sessionManager.isConnected()) { + isConnected = reconnect(true); + return bindFake(playerKey, name, 1); + } + if (bound) { + playerBinds.put(name, true); + } + return bound; + } + + @Override + public Boolean bindFake(int playerKey, String name) { + return bindFake(playerKey, name, 0); + } + + public Boolean disconnectPlayer(ProxiedPlayer player, int tries) { + if (!isConnected) { + return false; + } + + boolean disconnected = sessionManager.unbindPlayer(player.getUniqueId().toString()); + if (!disconnected && tries == 0 && !sessionManager.isConnected()) { + isConnected = reconnect(true); + return disconnectPlayer(player, 1); + } + if (disconnected) { + playerBinds.remove(player.getName()); + messageHandler.sendBindSync(player.getName(), false); + } + return disconnected; + } + + public Boolean disconnectPlayer(ProxiedPlayer player) { + return disconnectPlayer(player, 0); + } + + public boolean handleProxyBindRequest(int bindingKey, String playerId, String playerName) { + boolean bound = sessionManager.bindPlayer(bindingKey, playerId, playerName); + if (bound) { + playerBinds.put(playerName, true); + } + return bound; + } + + public void handleProxyUnbindRequest(String playerId, String playerName) { + sessionManager.unbindPlayer(playerId); + playerBinds.remove(playerName); + } + + @Override + public Boolean updateSettings(int proximityDistance, Boolean proximityToggle, Boolean voiceEffects) { + if (!isConnected) { + return false; + } + return sessionManager.updateSettings(proximityDistance, proximityToggle, voiceEffects); + } + + @Override + public void setNotConnected() { + if (!isConnected) { + return; + } + isConnected = false; + token = ""; + playerBinds.clear(); + sessionManager.close(); + } + + public void reloadConfig() { + try { + config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(new File(getDataFolder(), "config.yml")); + } catch (IOException exception) { + Logger.error("Could not reload config: " + exception.getMessage()); + } + } + + @Override + public void saveConfig() { + try { + ConfigurationProvider.getProvider(YamlConfiguration.class).save(config, new File(getDataFolder(), "config.yml")); + } catch (IOException exception) { + Logger.error("Could not save config: " + exception.getMessage()); + } + } + + @Override + public void saveResource(String resourcePath) { + saveResource(resourcePath, false); + } + + public void saveResource(String resourcePath, boolean replace) { + if (resourcePath == null || resourcePath.isEmpty()) { + throw new IllegalArgumentException("ResourcePath cannot be null or empty"); + } + + resourcePath = resourcePath.replace("\\", "/"); + try (InputStream in = getResourceAsStream(resourcePath)) { + if (in == null) { + throw new IllegalArgumentException("The embedded resource '" + resourcePath + "' cannot be found"); + } + + File outFile = new File(getDataFolder(), resourcePath); + File outDir = outFile.getParentFile(); + if (!outDir.exists()) { + outDir.mkdirs(); + } + + if (!outFile.exists() || replace) { + try (OutputStream out = new FileOutputStream(outFile)) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } + } + } catch (IOException ex) { + Logger.error("Could not save " + resourcePath + " to " + new File(getDataFolder(), resourcePath)); + } + } + + private String resolveConfiguredLanguage() { + return PluginLocalization.resolveConfiguredLanguage(getConfig().getString("config.lang", "system")); + } + + private void ensureLocalizedConfigExists() { + File target = new File(getDataFolder(), "config.yml"); + if (target.exists()) { + return; + } + + String language = PluginLocalization.resolveSystemLanguage(); + File parent = target.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + + try (InputStream input = openConfigTemplate(language)) { + if (input == null) { + throw new IOException("Missing embedded config template for language " + language); + } + ConfigTemplateWriter.write(input, target.toPath(), + Map.of("__GENERATED_LOGIN_TOKEN__", UUID.randomUUID().toString())); + } catch (IOException exception) { + Logger.error("Could not create localized config.yml: " + exception.getMessage()); + } + } + + private InputStream openConfigTemplate(String language) { + InputStream input = getResourceAsStream("config/" + language + ".yml"); + return input != null ? input : getResourceAsStream("config/en.yml"); + } + + private String getTransportHost() { + String value = getConfig().getString(TRANSPORT_HOST_PATH); + return value != null ? value : getConfig().getString(LEGACY_HOST_PATH); + } + + private int getTransportPort() { + int value = getConfig().getInt(TRANSPORT_PORT_PATH, -1); + return value > 0 ? value : getConfig().getInt(LEGACY_PORT_PATH); + } + + private String getTransportLoginToken() { + String value = getConfig().getString(TRANSPORT_LOGIN_TOKEN_PATH); + return value != null ? value : getConfig().getString(LEGACY_LOGIN_TOKEN_PATH); + } +} diff --git a/modules/bungeecord/src/main/java/team/avion/bungeecord/commands/VoiceCommand.java b/modules/bungeecord/src/main/java/team/avion/bungeecord/commands/VoiceCommand.java new file mode 100644 index 0000000..f5beb7f --- /dev/null +++ b/modules/bungeecord/src/main/java/team/avion/bungeecord/commands/VoiceCommand.java @@ -0,0 +1,68 @@ +package team.avion.bungeecord.commands; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.TabExecutor; +import team.avion.bungeecord.GeyserVoice; +import team.avion.bungeecord.utils.Language; +import team.avion.common.commands.BaseVoiceCommand; +import team.avion.common.utils.DoubleStringOperation; +import team.avion.common.utils.IntegerOperation; +import team.avion.common.utils.StringOperation; + +public class VoiceCommand extends Command implements TabExecutor { + private final BaseVoiceCommand voiceCommand; + private final GeyserVoice plugin; + private final String lang; + + public VoiceCommand(GeyserVoice plugin, String lang) { + super("voice"); + this.voiceCommand = new BaseVoiceCommand(plugin); + this.plugin = plugin; + this.lang = lang; + } + + @Override + public void execute(CommandSender sender, String[] args) { + voiceCommand.onCommand(args, plugin.isConnected(), sender instanceof ProxiedPlayer, + permission -> !(sender instanceof ProxiedPlayer) || sender.hasPermission(permission), + new DoubleStringOperation() { + @Override + public void execute(String text, String rawColor) { + ChatColor color = ChatColor.RED; + if ("aqua".equals(rawColor)) { + color = ChatColor.AQUA; + } else if ("green".equals(rawColor)) { + color = ChatColor.GREEN; + } else if ("yellow".equals(rawColor)) { + color = ChatColor.YELLOW; + } + + if (sender instanceof ProxiedPlayer) { + sender.sendMessage(new ComponentBuilder(Language.getMessage(lang, text)).color(color).create()); + } else { + plugin.Logger.info(Language.getMessage(lang, text)); + } + } + }, + new IntegerOperation() { + @Override + public boolean execute(int key) { + return sender instanceof ProxiedPlayer player && plugin.bind(key, player); + } + }); + } + + @Override + public Iterable onTabComplete(CommandSender sender, String[] args) { + return voiceCommand.onTabComplete(args, new StringOperation() { + @Override + public boolean execute(String permission) { + return sender.hasPermission(permission); + } + }); + } +} diff --git a/modules/bungeecord/src/main/java/team/avion/bungeecord/listeners/PlayerJoinHandler.java b/modules/bungeecord/src/main/java/team/avion/bungeecord/listeners/PlayerJoinHandler.java new file mode 100644 index 0000000..039f616 --- /dev/null +++ b/modules/bungeecord/src/main/java/team/avion/bungeecord/listeners/PlayerJoinHandler.java @@ -0,0 +1,19 @@ +package team.avion.bungeecord.listeners; + +import net.md_5.bungee.api.event.ServerConnectedEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; +import team.avion.bungeecord.GeyserVoice; + +public class PlayerJoinHandler implements Listener { + private final GeyserVoice plugin; + + public PlayerJoinHandler(GeyserVoice plugin, String lang) { + this.plugin = plugin; + } + + @EventHandler + public void onPlayerConnect(ServerConnectedEvent event) { + plugin.getMessageHandler().sendPlayerBindSync(event.getPlayer()); + } +} diff --git a/src/main/java/io/greitan/avion/bungeecord/listeners/PlayerQuitHandler.java b/modules/bungeecord/src/main/java/team/avion/bungeecord/listeners/PlayerQuitHandler.java similarity index 69% rename from src/main/java/io/greitan/avion/bungeecord/listeners/PlayerQuitHandler.java rename to modules/bungeecord/src/main/java/team/avion/bungeecord/listeners/PlayerQuitHandler.java index f430d37..e212806 100644 --- a/src/main/java/io/greitan/avion/bungeecord/listeners/PlayerQuitHandler.java +++ b/modules/bungeecord/src/main/java/team/avion/bungeecord/listeners/PlayerQuitHandler.java @@ -1,16 +1,15 @@ -package io.greitan.avion.bungeecord.listeners; +package team.avion.bungeecord.listeners; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.event.EventHandler; -import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.event.PlayerDisconnectEvent; -import io.greitan.avion.bungeecord.GeyserVoice; -import io.greitan.avion.bungeecord.utils.Language; -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; +import team.avion.bungeecord.GeyserVoice; +import team.avion.bungeecord.utils.Language; public class PlayerQuitHandler implements Listener { - private final GeyserVoice plugin; private final String lang; @@ -24,6 +23,7 @@ public void onPlayerQuit(PlayerDisconnectEvent event) { ProxiedPlayer player = event.getPlayer(); boolean isBound = plugin.getPlayerBinds().getOrDefault(player.getName(), false); + plugin.getSessionManager().removePlayerSnapshot(player.getUniqueId().toString()); if (plugin.isConnected() && isBound) { handlePlayerDisconnect(player); } @@ -31,23 +31,16 @@ public void onPlayerQuit(PlayerDisconnectEvent event) { private void handlePlayerDisconnect(ProxiedPlayer player) { boolean isDisconnected = plugin.disconnectPlayer(player); - - plugin.playerDataList.remove(player.getUniqueId().toString()); - - String playerName = player.getName(); - String disconnectMessage = Language.getMessage(lang, "player-disconnect-success").replace("$player", - playerName); + String disconnectMessage = Language.getMessage(lang, "player-disconnect-success").replace("$player", player.getName()); if (isDisconnected) { plugin.Logger.info(disconnectMessage); - - boolean sendDisconnectMessage = GeyserVoice.getConfig().getBoolean("config.voice.send-disconnect-message"); - if (sendDisconnectMessage) { + if (GeyserVoice.getConfig().getBoolean("config.voice.send-disconnect-message", true)) { plugin.getProxy().broadcast(new ComponentBuilder(disconnectMessage).color(ChatColor.YELLOW).create()); } } else { - plugin.Logger.error(Language.getMessage(lang, "player-disconnect-failed").replace("$player", playerName)); - plugin.getPlayerBinds().remove(playerName); + plugin.Logger.error(Language.getMessage(lang, "player-disconnect-failed").replace("$player", player.getName())); + plugin.getPlayerBinds().remove(player.getName()); } } } diff --git a/modules/bungeecord/src/main/java/team/avion/bungeecord/listeners/PluginMessageHandler.java b/modules/bungeecord/src/main/java/team/avion/bungeecord/listeners/PluginMessageHandler.java new file mode 100644 index 0000000..bfca800 --- /dev/null +++ b/modules/bungeecord/src/main/java/team/avion/bungeecord/listeners/PluginMessageHandler.java @@ -0,0 +1,68 @@ +package team.avion.bungeecord.listeners; + +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.connection.Server; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.event.PluginMessageEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; +import team.avion.bungeecord.GeyserVoice; +import team.avion.proxy.ProxyMessageCodec; +import team.avion.proxy.ProxyPlayerSnapshot; + +public class PluginMessageHandler implements Listener { + public static final String CHANNEL = ProxyMessageCodec.CHANNEL_NAME; + + private final GeyserVoice plugin; + + public PluginMessageHandler(GeyserVoice plugin) { + this.plugin = plugin; + } + + public void sendPlayerBindSync(ProxiedPlayer player) { + sendBindSync(player.getName(), plugin.getPlayerBinds().getOrDefault(player.getName(), false)); + } + + public void sendBindSync(String playerName, boolean bound) { + byte[] message = ProxyMessageCodec.encodeBindSync(playerName, bound); + for (ServerInfo server : plugin.getProxy().getServers().values()) { + try { + server.sendData(CHANNEL, message, true); + } catch (Exception ignored) { + } + } + } + + @EventHandler + public void onPluginMessageReceived(PluginMessageEvent event) { + if (!CHANNEL.equals(event.getTag())) { + return; + } + + String serverName; + if (event.getSender() instanceof Server backend) { + serverName = backend.getInfo().getName(); + } else if (event.getSender() instanceof ProxiedPlayer player) { + serverName = player.getServer().getInfo().getName(); + } else { + return; + } + + byte[] data = event.getData(); + String type = ProxyMessageCodec.peekType(data); + if (ProxyMessageCodec.SNAPSHOT.equals(type)) { + ProxyPlayerSnapshot snapshot = ProxyMessageCodec.decodeSnapshot(data); + plugin.getSessionManager().updatePlayerSnapshot(snapshot.withDimensionId(serverName + "_" + snapshot.dimensionId())); + } else if (ProxyMessageCodec.BIND_REQUEST.equals(type)) { + ProxyMessageCodec.BindRequest bindRequest = ProxyMessageCodec.decodeBindRequest(data); + boolean bound = plugin.handleProxyBindRequest(bindRequest.bindingKey(), bindRequest.playerId(), bindRequest.playerName()); + sendBindSync(bindRequest.playerName(), bound); + } else if (ProxyMessageCodec.UNBIND_REQUEST.equals(type)) { + ProxyMessageCodec.UnbindRequest unbindRequest = ProxyMessageCodec.decodeUnbindRequest(data); + plugin.handleProxyUnbindRequest(unbindRequest.playerId(), unbindRequest.playerName()); + sendBindSync(unbindRequest.playerName(), false); + } + + event.setCancelled(true); + } +} diff --git a/modules/bungeecord/src/main/java/team/avion/bungeecord/tasks/PositionsTask.java b/modules/bungeecord/src/main/java/team/avion/bungeecord/tasks/PositionsTask.java new file mode 100644 index 0000000..4f09a03 --- /dev/null +++ b/modules/bungeecord/src/main/java/team/avion/bungeecord/tasks/PositionsTask.java @@ -0,0 +1,80 @@ +package team.avion.bungeecord.tasks; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.ComponentBuilder; +import team.avion.bungeecord.GeyserVoice; +import team.avion.bungeecord.utils.Language; + +public class PositionsTask implements Runnable { + private final GeyserVoice plugin; + private final String lang; + private int reconnectRetries = 0; + private boolean reconnecting = false; + + public PositionsTask(GeyserVoice plugin, String lang) { + this.plugin = plugin; + this.lang = lang; + } + + @Override + public void run() { + if (reconnecting) { + reconnect(); + return; + } + + if (plugin.isConnected() && plugin.getSessionManager().tick()) { + return; + } + + if (!plugin.isConnected()) { + return; + } + + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-lost")); + plugin.setNotConnected(); + + if (GeyserVoice.getConfig().getBoolean("config.auto-reconnect", true)) { + if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message", true)) { + plugin.getProxy().broadcast(new ComponentBuilder(Language.getMessage(lang, "plugin-connection-lost-reconnect")).color(ChatColor.RED).create()); + } + reconnectRetries = 0; + reconnecting = true; + reconnect(); + return; + } + + if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message", true)) { + plugin.getProxy().broadcast(new ComponentBuilder(Language.getMessage(lang, "plugin-connection-lost")).color(ChatColor.RED).create()); + } + plugin.getTaskRunner().cancel(); + } + + private void reconnect() { + if (reconnectRetries >= 5) { + reconnecting = false; + plugin.Logger.error(Language.getMessage(lang, "plugin-connection-reconnecting-failed")); + if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message", true)) { + plugin.getProxy().broadcast(new ComponentBuilder(Language.getMessage(lang, "plugin-connection-reconnecting-failed")).color(ChatColor.RED).create()); + } + plugin.getTaskRunner().cancel(); + return; + } + + reconnectRetries++; + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-attempt").replace("$attempt", Integer.toString(reconnectRetries))); + + if (plugin.reconnect(true)) { + reconnecting = false; + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-success")); + if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message", true)) { + plugin.getProxy().broadcast(new ComponentBuilder(Language.getMessage(lang, "plugin-connection-reconnecting-success")).color(ChatColor.GREEN).create()); + } + return; + } + + if (reconnectRetries < 5) { + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-failed-retry")); + } + } +} diff --git a/modules/bungeecord/src/main/java/team/avion/bungeecord/utils/BungeecordLogger.java b/modules/bungeecord/src/main/java/team/avion/bungeecord/utils/BungeecordLogger.java new file mode 100644 index 0000000..5c96d65 --- /dev/null +++ b/modules/bungeecord/src/main/java/team/avion/bungeecord/utils/BungeecordLogger.java @@ -0,0 +1,41 @@ +package team.avion.bungeecord.utils; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.ComponentBuilder; +import team.avion.bungeecord.GeyserVoice; +import team.avion.common.utils.BaseLogger; + +public class BungeecordLogger extends BaseLogger { + @Override + public void info(String msg) { + console().sendMessage(prefix().append(msg).color(ChatColor.WHITE).bold(true).create()); + } + + @Override + public void warn(String msg) { + console().sendMessage(prefix().append(msg).color(ChatColor.YELLOW).bold(true).create()); + } + + @Override + public void error(String msg) { + console().sendMessage(prefix().append(msg).color(ChatColor.RED).bold(true).create()); + } + + @Override + public void debug(String msg) { + if (GeyserVoice.getConfig().getBoolean("config.debug", false)) { + console().sendMessage(prefix().append(msg).color(ChatColor.BLUE).bold(true).create()); + } + } + + private CommandSender console() { + return GeyserVoice.getInstance().getProxy().getConsole(); + } + + private ComponentBuilder prefix() { + return new ComponentBuilder("[").color(ChatColor.WHITE).bold(true) + .append("GeyserVoice").color(ChatColor.LIGHT_PURPLE).bold(true) + .append("] ").color(ChatColor.WHITE).bold(true); + } +} diff --git a/modules/bungeecord/src/main/java/team/avion/bungeecord/utils/Language.java b/modules/bungeecord/src/main/java/team/avion/bungeecord/utils/Language.java new file mode 100644 index 0000000..16308c5 --- /dev/null +++ b/modules/bungeecord/src/main/java/team/avion/bungeecord/utils/Language.java @@ -0,0 +1,48 @@ +package team.avion.bungeecord.utils; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import net.md_5.bungee.config.Configuration; +import net.md_5.bungee.config.ConfigurationProvider; +import net.md_5.bungee.config.YamlConfiguration; +import team.avion.common.localization.PluginLocalization; +import team.avion.bungeecord.GeyserVoice; + +public final class Language { + private static final Map languageConfigs = new HashMap<>(); + private static final String DEFAULT_LANGUAGE = PluginLocalization.DEFAULT_LANGUAGE; + + private Language() { + } + + public static void init(GeyserVoice plugin) { + File languageFolder = new File(plugin.getDataFolder(), "locale"); + languageFolder.mkdirs(); + plugin.saveResource("locale/en.yml"); + plugin.saveResource("locale/ru.yml"); + plugin.saveResource("locale/nl.yml"); + plugin.saveResource("locale/ja.yml"); + + languageConfigs.clear(); + File[] files = languageFolder.listFiles((dir, name) -> name.endsWith(".yml")); + if (files == null) { + return; + } + + for (File file : files) { + try { + languageConfigs.put(file.getName().replace(".yml", ""), ConfigurationProvider.getProvider(YamlConfiguration.class).load(file)); + } catch (IOException ignored) { + } + } + } + + public static String getMessage(String language, String key) { + String resolvedLanguage = PluginLocalization.resolveConfiguredLanguage(language); + Configuration config = languageConfigs.getOrDefault(resolvedLanguage, languageConfigs.get(DEFAULT_LANGUAGE)); + return config == null ? key : config.getString("messages." + key, key); + } +} diff --git a/modules/bungeecord/src/main/resources/bungee.yml b/modules/bungeecord/src/main/resources/bungee.yml new file mode 100644 index 0000000..591ec48 --- /dev/null +++ b/modules/bungeecord/src/main/resources/bungee.yml @@ -0,0 +1,26 @@ +name: GeyserVoice +version: 0.1.0-SNAPSHOT +main: team.avion.bungeecord.GeyserVoice +description: Plugin that adds support for using VoiceCraft on Java servers. +author: lil-jon-crunk +website: https://geyservoice.avion.team + +commands: + voice: + permission: voice.cmd + +permissions: + voice.cmd: + default: true + voice.connect: + default: op + voice.reconnect: + default: op + voice.disconnect: + default: op + voice.bind: + default: true + voice.bindfake: + default: op + voice.reload: + default: op diff --git a/modules/bungeecord/src/main/resources/config.yml b/modules/bungeecord/src/main/resources/config.yml new file mode 100644 index 0000000..69b64c0 --- /dev/null +++ b/modules/bungeecord/src/main/resources/config.yml @@ -0,0 +1,23 @@ +config: + debug: false + lang: "system" + auto-reconnect: true + + voicecraft: + transport: + host: "127.0.0.1" + port: 9050 + login-token: "__GENERATED_LOGIN_TOKEN__" + voice: + port: 1111 + + voice: + proximity-distance: 30 + proximity-toggle: true + voice-effects: true + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 diff --git a/modules/bungeecord/src/main/resources/config/en.yml b/modules/bungeecord/src/main/resources/config/en.yml new file mode 100644 index 0000000..3767bfd --- /dev/null +++ b/modules/bungeecord/src/main/resources/config/en.yml @@ -0,0 +1,23 @@ +config: + debug: false # Enable additional debug logging + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # Reconnect to VoiceCraft after a dropped connection + + voicecraft: + transport: + host: "127.0.0.1" # VoiceCraft TCP host + port: 9050 # VoiceCraft TCP port + login-token: "__GENERATED_LOGIN_TOKEN__" # VoiceCraft LoginToken + voice: + port: 1111 # VoiceCraft voice port + + voice: + proximity-distance: 30 # Maximum voice distance + proximity-toggle: true # Enable proximity processing + voice-effects: true # Enable cave/water voice effects + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # Position update interval in ticks diff --git a/modules/bungeecord/src/main/resources/config/ja.yml b/modules/bungeecord/src/main/resources/config/ja.yml new file mode 100644 index 0000000..740e1b5 --- /dev/null +++ b/modules/bungeecord/src/main/resources/config/ja.yml @@ -0,0 +1,23 @@ +config: + debug: false # 追加のデバッグログを有効化します + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # 切断時に VoiceCraft へ再接続します + + voicecraft: + transport: + host: "127.0.0.1" # VoiceCraft の TCP ホスト + port: 9050 # VoiceCraft の TCP ポート + login-token: "__GENERATED_LOGIN_TOKEN__" # VoiceCraft の LoginToken + voice: + port: 1111 # VoiceCraft voice port + + voice: + proximity-distance: 30 # ボイスの最大距離 + proximity-toggle: true # 距離ベース処理を有効化します + voice-effects: true # 洞窟/水中エフェクトを有効化します + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # 位置更新間隔(tick) diff --git a/modules/bungeecord/src/main/resources/config/nl.yml b/modules/bungeecord/src/main/resources/config/nl.yml new file mode 100644 index 0000000..db987fe --- /dev/null +++ b/modules/bungeecord/src/main/resources/config/nl.yml @@ -0,0 +1,23 @@ +config: + debug: false # Extra debuglogging inschakelen + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # Opnieuw verbinden met VoiceCraft na verbindingsverlies + + voicecraft: + transport: + host: "127.0.0.1" # VoiceCraft TCP-host + port: 9050 # VoiceCraft TCP-poort + login-token: "__GENERATED_LOGIN_TOKEN__" # VoiceCraft LoginToken + voice: + port: 1111 # VoiceCraft voice port + + voice: + proximity-distance: 30 # Maximale spraakafstand + proximity-toggle: true # Proximity-verwerking inschakelen + voice-effects: true # Grot- en watereffecten inschakelen + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # Positie-update-interval in ticks diff --git a/modules/bungeecord/src/main/resources/config/ru.yml b/modules/bungeecord/src/main/resources/config/ru.yml new file mode 100644 index 0000000..889f579 --- /dev/null +++ b/modules/bungeecord/src/main/resources/config/ru.yml @@ -0,0 +1,23 @@ +config: + debug: false # Включить дополнительное логирование + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # Переподключаться к VoiceCraft после потери соединения + + voicecraft: + transport: + host: "127.0.0.1" # TCP-хост VoiceCraft + port: 9050 # TCP-порт VoiceCraft + login-token: "__GENERATED_LOGIN_TOKEN__" # LoginToken сервера VoiceCraft + voice: + port: 1111 # Порт голосового сервера VoiceCraft + + voice: + proximity-distance: 30 # Максимальная дистанция голоса + proximity-toggle: true # Включить обработку proximity + voice-effects: true # Включить эффекты пещеры и воды + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # Интервал обновления позиций в тиках diff --git a/src/main/resources/locale/en.yml b/modules/bungeecord/src/main/resources/locale/en.yml similarity index 78% rename from src/main/resources/locale/en.yml rename to modules/bungeecord/src/main/resources/locale/en.yml index 083ac74..0d90ac5 100644 --- a/src/main/resources/locale/en.yml +++ b/modules/bungeecord/src/main/resources/locale/en.yml @@ -1,8 +1,8 @@ -# en +# en messages: commands: connect: - invalid-args: "Invalid command usage. Usage: /voice connect " + invalid-args: "Invalid command usage. Usage: /voice connect " disconnect: disconnecting: "Disconnecting from Server..." @@ -23,17 +23,9 @@ messages: invalid-args: "Invalid command usage. Usage: /voice bind " fake-invalid-args: "Invalid command usage. Usage: /voice bindfake " - updatefake: - updating: "Updating fake player..." - updated: "Update Successful!" - failed: "Update Failed!" - invalid-args: "Invalid command usage. Usage: /voice bindfake " - - clearautobind: "Successfully cleared autobind!" - reload: "Plugin reloaded!" - cmd-invalid-args: "Invalid command usage. Usage: /voice " # Error message for invalid command usage. + cmd-invalid-args: "Invalid command usage. Usage: /voice " # Error message for invalid command usage. cmd-not-player: "Only players can use this command!" # Error message for command use restricted to players only. cmd-not-exists: "This command is not exist." # Message indicating that the command does not exist. @@ -47,7 +39,6 @@ messages: plugin-connect-connecting: "Connecting/Linking Server..." # Successful connection to the voice chat server. plugin-connect-connected: "Login Accepted. Server successfully linked!" # Successful connection to the voice chat server. plugin-connect-failed: "Login Failed! Server not linked! Run /voice to reconnect." # Error message during connection to the voice chat server. - plugin-connect-proxy: "Proxy based server setup. Not connecting to voice chat server." # Velocity/Bungeecord based voice chat server connection. plugin-connect-invalid-data: "Connection failed. Invalid config." # Error message indicating failed connection due to invalid configuration. plugin-connection-lost: "Lost connection from voice chat server." # Message when connection is lost. @@ -60,11 +51,6 @@ messages: plugin-connection-already-disconnected: "Already disconnected from server." # Message when already disconnected plugin-connection-disconnect: "Disconnected from VOIP Server, Reason: $reason" # Message when disconnect - plugin-autobind-enabled: "Autobinding Enabled." # Message indicating we'll try autobind - plugin-autobind-binding: "Binding to key: $key!" # Message indicating that we're autobinding - plugin-autobind-success: "AutoBind success!" # Successful AutoBind execution. - plugin-autobind-failed: "AutoBind failed! Are you not in the VoiceCraft app?" # Error message during failed AutoBind execution. - player-disconnect-success: "Player $player left from the voice chat." # Message indicating player leaving the voice chat. player-disconnect-failed: "Player $player received an error when leaving the voice chat." # Error message during player leaving the voice chat. diff --git a/src/main/resources/locale/ja.yml b/modules/bungeecord/src/main/resources/locale/ja.yml similarity index 79% rename from src/main/resources/locale/ja.yml rename to modules/bungeecord/src/main/resources/locale/ja.yml index b675439..253ea23 100644 --- a/src/main/resources/locale/ja.yml +++ b/modules/bungeecord/src/main/resources/locale/ja.yml @@ -1,7 +1,7 @@ -messages: +messages: commands: connect: - invalid-args: "無効なコマンドの使用法です。使用法: /voice connect " + invalid-args: "無効なコマンドの使用法です。使用法: /voice connect " disconnect: disconnecting: "サーバーから切断しています..." @@ -22,17 +22,9 @@ messages: invalid-args: "コマンドの使用法が無効です。使用法: /voice bind " fake-invalid-args: "コマンドの使用法が無効です。使用法: /voice bindfake " - updatefake: - updating: "偽のプレイヤーを更新しています..." - updated: "更新に成功しました!" - failed: "更新に失敗しました!" - invalid-args: "コマンドの使用法が無効です。使用法: /voice bindfake " - - clearautobind: "自動バインドを正常にクリアしました!" - reload: "プラグインがリロードされました!" - cmd-invalid-args: "コマンドの使用法が無効です。使用法: /voice " # 無効なコマンドの使用に関するエラー メッセージ。 + cmd-invalid-args: "コマンドの使用法が無効です。使用法: /voice " # 無効なコマンドの使用に関するエラー メッセージ。 cmd-not-player: "このコマンドはプレイヤーのみが使用できます!" # コマンドの使用がプレイヤーのみに制限されていることを示すエラー メッセージ。 cmd-not-exists: "このコマンドは存在しません。" # コマンドが存在しないことを示すメッセージ。 @@ -46,7 +38,6 @@ messages: plugin-connect-connecting: "サーバーを接続/リンクしています..." # ボイスチャット サーバーへの接続に成功しました。 plugin-connect-connected: "ログインが受け入れられました。サーバーが正常にリンクされました!" # ボイスチャット サーバーへの接続に成功しました。 plugin-connect-failed: "ログインに失敗しました。サーバーがリンクされていません。再接続するには、/voice を実行してください。" # ボイスチャット サーバーへの接続中にエラー メッセージが表示されます。 - plugin-connect-proxy: "プロキシ ベースのサーバー設定。ボイスチャット サーバーに接続していません。" # Velocity/Bungeecord ベースのボイスチャット サーバー接続。 plugin-connect-invalid-data: "接続に失敗しました。無効な構成です。" # 無効な構成が原因で接続に失敗したことを示すエラー メッセージ。 plugin-connection-lost: "ボイスチャット サーバーからの接続が失われました。" # 接続が失われたときのメッセージ。 @@ -59,12 +50,7 @@ messages: plugin-connection-already-disconnected: "すでにサーバーから切断されています。" # すでに切断されている場合のメッセージ plugin-connection-disconnect: "VOIP サーバーから切断されました。理由: $reason" # 切断されている場合のメッセージ - plugin-autobind-enabled: "Autobinding Enabled." # Message indicating we'll try autobind - plugin-autobind-binding: "Binding to key: $key!" # Message indicating that we're autobinding - plugin-autobind-success: "AutoBind 成功!" # AutoBind の実行が成功しました。 - plugin-autobind-failed: "AutoBind に失敗しました。VoiceCraft アプリを使用していませんか?" # AutoBind の実行が失敗した場合のエラー メッセージ。 - player-disconnect-success: "プレイヤー $player がボイス チャットから退出しました。" # プレイヤーがボイス チャットを退出することを示すメッセージ。 player-disconnect-failed: "プレイヤー $player がボイス チャットを退出するときにエラーを受信しました。" # プレイヤーがボイス チャットを退出するときのエラー メッセージ。 - player-binded: "$player が VoiceCraft に接続しました。" # プレイヤーがボイス チャットに参加していることを示すメッセージ。 \ No newline at end of file + player-binded: "$player が VoiceCraft に接続しました。" # プレイヤーがボイス チャットに参加していることを示すメッセージ。 diff --git a/src/main/resources/locale/nl.yml b/modules/bungeecord/src/main/resources/locale/nl.yml similarity index 79% rename from src/main/resources/locale/nl.yml rename to modules/bungeecord/src/main/resources/locale/nl.yml index edfccce..9689ad0 100644 --- a/src/main/resources/locale/nl.yml +++ b/modules/bungeecord/src/main/resources/locale/nl.yml @@ -1,8 +1,8 @@ -# nl +# nl messages: commands: connect: - invalid-args: "Ongeldig commando gebruik. Gebruik: /voice connect " + invalid-args: "Ongeldig commando gebruik. Gebruik: /voice connect " disconnect: disconnecting: "Verbinding met Server verbreken..." @@ -22,17 +22,9 @@ messages: invalid-args: "Ongeldig commando gebruik. Gebruik: /voice bind " fake-invalid-args: "Ongeldig commando gebruik. Gebruik: /voice bindfake " - updatefake: - updating: "Updaten van fake speler..." - updated: "Update Succesvol!" - failed: "Update Mislukt!" - invalid-args: "Ongeldig commando gebruik. Gebruik: /voice bindfake " - - clearautobind: "Legen van autobind gelukt!" - reload: "Plugin herladen!" - cmd-invalid-args: "Ongeldig commando gebruik. Gebruik: /voice " # Error message for invalid command usage. + cmd-invalid-args: "Ongeldig commando gebruik. Gebruik: /voice " # Error message for invalid command usage. cmd-not-player: "Alleen spelers kunnen dit commando gebruiken!" # Error message for command use restricted to players only. cmd-not-exists: "Dit commando bestaat niet." # Message indicating that the command does not exist. @@ -46,7 +38,6 @@ messages: plugin-connect-connecting: "Verbinden/Linken van Server..." # Successful connection to the voice chat server. plugin-connect-connected: "Login Geaccepteerd. Server succesvol gelinkt." # Successful connection to the voice chat server. plugin-connect-failed: "Login Mislukt! Server niet gelinkt! Voer /voice uit om opnieuw verbinding te makne." # Error message during connection to the voice chat server. - plugin-connect-proxy: "Proxy gebaseerde server setup. Niet verbinden met voicechatserver." # Velocity based voice chat server connection. plugin-connect-invalid-data: "Verbinding mislukt. Ongeldige configuratie." # Error message indicating failed connection due to invalid configuration. plugin-connection-lost: "Verbinding met voicechatserver verloren." # Message when connection is lost. @@ -59,11 +50,6 @@ messages: plugin-connection-already-disconnected: "Verbinding was al verbroken met server." # Message when already disconnected plugin-connection-disconnect: "Verbinding met VOIP Server verbroken, Reden: $reason" # Message when disconnect - plugin-autobind-enabled: "Autobinden Ingeschakeld." # Message indicating we'll try autobind - plugin-autobind-binding: "Binden met sleutel: $key!" # Message indicating that we're autobinding - plugin-autobind-success: "AutoBind succes!" # Successful AutoBind execution. - plugin-autobind-failed: "AutoBind mislukt! Zit je niet in de VoiceCraft app?" # Error message during failed AutoBind execution. - player-disconnect-success: "Speler $player heeft de voicechat verlaten." # Message indicating player leaving the voice chat. player-disconnect-failed: "Speler $player ontving een foutmelding bij het verlaten van de voicechat." # Error message during player leaving the voice chat. diff --git a/src/main/resources/locale/ru.yml b/modules/bungeecord/src/main/resources/locale/ru.yml similarity index 79% rename from src/main/resources/locale/ru.yml rename to modules/bungeecord/src/main/resources/locale/ru.yml index c97df4c..75fc126 100644 --- a/src/main/resources/locale/ru.yml +++ b/modules/bungeecord/src/main/resources/locale/ru.yml @@ -1,4 +1,4 @@ -# ru +# ru messages: commands: connect: @@ -23,17 +23,9 @@ messages: invalid-args: "Неверное использование команды. Использование: /voice bind <ключ>" fake-invalid-args: "Неверное использование команды. Использование: /voice bindfake <ключ> <имя>" - updatefake: - updating: "Обновление фейкового игрока..." - updated: "Обновление успешно!" - failed: "Ошибка обновления!" - invalid-args: "Неверное использование команды. Использование: /voice bindfake <ключ> <имя>" - - clearautobind: "Автопривязка успешно очищена!" - reload: "Плагин перезагружен!" - cmd-invalid-args: "Неверное использование команды. Использование: /voice " + cmd-invalid-args: "Неверное использование команды. Использование: /voice " cmd-not-player: "Эту команду могут использовать только игроки!" cmd-not-exists: "Эта команда не существует." @@ -47,7 +39,6 @@ messages: plugin-connect-connecting: "Подключение/связь с сервером..." plugin-connect-connected: "Вход принят. Сервер успешно связан!" plugin-connect-failed: "Ошибка входа! Сервер не связан! Запустите /voice , чтобы повторить попытку." - plugin-connect-proxy: "Настроен сервер-прокси. Подключение к голосовому серверу не требуется." plugin-connect-invalid-data: "Ошибка подключения. Некорректная конфигурация." plugin-connection-lost: "Соединение с голосовым сервером потеряно." @@ -60,11 +51,6 @@ messages: plugin-connection-already-disconnected: "Вы уже отключены от сервера." plugin-connection-disconnect: "Отключено от VOIP-сервера, причина: $reason" - plugin-autobind-enabled: "Автопривязка включена." - plugin-autobind-binding: "Привязка к ключу: $key!" - plugin-autobind-success: "Автопривязка выполнена успешно!" - plugin-autobind-failed: "Ошибка автопривязки! Вы не находитесь в приложении VoiceCraft?" - player-disconnect-success: "Игрок $player покинул голосовой чат." player-disconnect-failed: "Игрок $player столкнулся с ошибкой при выходе из голосового чата." diff --git a/modules/core/build.gradle b/modules/core/build.gradle new file mode 100644 index 0000000..fad0961 --- /dev/null +++ b/modules/core/build.gradle @@ -0,0 +1,6 @@ +description = 'Core domain and application services for GeyserVoice.' + +dependencies { + compileOnly 'net.kyori:adventure-api:4.25.0' + compileOnly 'me.clip:placeholderapi:2.12.2' +} diff --git a/src/main/java/io/greitan/avion/common/BaseGeyserVoice.java b/modules/core/src/main/java/team/avion/common/BaseGeyserVoice.java similarity index 94% rename from src/main/java/io/greitan/avion/common/BaseGeyserVoice.java rename to modules/core/src/main/java/team/avion/common/BaseGeyserVoice.java index e4b70be..9fb7222 100644 --- a/src/main/java/io/greitan/avion/common/BaseGeyserVoice.java +++ b/modules/core/src/main/java/team/avion/common/BaseGeyserVoice.java @@ -1,4 +1,4 @@ -package io.greitan.avion.common; +package team.avion.common; // import lombok.Getter; import java.util.Map; @@ -8,7 +8,7 @@ public interface BaseGeyserVoice { public boolean isConnected = false; public String host = ""; public int port = 0; - public String serverKey = ""; + public String loginToken = ""; public Map playerBinds = new HashMap<>(); public String token = ""; public String lang = ""; @@ -23,10 +23,10 @@ public interface BaseGeyserVoice { * * @param host The host to connect to. * @param port The port to connect to. - * @param serverKey The server key. + * @param loginToken The VoiceCraft login token. * @return True if connected successfully, otherwise false. */ - abstract public Boolean connect(String host, int port, String serverKey); + abstract public Boolean connect(String host, int port, String loginToken); /** * Reconnects to the server. diff --git a/src/main/java/io/greitan/avion/common/commands/BaseVoiceCommand.java b/modules/core/src/main/java/team/avion/common/commands/BaseVoiceCommand.java similarity index 70% rename from src/main/java/io/greitan/avion/common/commands/BaseVoiceCommand.java rename to modules/core/src/main/java/team/avion/common/commands/BaseVoiceCommand.java index 8acbd9f..efd6a7d 100644 --- a/src/main/java/io/greitan/avion/common/commands/BaseVoiceCommand.java +++ b/modules/core/src/main/java/team/avion/common/commands/BaseVoiceCommand.java @@ -1,15 +1,14 @@ -package io.greitan.avion.common.commands; +package team.avion.common.commands; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import java.lang.NumberFormatException; -import io.greitan.avion.common.BaseGeyserVoice; -import io.greitan.avion.common.utils.IntegerOperation; -import io.greitan.avion.common.utils.StringOperation; -import io.greitan.avion.common.utils.DoubleStringOperation; -import io.greitan.avion.common.utils.EmptyOperation; +import team.avion.common.BaseGeyserVoice; +import team.avion.common.utils.IntegerOperation; +import team.avion.common.utils.StringOperation; +import team.avion.common.utils.DoubleStringOperation; public class BaseVoiceCommand { private final BaseGeyserVoice plugin; @@ -19,7 +18,7 @@ public BaseVoiceCommand(T plugin) { this.plugin = plugin; } - public boolean onCommand(String[] args, boolean isConnected, boolean isPlayer, StringOperation hasPermission, DoubleStringOperation sendMessage, IntegerOperation bindUser, EmptyOperation clearAutoBind) { + public boolean onCommand(String[] args, boolean isConnected, boolean isPlayer, StringOperation hasPermission, DoubleStringOperation sendMessage, IntegerOperation bindUser) { if (args.length >= 1) { // Connect command - connect to the server. if (args[0].equalsIgnoreCase("connect") && hasPermission.execute("voice.connect")) { @@ -79,15 +78,6 @@ else if (args[0].equalsIgnoreCase("disconnect") && hasPermission.execute("voice. plugin.disconnect("Disconnection Request."); } - // Settings command - allows to change settings? - else if (args[0].equalsIgnoreCase("settings") && hasPermission.execute("voice.settings") && isConnected) { - int proximityDistance = 1; - Boolean proximityToggle = true; - Boolean voiceEffects = true; - - // Show a menu? - plugin.updateSettings(proximityDistance, proximityToggle, voiceEffects); - } // Bind command - bind player. else if (args[0].equalsIgnoreCase("bind") && hasPermission.execute("voice.bind") && isConnected) { if (isPlayer) { @@ -139,44 +129,6 @@ else if (args[0].equalsIgnoreCase("bindfake") && hasPermission.execute("voice.bi sendMessage.execute("commands.bind.fake-invalid-args", "red"); } } - // Update fake command - update fake player. - else if (args[0].equalsIgnoreCase("updatefake") && hasPermission.execute("voice.bindfake") && isConnected) { - if (isPlayer) { - if (args.length >= 2 && Objects.nonNull(args[1])) { - int bindKey; - try { - bindKey = Integer.parseInt(args[1]); - } catch (NumberFormatException e) { - sendMessage.execute("commands.updatefake.invalid-args", "red"); - return true; - } - - // Not doing this now, since this isn't needed nor simple to implement - sendMessage.execute("commands.updatefake.updating", "yellow"); - Boolean updated = bindKey == -1; // updateFake.execute(bindKey); - if (updated) { - sendMessage.execute("commands.updatefake.updated", "green"); - } else { - sendMessage.execute("commands.updatefake.failed", "red"); - } - } else { - sendMessage.execute("commands.updatefake.invalid-args", "red"); - } - } else { - sendMessage.execute("cmd-not-player", "red"); - } - } - // Clear auto bind command - clear auto bind player. - else if (args[0].equalsIgnoreCase("clearautobind") && hasPermission.execute("voice.bind") && isConnected) { - if (isPlayer) { - clearAutoBind.execute(); - sendMessage.execute("commands.clearautobind", "green"); - } - // Player only command. - else { - sendMessage.execute("cmd-not-player", "red"); - } - } // Reload command - reload the configs. else if (args[0].equalsIgnoreCase("reload") && hasPermission.execute("voice.reload")) { plugin.reload(); @@ -198,8 +150,11 @@ public List onTabComplete(String[] args, StringOperation hasPermission) // Main command arguments. if (args.length == 1 || args.length == 0) { - List options = List.of("connect", "reconnect", "disconnect", "settings", "bind", "bindfake", "updatefake", "clearautobind", "reload"); - completions = options.stream().filter(val -> (args.length == 0 || val.startsWith(args[0])) && hasPermission.execute(val == "clearautobind" ? "voice.bind" : (val == "updatefake" ? "voice.bindfake" : "voice." + val))).collect(Collectors.toList()); + List options = List.of("connect", "reconnect", "disconnect", "bind", "bindfake", "reload"); + completions = options.stream() + .filter(val -> (args.length == 0 || val.startsWith(args[0])) + && hasPermission.execute("voice." + val)) + .collect(Collectors.toList()); } // Setup command arguments. diff --git a/modules/core/src/main/java/team/avion/common/config/ConfigTemplateWriter.java b/modules/core/src/main/java/team/avion/common/config/ConfigTemplateWriter.java new file mode 100644 index 0000000..b277401 --- /dev/null +++ b/modules/core/src/main/java/team/avion/common/config/ConfigTemplateWriter.java @@ -0,0 +1,29 @@ +package team.avion.common.config; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Map; + +public final class ConfigTemplateWriter { + private ConfigTemplateWriter() { + } + + public static void write(InputStream input, Path target, Map replacements) throws IOException { + String content = new String(input.readAllBytes(), StandardCharsets.UTF_8); + for (Map.Entry entry : replacements.entrySet()) { + content = content.replace(entry.getKey(), entry.getValue()); + } + + Path parent = target.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + + Files.writeString(target, content, StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + } +} diff --git a/modules/core/src/main/java/team/avion/common/localization/PluginLocalization.java b/modules/core/src/main/java/team/avion/common/localization/PluginLocalization.java new file mode 100644 index 0000000..9307853 --- /dev/null +++ b/modules/core/src/main/java/team/avion/common/localization/PluginLocalization.java @@ -0,0 +1,45 @@ +package team.avion.common.localization; + +import java.util.Locale; +import java.util.Set; + +public final class PluginLocalization { + public static final String DEFAULT_LANGUAGE = "en"; + public static final String SYSTEM_LANGUAGE = "system"; + private static final Set SUPPORTED_LANGUAGES = Set.of("en", "ru", "nl", "ja"); + + private PluginLocalization() { + } + + public static String resolveConfiguredLanguage(String configuredLanguage) { + if (configuredLanguage == null || configuredLanguage.isBlank() + || SYSTEM_LANGUAGE.equalsIgnoreCase(configuredLanguage)) { + return resolveSystemLanguage(); + } + + String normalized = configuredLanguage.trim().toLowerCase(Locale.ROOT); + return SUPPORTED_LANGUAGES.contains(normalized) ? normalized : DEFAULT_LANGUAGE; + } + + public static String resolveSystemLanguage() { + String language = Locale.getDefault().getLanguage(); + if (language == null || language.isBlank()) { + return DEFAULT_LANGUAGE; + } + + String normalized = language.toLowerCase(Locale.ROOT); + return SUPPORTED_LANGUAGES.contains(normalized) ? normalized : DEFAULT_LANGUAGE; + } + + public static String normalizeConfigLanguage(String configuredLanguage) { + if (configuredLanguage == null || configuredLanguage.isBlank()) { + return SYSTEM_LANGUAGE; + } + + String normalized = configuredLanguage.trim().toLowerCase(Locale.ROOT); + if (SYSTEM_LANGUAGE.equals(normalized)) { + return SYSTEM_LANGUAGE; + } + return SUPPORTED_LANGUAGES.contains(normalized) ? normalized : DEFAULT_LANGUAGE; + } +} diff --git a/src/main/java/io/greitan/avion/common/utils/BaseLogger.java b/modules/core/src/main/java/team/avion/common/utils/BaseLogger.java similarity index 88% rename from src/main/java/io/greitan/avion/common/utils/BaseLogger.java rename to modules/core/src/main/java/team/avion/common/utils/BaseLogger.java index 511d4b0..8a5d677 100644 --- a/src/main/java/io/greitan/avion/common/utils/BaseLogger.java +++ b/modules/core/src/main/java/team/avion/common/utils/BaseLogger.java @@ -1,4 +1,4 @@ -package io.greitan.avion.common.utils; +package team.avion.common.utils; import net.kyori.adventure.text.Component; diff --git a/src/main/java/io/greitan/avion/common/utils/BasePlaceholder.java b/modules/core/src/main/java/team/avion/common/utils/BasePlaceholder.java similarity index 90% rename from src/main/java/io/greitan/avion/common/utils/BasePlaceholder.java rename to modules/core/src/main/java/team/avion/common/utils/BasePlaceholder.java index 1f1029d..e28febc 100644 --- a/src/main/java/io/greitan/avion/common/utils/BasePlaceholder.java +++ b/modules/core/src/main/java/team/avion/common/utils/BasePlaceholder.java @@ -1,4 +1,4 @@ -package io.greitan.avion.common.utils; +package team.avion.common.utils; import me.clip.placeholderapi.expansion.PlaceholderExpansion; diff --git a/modules/core/src/main/java/team/avion/common/utils/Constants.java b/modules/core/src/main/java/team/avion/common/utils/Constants.java new file mode 100644 index 0000000..dddae89 --- /dev/null +++ b/modules/core/src/main/java/team/avion/common/utils/Constants.java @@ -0,0 +1,11 @@ +package team.avion.common.utils; + +public final class Constants { + public static final String NAME = "GeyserVoice"; + public static final String VERSION = "DEV"; + public static final String DESCRIPTION = "Plugin that adds support for using VoiceCraft on Java servers."; + public static final String URL = "https://geyservoice.avion.team"; + + private Constants() { + } +} diff --git a/src/main/java/io/greitan/avion/common/utils/DoubleStringOperation.java b/modules/core/src/main/java/team/avion/common/utils/DoubleStringOperation.java similarity index 70% rename from src/main/java/io/greitan/avion/common/utils/DoubleStringOperation.java rename to modules/core/src/main/java/team/avion/common/utils/DoubleStringOperation.java index 134e811..7c03460 100644 --- a/src/main/java/io/greitan/avion/common/utils/DoubleStringOperation.java +++ b/modules/core/src/main/java/team/avion/common/utils/DoubleStringOperation.java @@ -1,4 +1,4 @@ -package io.greitan.avion.common.utils; +package team.avion.common.utils; public interface DoubleStringOperation { void execute(String key, String rawColor); diff --git a/src/main/java/io/greitan/avion/common/utils/IntegerOperation.java b/modules/core/src/main/java/team/avion/common/utils/IntegerOperation.java similarity index 63% rename from src/main/java/io/greitan/avion/common/utils/IntegerOperation.java rename to modules/core/src/main/java/team/avion/common/utils/IntegerOperation.java index 3d3211c..94cbf39 100644 --- a/src/main/java/io/greitan/avion/common/utils/IntegerOperation.java +++ b/modules/core/src/main/java/team/avion/common/utils/IntegerOperation.java @@ -1,4 +1,4 @@ -package io.greitan.avion.common.utils; +package team.avion.common.utils; public interface IntegerOperation { boolean execute(int key); diff --git a/src/main/java/io/greitan/avion/common/utils/StringOperation.java b/modules/core/src/main/java/team/avion/common/utils/StringOperation.java similarity index 64% rename from src/main/java/io/greitan/avion/common/utils/StringOperation.java rename to modules/core/src/main/java/team/avion/common/utils/StringOperation.java index 4ba5086..08402ef 100644 --- a/src/main/java/io/greitan/avion/common/utils/StringOperation.java +++ b/modules/core/src/main/java/team/avion/common/utils/StringOperation.java @@ -1,4 +1,4 @@ -package io.greitan.avion.common.utils; +package team.avion.common.utils; public interface StringOperation { boolean execute(String name); diff --git a/modules/core/src/main/java/team/avion/protocol/McApiTcpClient.java b/modules/core/src/main/java/team/avion/protocol/McApiTcpClient.java new file mode 100644 index 0000000..9230d2f --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/McApiTcpClient.java @@ -0,0 +1,218 @@ +package team.avion.protocol; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import team.avion.common.utils.BaseLogger; +import team.avion.protocol.McPackets.AcceptResponsePacket; +import team.avion.protocol.McPackets.DenyResponsePacket; +import team.avion.protocol.McPackets.LoginRequestPacket; +import team.avion.protocol.McPackets.LogoutRequestPacket; +import team.avion.protocol.McPackets.McApiPacket; + +public final class McApiTcpClient { + private final BaseLogger logger; + private final Duration connectTimeout; + private final Duration readTimeout; + + private Socket socket; + private DataInputStream input; + private DataOutputStream output; + private String sessionToken = ""; + private String lastDenyReason = ""; + + public McApiTcpClient(BaseLogger logger) { + this(logger, Duration.ofSeconds(2), Duration.ofSeconds(1)); + } + + public McApiTcpClient(BaseLogger logger, Duration connectTimeout, Duration readTimeout) { + this.logger = logger; + this.connectTimeout = connectTimeout; + this.readTimeout = readTimeout; + } + + public synchronized boolean connect(String host, int port, String loginToken) { + close(); + + try { + Socket newSocket = new Socket(); + newSocket.connect(new InetSocketAddress(host, port), Math.toIntExact(connectTimeout.toMillis())); + newSocket.setSoTimeout(Math.toIntExact(readTimeout.toMillis())); + + socket = newSocket; + input = new DataInputStream(new BufferedInputStream(newSocket.getInputStream())); + output = new DataOutputStream(new BufferedOutputStream(newSocket.getOutputStream())); + sessionToken = ""; + lastDenyReason = ""; + + LoginRequestPacket loginPacket = new LoginRequestPacket(); + loginPacket.RequestId = nextRequestId(); + loginPacket.Token = loginToken; + loginPacket.Version = VoiceCraftProtocol.VERSION; + + List responsePackets = exchangeInternal("", List.of(loginPacket)); + for (McApiPacket packet : responsePackets) { + if (packet instanceof AcceptResponsePacket accept) { + sessionToken = accept.Token; + logger.info("VoiceCraft McApi session established."); + return true; + } + if (packet instanceof DenyResponsePacket deny) { + lastDenyReason = deny.Reason; + logger.error("VoiceCraft login denied: " + deny.Reason); + close(); + return false; + } + } + + logger.error("VoiceCraft login failed: no accept response received."); + close(); + return false; + } catch (Exception ex) { + logger.error("VoiceCraft TCP connect failed: " + ex.getMessage()); + close(); + return false; + } + } + + public synchronized List exchange(List packets) { + if (!isConnected()) { + return List.of(); + } + + try { + return exchangeInternal(sessionToken, packets); + } catch (SocketTimeoutException ex) { + logger.error("VoiceCraft TCP request timed out: " + ex.getMessage()); + close(); + return List.of(); + } catch (Exception ex) { + logger.error("VoiceCraft TCP request failed: " + ex.getMessage()); + close(); + return List.of(); + } + } + + public synchronized void logout() { + if (!isConnected()) { + close(); + return; + } + + try { + LogoutRequestPacket logoutPacket = new LogoutRequestPacket(); + logoutPacket.Token = sessionToken; + exchangeInternal(sessionToken, List.of(logoutPacket)); + } catch (Exception ignored) { + } finally { + close(); + } + } + + public synchronized boolean isConnected() { + return socket != null && socket.isConnected() && !socket.isClosed() && !sessionToken.isBlank(); + } + + public synchronized String getSessionToken() { + return sessionToken; + } + + public synchronized String getLastDenyReason() { + return lastDenyReason; + } + + public synchronized void close() { + sessionToken = ""; + lastDenyReason = ""; + + if (input != null) { + try { + input.close(); + } catch (IOException ignored) { + } + } + if (output != null) { + try { + output.close(); + } catch (IOException ignored) { + } + } + if (socket != null) { + try { + socket.close(); + } catch (IOException ignored) { + } + } + + input = null; + output = null; + socket = null; + } + + public static String nextRequestId() { + return UUID.randomUUID().toString(); + } + + private List exchangeInternal(String token, List packets) throws IOException { + if (output == null || input == null) { + throw new IOException("Client socket is not open."); + } + + List rawPackets = new ArrayList<>(packets.size()); + for (McApiPacket packet : packets) { + rawPackets.add(McProtocolCodec.encode(packet)); + } + + byte[] frame = McTcpFrameCodec.encodeRequest(token, rawPackets); + output.write(frame); + output.flush(); + + byte[] header = input.readNBytes(McTcpFrameCodec.FRAME_HEADER_SIZE); + if (header.length != McTcpFrameCodec.FRAME_HEADER_SIZE) { + throw new IOException("Unexpected EOF while reading MCTP header."); + } + + ByteBuffer headerBuffer = ByteBuffer.wrap(header).order(ByteOrder.BIG_ENDIAN); + int magic = headerBuffer.getInt(); + short version = headerBuffer.getShort(); + short kind = headerBuffer.getShort(); + int payloadLength = headerBuffer.getInt(); + + if (magic != McTcpFrameCodec.FRAME_MAGIC) { + throw new IOException("Unexpected MCTP magic: " + magic); + } + if (version != McTcpFrameCodec.FRAME_VERSION) { + throw new IOException("Unsupported MCTP version: " + version); + } + if (kind != McTcpFrameCodec.RESPONSE_KIND) { + throw new IOException("Unexpected MCTP frame kind: " + kind); + } + if (payloadLength < 0 || payloadLength > McTcpFrameCodec.MAX_FRAME_PAYLOAD_LENGTH) { + throw new IOException("Invalid MCTP payload length: " + payloadLength); + } + + byte[] payload = input.readNBytes(payloadLength); + if (payload.length != payloadLength) { + throw new IOException("Unexpected EOF while reading MCTP payload."); + } + + McTcpFrameCodec.DecodedPayload decodedPayload = McTcpFrameCodec.decodePayload(payload); + List decodedPackets = new ArrayList<>(decodedPayload.packets().size()); + for (byte[] packetBytes : decodedPayload.packets()) { + decodedPackets.add(McProtocolCodec.decode(packetBytes)); + } + return decodedPackets; + } +} diff --git a/modules/core/src/main/java/team/avion/protocol/McPackets.java b/modules/core/src/main/java/team/avion/protocol/McPackets.java new file mode 100644 index 0000000..86e89cc --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/McPackets.java @@ -0,0 +1,1097 @@ +package team.avion.protocol; + +import team.avion.protocol.McProtocolEnums.CreateEntityResponseCode; +import team.avion.protocol.McProtocolEnums.DestroyEntityResponseCode; +import team.avion.protocol.McProtocolEnums.EffectType; +import team.avion.protocol.McProtocolEnums.McApiPacketType; +import team.avion.protocol.McProtocolEnums.PositioningType; +import team.avion.protocol.McProtocolEnums.ResetResponseCode; +import team.avion.protocol.McProtocolTypes.Guid; +import team.avion.protocol.McProtocolTypes.Vector2; +import team.avion.protocol.McProtocolTypes.Vector3; +import team.avion.protocol.McProtocolTypes.Version; + +public final class McPackets { + private McPackets() { + } + + public interface NetSerializable { + void serialize(NetDataWriter writer); + + void deserialize(NetDataReader reader); + } + + public interface McApiPacket extends NetSerializable { + McApiPacketType packetType(); + } + + public interface RequestIdPacket { + String requestId(); + } + + public interface AudioEffect extends NetSerializable { + EffectType effectType(); + } + + public abstract static class EmptyPacket implements McApiPacket { + @Override + public void serialize(NetDataWriter writer) { + } + + @Override + public void deserialize(NetDataReader reader) { + } + } + + public abstract static class IntValuePacket implements McApiPacket { + public int Id; + public int Value; + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + writer.putUshort(Value); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + Value = reader.getUshort(); + } + } + + public abstract static class BoolValuePacket implements McApiPacket { + public int Id; + public boolean Value; + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + writer.putBool(Value); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + Value = reader.getBool(); + } + } + + public abstract static class FloatValuePacket implements McApiPacket { + public int Id; + public float Value; + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + writer.putFloat(Value); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + Value = reader.getFloat(); + } + } + + public abstract static class StringValuePacket implements McApiPacket { + public int Id; + public String Value = ""; + private final int maxLength; + + protected StringValuePacket(int maxLength) { + this.maxLength = maxLength; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + writer.putString(Value, maxLength); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + Value = reader.getString(maxLength); + } + } + + public abstract static class Vector2ValuePacket implements McApiPacket { + public int Id; + public Vector2 Value = new Vector2(0, 0); + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + writer.putFloat(Value.x()); + writer.putFloat(Value.y()); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + Value = new Vector2(reader.getFloat(), reader.getFloat()); + } + } + + public abstract static class Vector3ValuePacket implements McApiPacket { + public int Id; + public Vector3 Value = new Vector3(0, 0, 0); + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + writer.putFloat(Value.x()); + writer.putFloat(Value.y()); + writer.putFloat(Value.z()); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + Value = new Vector3(reader.getFloat(), reader.getFloat(), reader.getFloat()); + } + } + + public abstract static class EntityCreatedPacket implements McApiPacket { + public int Id; + public float Loudness; + public long LastSpoke; + public String WorldId = ""; + public String Name = ""; + public boolean Muted; + public boolean Deafened; + public int TalkBitmask; + public int ListenBitmask; + public int EffectBitmask; + public Vector3 Position = new Vector3(0, 0, 0); + public Vector2 Rotation = new Vector2(0, 0); + public float CaveFactor; + public float MuffleFactor; + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + writer.putFloat(Loudness); + writer.putLong(LastSpoke); + writer.putString(WorldId, McProtocolConstants.MAX_STRING_LENGTH); + writer.putString(Name, McProtocolConstants.MAX_STRING_LENGTH); + writer.putBool(Muted); + writer.putBool(Deafened); + writer.putUshort(TalkBitmask); + writer.putUshort(ListenBitmask); + writer.putUshort(EffectBitmask); + writer.putFloat(Position.x()); + writer.putFloat(Position.y()); + writer.putFloat(Position.z()); + writer.putFloat(Rotation.x()); + writer.putFloat(Rotation.y()); + writer.putFloat(CaveFactor); + writer.putFloat(MuffleFactor); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + Loudness = reader.getFloat(); + LastSpoke = reader.getLong(); + WorldId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + Name = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + Muted = reader.getBool(); + Deafened = reader.getBool(); + TalkBitmask = reader.getUshort(); + ListenBitmask = reader.getUshort(); + EffectBitmask = reader.getUshort(); + Position = new Vector3(reader.getFloat(), reader.getFloat(), reader.getFloat()); + Rotation = new Vector2(reader.getFloat(), reader.getFloat()); + CaveFactor = reader.getFloat(); + MuffleFactor = reader.getFloat(); + } + } + + public static final class VisibilityEffect implements AudioEffect { + @Override + public EffectType effectType() { + return EffectType.VISIBILITY; + } + + @Override + public void serialize(NetDataWriter writer) { + } + + @Override + public void deserialize(NetDataReader reader) { + } + } + + public static final class ProximityEffect implements AudioEffect { + public float MinRange; + public float MaxRange; + public float WetDry = 1.0f; + + @Override + public EffectType effectType() { + return EffectType.PROXIMITY; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putFloat(MinRange); + writer.putFloat(MaxRange); + writer.putFloat(WetDry); + } + + @Override + public void deserialize(NetDataReader reader) { + MinRange = reader.getFloat(); + MaxRange = reader.getFloat(); + WetDry = reader.getFloat(); + } + } + + public static final class DirectionalEffect implements AudioEffect { + public float WetDry = 1.0f; + + @Override + public EffectType effectType() { + return EffectType.DIRECTIONAL; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putFloat(WetDry); + } + + @Override + public void deserialize(NetDataReader reader) { + WetDry = reader.getFloat(); + } + } + + public static final class ProximityEchoEffect implements AudioEffect { + public float Delay = 0.5f; + public float Range; + public float WetDry = 1.0f; + + @Override + public EffectType effectType() { + return EffectType.PROXIMITY_ECHO; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putFloat(Delay); + writer.putFloat(Range); + writer.putFloat(WetDry); + } + + @Override + public void deserialize(NetDataReader reader) { + Delay = reader.getFloat(); + Range = reader.getFloat(); + WetDry = reader.getFloat(); + } + } + + public static final class EchoEffect implements AudioEffect { + public float Delay = 0.5f; + public float Feedback = 0.5f; + public float WetDry = 1.0f; + + @Override + public EffectType effectType() { + return EffectType.ECHO; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putFloat(Delay); + writer.putFloat(Feedback); + writer.putFloat(WetDry); + } + + @Override + public void deserialize(NetDataReader reader) { + Delay = reader.getFloat(); + Feedback = reader.getFloat(); + WetDry = reader.getFloat(); + } + } + + public static final class ProximityMuffleEffect implements AudioEffect { + public float WetDry = 1.0f; + + @Override + public EffectType effectType() { + return EffectType.PROXIMITY_MUFFLE; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putFloat(WetDry); + } + + @Override + public void deserialize(NetDataReader reader) { + WetDry = reader.getFloat(); + } + } + + public static final class MuffleEffect implements AudioEffect { + public float WetDry = 1.0f; + + @Override + public EffectType effectType() { + return EffectType.MUFFLE; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putFloat(WetDry); + } + + @Override + public void deserialize(NetDataReader reader) { + WetDry = reader.getFloat(); + } + } + + public static final class LoginRequestPacket implements McApiPacket, RequestIdPacket { + public String RequestId = ""; + public String Token = ""; + public Version Version = VoiceCraftProtocol.VERSION; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.LOGIN_REQUEST; + } + + @Override + public String requestId() { + return RequestId; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(RequestId, McProtocolConstants.MAX_STRING_LENGTH); + writer.putString(Token, McProtocolConstants.MAX_STRING_LENGTH); + writer.putInt(Version.major()); + writer.putInt(Version.minor()); + writer.putInt(Version.build()); + } + + @Override + public void deserialize(NetDataReader reader) { + RequestId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + Token = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + Version = new Version(reader.getInt(), reader.getInt(), reader.getInt()); + } + } + + public static final class LogoutRequestPacket implements McApiPacket { + public String Token = ""; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.LOGOUT_REQUEST; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(Token, McProtocolConstants.MAX_STRING_LENGTH); + } + + @Override + public void deserialize(NetDataReader reader) { + Token = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + } + } + + public static final class PingRequestPacket extends EmptyPacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.PING_REQUEST; + } + } + + public static final class AcceptResponsePacket implements McApiPacket, RequestIdPacket { + public String RequestId = ""; + public String Token = ""; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.ACCEPT_RESPONSE; + } + + @Override + public String requestId() { + return RequestId; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(RequestId, McProtocolConstants.MAX_STRING_LENGTH); + writer.putString(Token, McProtocolConstants.MAX_STRING_LENGTH); + } + + @Override + public void deserialize(NetDataReader reader) { + RequestId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + Token = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + } + } + + public static final class DenyResponsePacket implements McApiPacket, RequestIdPacket { + public String RequestId = ""; + public String Reason = ""; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.DENY_RESPONSE; + } + + @Override + public String requestId() { + return RequestId; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(RequestId, McProtocolConstants.MAX_STRING_LENGTH); + writer.putString(Reason, McProtocolConstants.MAX_STRING_LENGTH); + } + + @Override + public void deserialize(NetDataReader reader) { + RequestId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + Reason = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + } + } + + public static final class PingResponsePacket implements McApiPacket { + public String Token = ""; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.PING_RESPONSE; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(Token, McProtocolConstants.MAX_STRING_LENGTH); + } + + @Override + public void deserialize(NetDataReader reader) { + Token = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + } + } + + public static final class ResetRequestPacket implements McApiPacket, RequestIdPacket { + public String RequestId = ""; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.RESET_REQUEST; + } + + @Override + public String requestId() { + return RequestId; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(RequestId, McProtocolConstants.MAX_STRING_LENGTH); + } + + @Override + public void deserialize(NetDataReader reader) { + RequestId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + } + } + + public static final class SetEffectRequestPacket implements McApiPacket { + public int Bitmask; + public EffectType EffectTypeValue = EffectType.NONE; + public AudioEffect Effect; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_EFFECT_REQUEST; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putUshort(Bitmask); + writer.putByte((Effect != null ? Effect.effectType() : EffectTypeValue).ordinal()); + if (Effect != null) { + Effect.serialize(writer); + } + } + + @Override + public void deserialize(NetDataReader reader) { + Bitmask = reader.getUshort(); + EffectTypeValue = McProtocolEnums.EffectType.fromByte(reader.getByte()); + Effect = McProtocolCodec.readEffect(EffectTypeValue, reader); + } + } + + public static final class ClearEffectsRequestPacket extends EmptyPacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.CLEAR_EFFECTS_REQUEST; + } + } + + public static final class CreateEntityRequestPacket implements McApiPacket, RequestIdPacket { + public String RequestId = ""; + public String WorldId = ""; + public String Name = ""; + public boolean Muted; + public boolean Deafened; + public int TalkBitmask; + public int ListenBitmask; + public int EffectBitmask; + public Vector3 Position = new Vector3(0, 0, 0); + public Vector2 Rotation = new Vector2(0, 0); + public float CaveFactor; + public float MuffleFactor; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.CREATE_ENTITY_REQUEST; + } + + @Override + public String requestId() { + return RequestId; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(RequestId, McProtocolConstants.MAX_STRING_LENGTH); + writer.putString(WorldId, McProtocolConstants.MAX_STRING_LENGTH); + writer.putString(Name, McProtocolConstants.MAX_STRING_LENGTH); + writer.putBool(Muted); + writer.putBool(Deafened); + writer.putUshort(TalkBitmask); + writer.putUshort(ListenBitmask); + writer.putUshort(EffectBitmask); + writer.putFloat(Position.x()); + writer.putFloat(Position.y()); + writer.putFloat(Position.z()); + writer.putFloat(Rotation.x()); + writer.putFloat(Rotation.y()); + writer.putFloat(CaveFactor); + writer.putFloat(MuffleFactor); + } + + @Override + public void deserialize(NetDataReader reader) { + RequestId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + WorldId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + Name = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + Muted = reader.getBool(); + Deafened = reader.getBool(); + TalkBitmask = reader.getUshort(); + ListenBitmask = reader.getUshort(); + EffectBitmask = reader.getUshort(); + Position = new Vector3(reader.getFloat(), reader.getFloat(), reader.getFloat()); + Rotation = new Vector2(reader.getFloat(), reader.getFloat()); + CaveFactor = reader.getFloat(); + MuffleFactor = reader.getFloat(); + } + } + + public static final class DestroyEntityRequestPacket implements McApiPacket, RequestIdPacket { + public String RequestId = ""; + public int Id; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.DESTROY_ENTITY_REQUEST; + } + + @Override + public String requestId() { + return RequestId; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(RequestId, McProtocolConstants.MAX_STRING_LENGTH); + writer.putInt(Id); + } + + @Override + public void deserialize(NetDataReader reader) { + RequestId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + Id = reader.getInt(); + } + } + + public static final class EntityAudioRequestPacket implements McApiPacket { + public int Id; + public int Timestamp; + public float FrameLoudness; + public int Length; + public byte[] Data = new byte[0]; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.ENTITY_AUDIO_REQUEST; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + writer.putUshort(Timestamp); + writer.putFloat(FrameLoudness); + writer.putBytes(Data, 0, Length); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + Timestamp = reader.getUshort(); + FrameLoudness = reader.getFloat(); + Length = reader.availableBytes(); + if (Length > McProtocolConstants.MAXIMUM_ENCODED_BYTES) { + throw new IllegalStateException("Array length exceeds maximum number of bytes per packet"); + } + Data = new byte[Length]; + reader.getBytes(Data, Length); + } + } + + public static final class SetEntityTitleRequestPacket extends StringValuePacket { + public SetEntityTitleRequestPacket() { + super(McProtocolConstants.MAX_STRING_LENGTH); + } + + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_TITLE_REQUEST; + } + } + + public static final class SetEntityDescriptionRequestPacket extends StringValuePacket { + public SetEntityDescriptionRequestPacket() { + super(McProtocolConstants.MAX_DESCRIPTION_STRING_LENGTH); + } + + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_DESCRIPTION_REQUEST; + } + } + + public static final class SetEntityWorldIdRequestPacket extends StringValuePacket { + public SetEntityWorldIdRequestPacket() { + super(McProtocolConstants.MAX_STRING_LENGTH); + } + + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_WORLD_ID_REQUEST; + } + } + + public static final class SetEntityNameRequestPacket extends StringValuePacket { + public SetEntityNameRequestPacket() { + super(McProtocolConstants.MAX_STRING_LENGTH); + } + + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_NAME_REQUEST; + } + } + + public static final class SetEntityMuteRequestPacket extends BoolValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_MUTE_REQUEST; + } + } + + public static final class SetEntityDeafenRequestPacket extends BoolValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_DEAFEN_REQUEST; + } + } + + public static final class SetEntityTalkBitmaskRequestPacket extends IntValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_TALK_BITMASK_REQUEST; + } + } + + public static final class SetEntityListenBitmaskRequestPacket extends IntValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_LISTEN_BITMASK_REQUEST; + } + } + + public static final class SetEntityEffectBitmaskRequestPacket extends IntValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_EFFECT_BITMASK_REQUEST; + } + } + + public static final class SetEntityPositionRequestPacket extends Vector3ValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_POSITION_REQUEST; + } + } + + public static final class SetEntityRotationRequestPacket extends Vector2ValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_ROTATION_REQUEST; + } + } + + public static final class SetEntityCaveFactorRequestPacket extends FloatValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_CAVE_FACTOR_REQUEST; + } + } + + public static final class SetEntityMuffleFactorRequestPacket extends FloatValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.SET_ENTITY_MUFFLE_FACTOR_REQUEST; + } + } + + public static final class ResetResponsePacket implements McApiPacket, RequestIdPacket { + public String RequestId = ""; + public ResetResponseCode ResponseCode = ResetResponseCode.OK; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.RESET_RESPONSE; + } + + @Override + public String requestId() { + return RequestId; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(RequestId, McProtocolConstants.MAX_STRING_LENGTH); + writer.putSbyte(ResponseCode.code()); + } + + @Override + public void deserialize(NetDataReader reader) { + RequestId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + ResponseCode = ResetResponseCode.fromCode(reader.getSbyte()); + } + } + + public static final class CreateEntityResponsePacket implements McApiPacket, RequestIdPacket { + public String RequestId = ""; + public CreateEntityResponseCode ResponseCode = CreateEntityResponseCode.OK; + public int Id; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.CREATE_ENTITY_RESPONSE; + } + + @Override + public String requestId() { + return RequestId; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(RequestId, McProtocolConstants.MAX_STRING_LENGTH); + writer.putSbyte(ResponseCode.code()); + writer.putInt(Id); + } + + @Override + public void deserialize(NetDataReader reader) { + RequestId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + ResponseCode = CreateEntityResponseCode.fromCode(reader.getSbyte()); + Id = reader.getInt(); + } + } + + public static final class DestroyEntityResponsePacket implements McApiPacket, RequestIdPacket { + public String RequestId = ""; + public DestroyEntityResponseCode ResponseCode = DestroyEntityResponseCode.OK; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.DESTROY_ENTITY_RESPONSE; + } + + @Override + public String requestId() { + return RequestId; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putString(RequestId, McProtocolConstants.MAX_STRING_LENGTH); + writer.putSbyte(ResponseCode.code()); + } + + @Override + public void deserialize(NetDataReader reader) { + RequestId = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + ResponseCode = DestroyEntityResponseCode.fromCode(reader.getSbyte()); + } + } + + public static final class OnEffectUpdatedPacket implements McApiPacket { + public int Bitmask; + public EffectType EffectTypeValue = EffectType.NONE; + public AudioEffect Effect; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_EFFECT_UPDATED; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putUshort(Bitmask); + writer.putByte((Effect != null ? Effect.effectType() : EffectTypeValue).ordinal()); + if (Effect != null) { + Effect.serialize(writer); + } + } + + @Override + public void deserialize(NetDataReader reader) { + Bitmask = reader.getUshort(); + EffectTypeValue = McProtocolEnums.EffectType.fromByte(reader.getByte()); + Effect = McProtocolCodec.readEffect(EffectTypeValue, reader); + } + } + + public static final class OnEntityCreatedPacket extends EntityCreatedPacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_CREATED; + } + } + + public static final class OnNetworkEntityCreatedPacket extends EntityCreatedPacket { + public Guid UserGuid = Guid.empty(); + public Guid ServerUserGuid = Guid.empty(); + public String Locale = ""; + public PositioningType PositioningTypeValue = PositioningType.SERVER; + public boolean ServerMuted; + public boolean ServerDeafened; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_NETWORK_ENTITY_CREATED; + } + + @Override + public void serialize(NetDataWriter writer) { + super.serialize(writer); + writer.putString(UserGuid.toString(), McProtocolConstants.MAX_STRING_LENGTH); + writer.putString(ServerUserGuid.toString(), McProtocolConstants.MAX_STRING_LENGTH); + writer.putString(Locale, McProtocolConstants.MAX_STRING_LENGTH); + writer.putByte(PositioningTypeValue.ordinal()); + writer.putBool(ServerMuted); + writer.putBool(ServerDeafened); + } + + @Override + public void deserialize(NetDataReader reader) { + super.deserialize(reader); + UserGuid = Guid.parse(reader.getString(McProtocolConstants.MAX_STRING_LENGTH)); + ServerUserGuid = Guid.parse(reader.getString(McProtocolConstants.MAX_STRING_LENGTH)); + Locale = reader.getString(McProtocolConstants.MAX_STRING_LENGTH); + PositioningTypeValue = McProtocolEnums.PositioningType.fromByte(reader.getByte()); + ServerMuted = reader.getBool(); + ServerDeafened = reader.getBool(); + } + } + + public static final class OnEntityDestroyedPacket implements McApiPacket { + public int Id; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_DESTROYED; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + } + } + + public static final class OnEntityVisibilityUpdatedPacket implements McApiPacket { + public int Id; + public int Id2; + public boolean Value; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_VISIBILITY_UPDATED; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + writer.putInt(Id2); + writer.putBool(Value); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + Id2 = reader.getInt(); + Value = reader.getBool(); + } + } + + public static final class OnEntityWorldIdUpdatedPacket extends StringValuePacket { + public OnEntityWorldIdUpdatedPacket() { + super(McProtocolConstants.MAX_STRING_LENGTH); + } + + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_WORLD_ID_UPDATED; + } + } + + public static final class OnEntityNameUpdatedPacket extends StringValuePacket { + public OnEntityNameUpdatedPacket() { + super(McProtocolConstants.MAX_STRING_LENGTH); + } + + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_NAME_UPDATED; + } + } + + public static final class OnEntityMuteUpdatedPacket extends BoolValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_MUTE_UPDATED; + } + } + + public static final class OnEntityDeafenUpdatedPacket extends BoolValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_DEAFEN_UPDATED; + } + } + + public static final class OnEntityServerMuteUpdatedPacket extends BoolValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_SERVER_MUTE_UPDATED; + } + } + + public static final class OnEntityServerDeafenUpdatedPacket extends BoolValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_SERVER_DEAFEN_UPDATED; + } + } + + public static final class OnEntityTalkBitmaskUpdatedPacket extends IntValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_TALK_BITMASK_UPDATED; + } + } + + public static final class OnEntityListenBitmaskUpdatedPacket extends IntValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_LISTEN_BITMASK_UPDATED; + } + } + + public static final class OnEntityEffectBitmaskUpdatedPacket extends IntValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_EFFECT_BITMASK_UPDATED; + } + } + + public static final class OnEntityPositionUpdatedPacket extends Vector3ValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_POSITION_UPDATED; + } + } + + public static final class OnEntityRotationUpdatedPacket extends Vector2ValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_ROTATION_UPDATED; + } + } + + public static final class OnEntityCaveFactorUpdatedPacket extends FloatValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_CAVE_FACTOR_UPDATED; + } + } + + public static final class OnEntityMuffleFactorUpdatedPacket extends FloatValuePacket { + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_MUFFLE_FACTOR_UPDATED; + } + } + + public static final class OnEntityAudioReceivedPacket implements McApiPacket { + public int Id; + public int Timestamp; + public float FrameLoudness; + + @Override + public McApiPacketType packetType() { + return McApiPacketType.ON_ENTITY_AUDIO_RECEIVED; + } + + @Override + public void serialize(NetDataWriter writer) { + writer.putInt(Id); + writer.putUshort(Timestamp); + writer.putFloat(FrameLoudness); + } + + @Override + public void deserialize(NetDataReader reader) { + Id = reader.getInt(); + Timestamp = reader.getUshort(); + FrameLoudness = reader.getFloat(); + } + } +} diff --git a/modules/core/src/main/java/team/avion/protocol/McProtocolCodec.java b/modules/core/src/main/java/team/avion/protocol/McProtocolCodec.java new file mode 100644 index 0000000..d92fafc --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/McProtocolCodec.java @@ -0,0 +1,164 @@ +package team.avion.protocol; + +import java.util.EnumMap; +import java.util.Map; +import java.util.function.Supplier; + +import team.avion.protocol.McPackets.AcceptResponsePacket; +import team.avion.protocol.McPackets.AudioEffect; +import team.avion.protocol.McPackets.ClearEffectsRequestPacket; +import team.avion.protocol.McPackets.CreateEntityRequestPacket; +import team.avion.protocol.McPackets.CreateEntityResponsePacket; +import team.avion.protocol.McPackets.DenyResponsePacket; +import team.avion.protocol.McPackets.DestroyEntityRequestPacket; +import team.avion.protocol.McPackets.DestroyEntityResponsePacket; +import team.avion.protocol.McPackets.DirectionalEffect; +import team.avion.protocol.McPackets.EchoEffect; +import team.avion.protocol.McPackets.EntityAudioRequestPacket; +import team.avion.protocol.McPackets.LoginRequestPacket; +import team.avion.protocol.McPackets.LogoutRequestPacket; +import team.avion.protocol.McPackets.McApiPacket; +import team.avion.protocol.McPackets.MuffleEffect; +import team.avion.protocol.McPackets.OnEffectUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityAudioReceivedPacket; +import team.avion.protocol.McPackets.OnEntityCaveFactorUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityCreatedPacket; +import team.avion.protocol.McPackets.OnEntityDeafenUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityDestroyedPacket; +import team.avion.protocol.McPackets.OnEntityEffectBitmaskUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityListenBitmaskUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityMuffleFactorUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityMuteUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityNameUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityPositionUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityRotationUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityServerDeafenUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityServerMuteUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityTalkBitmaskUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityVisibilityUpdatedPacket; +import team.avion.protocol.McPackets.OnEntityWorldIdUpdatedPacket; +import team.avion.protocol.McPackets.OnNetworkEntityCreatedPacket; +import team.avion.protocol.McPackets.PingRequestPacket; +import team.avion.protocol.McPackets.PingResponsePacket; +import team.avion.protocol.McPackets.ProximityEchoEffect; +import team.avion.protocol.McPackets.ProximityEffect; +import team.avion.protocol.McPackets.ProximityMuffleEffect; +import team.avion.protocol.McPackets.ResetRequestPacket; +import team.avion.protocol.McPackets.ResetResponsePacket; +import team.avion.protocol.McPackets.SetEffectRequestPacket; +import team.avion.protocol.McPackets.SetEntityCaveFactorRequestPacket; +import team.avion.protocol.McPackets.SetEntityDeafenRequestPacket; +import team.avion.protocol.McPackets.SetEntityDescriptionRequestPacket; +import team.avion.protocol.McPackets.SetEntityEffectBitmaskRequestPacket; +import team.avion.protocol.McPackets.SetEntityListenBitmaskRequestPacket; +import team.avion.protocol.McPackets.SetEntityMuffleFactorRequestPacket; +import team.avion.protocol.McPackets.SetEntityMuteRequestPacket; +import team.avion.protocol.McPackets.SetEntityNameRequestPacket; +import team.avion.protocol.McPackets.SetEntityPositionRequestPacket; +import team.avion.protocol.McPackets.SetEntityRotationRequestPacket; +import team.avion.protocol.McPackets.SetEntityTalkBitmaskRequestPacket; +import team.avion.protocol.McPackets.SetEntityTitleRequestPacket; +import team.avion.protocol.McPackets.SetEntityWorldIdRequestPacket; +import team.avion.protocol.McPackets.VisibilityEffect; +import team.avion.protocol.McProtocolEnums.EffectType; +import team.avion.protocol.McProtocolEnums.McApiPacketType; + +public final class McProtocolCodec { + private static final Map> PACKET_FACTORIES = new EnumMap<>(McApiPacketType.class); + + static { + register(McApiPacketType.LOGIN_REQUEST, LoginRequestPacket::new); + register(McApiPacketType.LOGOUT_REQUEST, LogoutRequestPacket::new); + register(McApiPacketType.PING_REQUEST, PingRequestPacket::new); + register(McApiPacketType.ACCEPT_RESPONSE, AcceptResponsePacket::new); + register(McApiPacketType.DENY_RESPONSE, DenyResponsePacket::new); + register(McApiPacketType.PING_RESPONSE, PingResponsePacket::new); + register(McApiPacketType.RESET_REQUEST, ResetRequestPacket::new); + register(McApiPacketType.SET_EFFECT_REQUEST, SetEffectRequestPacket::new); + register(McApiPacketType.CLEAR_EFFECTS_REQUEST, ClearEffectsRequestPacket::new); + register(McApiPacketType.CREATE_ENTITY_REQUEST, CreateEntityRequestPacket::new); + register(McApiPacketType.DESTROY_ENTITY_REQUEST, DestroyEntityRequestPacket::new); + register(McApiPacketType.ENTITY_AUDIO_REQUEST, EntityAudioRequestPacket::new); + register(McApiPacketType.SET_ENTITY_TITLE_REQUEST, SetEntityTitleRequestPacket::new); + register(McApiPacketType.SET_ENTITY_DESCRIPTION_REQUEST, SetEntityDescriptionRequestPacket::new); + register(McApiPacketType.SET_ENTITY_WORLD_ID_REQUEST, SetEntityWorldIdRequestPacket::new); + register(McApiPacketType.SET_ENTITY_NAME_REQUEST, SetEntityNameRequestPacket::new); + register(McApiPacketType.SET_ENTITY_MUTE_REQUEST, SetEntityMuteRequestPacket::new); + register(McApiPacketType.SET_ENTITY_DEAFEN_REQUEST, SetEntityDeafenRequestPacket::new); + register(McApiPacketType.SET_ENTITY_TALK_BITMASK_REQUEST, SetEntityTalkBitmaskRequestPacket::new); + register(McApiPacketType.SET_ENTITY_LISTEN_BITMASK_REQUEST, SetEntityListenBitmaskRequestPacket::new); + register(McApiPacketType.SET_ENTITY_EFFECT_BITMASK_REQUEST, SetEntityEffectBitmaskRequestPacket::new); + register(McApiPacketType.SET_ENTITY_POSITION_REQUEST, SetEntityPositionRequestPacket::new); + register(McApiPacketType.SET_ENTITY_ROTATION_REQUEST, SetEntityRotationRequestPacket::new); + register(McApiPacketType.SET_ENTITY_CAVE_FACTOR_REQUEST, SetEntityCaveFactorRequestPacket::new); + register(McApiPacketType.SET_ENTITY_MUFFLE_FACTOR_REQUEST, SetEntityMuffleFactorRequestPacket::new); + register(McApiPacketType.RESET_RESPONSE, ResetResponsePacket::new); + register(McApiPacketType.CREATE_ENTITY_RESPONSE, CreateEntityResponsePacket::new); + register(McApiPacketType.DESTROY_ENTITY_RESPONSE, DestroyEntityResponsePacket::new); + register(McApiPacketType.ON_EFFECT_UPDATED, OnEffectUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_CREATED, OnEntityCreatedPacket::new); + register(McApiPacketType.ON_NETWORK_ENTITY_CREATED, OnNetworkEntityCreatedPacket::new); + register(McApiPacketType.ON_ENTITY_DESTROYED, OnEntityDestroyedPacket::new); + register(McApiPacketType.ON_ENTITY_VISIBILITY_UPDATED, OnEntityVisibilityUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_WORLD_ID_UPDATED, OnEntityWorldIdUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_NAME_UPDATED, OnEntityNameUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_MUTE_UPDATED, OnEntityMuteUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_DEAFEN_UPDATED, OnEntityDeafenUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_SERVER_MUTE_UPDATED, OnEntityServerMuteUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_SERVER_DEAFEN_UPDATED, OnEntityServerDeafenUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_TALK_BITMASK_UPDATED, OnEntityTalkBitmaskUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_LISTEN_BITMASK_UPDATED, OnEntityListenBitmaskUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_EFFECT_BITMASK_UPDATED, OnEntityEffectBitmaskUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_POSITION_UPDATED, OnEntityPositionUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_ROTATION_UPDATED, OnEntityRotationUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_CAVE_FACTOR_UPDATED, OnEntityCaveFactorUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_MUFFLE_FACTOR_UPDATED, OnEntityMuffleFactorUpdatedPacket::new); + register(McApiPacketType.ON_ENTITY_AUDIO_RECEIVED, OnEntityAudioReceivedPacket::new); + } + + private McProtocolCodec() { + } + + public static byte[] encode(McApiPacket packet) { + NetDataWriter writer = new NetDataWriter(); + writer.putByte(packet.packetType().ordinal()); + packet.serialize(writer); + return writer.copyData(); + } + + public static McApiPacket decode(byte[] data) { + NetDataReader reader = new NetDataReader(data); + McApiPacketType type = McApiPacketType.fromByte(reader.getByte()); + Supplier factory = PACKET_FACTORIES.get(type); + if (factory == null) { + throw new IllegalArgumentException("Unsupported packet type: " + type); + } + McApiPacket packet = factory.get(); + packet.deserialize(reader); + return packet; + } + + public static AudioEffect readEffect(EffectType effectType, NetDataReader reader) { + if (effectType == null || effectType == EffectType.NONE) { + return null; + } + AudioEffect effect = switch (effectType) { + case VISIBILITY -> new VisibilityEffect(); + case PROXIMITY -> new ProximityEffect(); + case DIRECTIONAL -> new DirectionalEffect(); + case PROXIMITY_ECHO -> new ProximityEchoEffect(); + case ECHO -> new EchoEffect(); + case PROXIMITY_MUFFLE -> new ProximityMuffleEffect(); + case MUFFLE -> new MuffleEffect(); + case NONE -> null; + }; + if (effect != null) { + effect.deserialize(reader); + } + return effect; + } + + private static void register(McApiPacketType type, Supplier factory) { + PACKET_FACTORIES.put(type, factory); + } +} diff --git a/modules/core/src/main/java/team/avion/protocol/McProtocolConstants.java b/modules/core/src/main/java/team/avion/protocol/McProtocolConstants.java new file mode 100644 index 0000000..80ca01f --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/McProtocolConstants.java @@ -0,0 +1,10 @@ +package team.avion.protocol; + +public final class McProtocolConstants { + public static final int MAXIMUM_ENCODED_BYTES = 1000; + public static final int MAX_STRING_LENGTH = 100; + public static final int MAX_DESCRIPTION_STRING_LENGTH = 500; + + private McProtocolConstants() { + } +} diff --git a/modules/core/src/main/java/team/avion/protocol/McProtocolEnums.java b/modules/core/src/main/java/team/avion/protocol/McProtocolEnums.java new file mode 100644 index 0000000..6354395 --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/McProtocolEnums.java @@ -0,0 +1,175 @@ +package team.avion.protocol; + +public final class McProtocolEnums { + private McProtocolEnums() { + } + + public enum McApiConnectionState { + DISCONNECTED, + CONNECTING, + CONNECTED, + DISCONNECTING + } + + public enum McApiPacketType { + LOGIN_REQUEST, + LOGOUT_REQUEST, + PING_REQUEST, + ACCEPT_RESPONSE, + DENY_RESPONSE, + PING_RESPONSE, + RESET_REQUEST, + SET_EFFECT_REQUEST, + CLEAR_EFFECTS_REQUEST, + CREATE_ENTITY_REQUEST, + DESTROY_ENTITY_REQUEST, + ENTITY_AUDIO_REQUEST, + SET_ENTITY_TITLE_REQUEST, + SET_ENTITY_DESCRIPTION_REQUEST, + SET_ENTITY_WORLD_ID_REQUEST, + SET_ENTITY_NAME_REQUEST, + SET_ENTITY_MUTE_REQUEST, + SET_ENTITY_DEAFEN_REQUEST, + SET_ENTITY_TALK_BITMASK_REQUEST, + SET_ENTITY_LISTEN_BITMASK_REQUEST, + SET_ENTITY_EFFECT_BITMASK_REQUEST, + SET_ENTITY_POSITION_REQUEST, + SET_ENTITY_ROTATION_REQUEST, + SET_ENTITY_CAVE_FACTOR_REQUEST, + SET_ENTITY_MUFFLE_FACTOR_REQUEST, + RESET_RESPONSE, + CREATE_ENTITY_RESPONSE, + DESTROY_ENTITY_RESPONSE, + ON_EFFECT_UPDATED, + ON_ENTITY_CREATED, + ON_NETWORK_ENTITY_CREATED, + ON_ENTITY_DESTROYED, + ON_ENTITY_VISIBILITY_UPDATED, + ON_ENTITY_WORLD_ID_UPDATED, + ON_ENTITY_NAME_UPDATED, + ON_ENTITY_MUTE_UPDATED, + ON_ENTITY_DEAFEN_UPDATED, + ON_ENTITY_SERVER_MUTE_UPDATED, + ON_ENTITY_SERVER_DEAFEN_UPDATED, + ON_ENTITY_TALK_BITMASK_UPDATED, + ON_ENTITY_LISTEN_BITMASK_UPDATED, + ON_ENTITY_EFFECT_BITMASK_UPDATED, + ON_ENTITY_POSITION_UPDATED, + ON_ENTITY_ROTATION_UPDATED, + ON_ENTITY_CAVE_FACTOR_UPDATED, + ON_ENTITY_MUFFLE_FACTOR_UPDATED, + ON_ENTITY_AUDIO_RECEIVED; + + public static McApiPacketType fromByte(int value) { + McApiPacketType[] values = values(); + if (value < 0 || value >= values.length) { + throw new IllegalArgumentException("Unknown packet type id: " + value); + } + return values[value]; + } + } + + public enum PositioningType { + SERVER, + CLIENT; + + public static PositioningType fromByte(int value) { + PositioningType[] values = values(); + if (value < 0 || value >= values.length) { + throw new IllegalArgumentException("Unknown positioning type id: " + value); + } + return values[value]; + } + } + + public enum EffectType { + NONE, + VISIBILITY, + PROXIMITY, + DIRECTIONAL, + PROXIMITY_ECHO, + ECHO, + PROXIMITY_MUFFLE, + MUFFLE; + + public static EffectType fromByte(int value) { + EffectType[] values = values(); + if (value < 0 || value >= values.length) { + throw new IllegalArgumentException("Unknown effect type id: " + value); + } + return values[value]; + } + } + + public enum CreateEntityResponseCode { + OK(0), + FAILURE(-1); + + private final int code; + + CreateEntityResponseCode(int code) { + this.code = code; + } + + public int code() { + return code; + } + + public static CreateEntityResponseCode fromCode(int code) { + for (CreateEntityResponseCode value : values()) { + if (value.code == code) { + return value; + } + } + throw new IllegalArgumentException("Unknown create entity response code: " + code); + } + } + + public enum DestroyEntityResponseCode { + OK(0), + NOT_FOUND(-1); + + private final int code; + + DestroyEntityResponseCode(int code) { + this.code = code; + } + + public int code() { + return code; + } + + public static DestroyEntityResponseCode fromCode(int code) { + for (DestroyEntityResponseCode value : values()) { + if (value.code == code) { + return value; + } + } + throw new IllegalArgumentException("Unknown destroy entity response code: " + code); + } + } + + public enum ResetResponseCode { + OK(0), + FAILURE(-1); + + private final int code; + + ResetResponseCode(int code) { + this.code = code; + } + + public int code() { + return code; + } + + public static ResetResponseCode fromCode(int code) { + for (ResetResponseCode value : values()) { + if (value.code == code) { + return value; + } + } + throw new IllegalArgumentException("Unknown reset response code: " + code); + } + } +} diff --git a/modules/core/src/main/java/team/avion/protocol/McProtocolTypes.java b/modules/core/src/main/java/team/avion/protocol/McProtocolTypes.java new file mode 100644 index 0000000..34b2c25 --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/McProtocolTypes.java @@ -0,0 +1,35 @@ +package team.avion.protocol; + +import java.util.UUID; + +public final class McProtocolTypes { + private McProtocolTypes() { + } + + public record Version(int major, int minor, int build) { + } + + public record Vector2(float x, float y) { + } + + public record Vector3(float x, float y, float z) { + } + + public record Guid(UUID value) { + public static Guid empty() { + return new Guid(new UUID(0L, 0L)); + } + + public static Guid parse(String value) { + if (value == null || value.isBlank()) { + return empty(); + } + return new Guid(UUID.fromString(value)); + } + + @Override + public String toString() { + return value.toString(); + } + } +} diff --git a/modules/core/src/main/java/team/avion/protocol/McTcpFrameCodec.java b/modules/core/src/main/java/team/avion/protocol/McTcpFrameCodec.java new file mode 100644 index 0000000..5a1f423 --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/McTcpFrameCodec.java @@ -0,0 +1,138 @@ +package team.avion.protocol; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public final class McTcpFrameCodec { + public static final int FRAME_HEADER_SIZE = 12; + public static final int MAX_FRAME_PAYLOAD_LENGTH = 1024 * 1024; + public static final int FRAME_MAGIC = 0x4D435450; // MCTP + public static final short FRAME_VERSION = 1; + public static final short REQUEST_KIND = 1; + public static final short RESPONSE_KIND = 2; + + private McTcpFrameCodec() { + } + + public static byte[] encodeRequest(String token, List packets) { + return encodeFrame(REQUEST_KIND, encodePayload(token, packets)); + } + + public static byte[] encodeResponse(List packets) { + return encodeFrame(RESPONSE_KIND, encodePayload("", packets)); + } + + public static DecodedFrame decodeFrame(byte[] frame) { + if (frame.length < FRAME_HEADER_SIZE) { + throw new IllegalArgumentException("Frame is shorter than the MCTP header."); + } + + ByteBuffer header = ByteBuffer.wrap(frame).order(ByteOrder.BIG_ENDIAN); + int magic = header.getInt(); + short version = header.getShort(); + short kind = header.getShort(); + int payloadLength = header.getInt(); + + if (magic != FRAME_MAGIC) { + throw new IllegalArgumentException("Unexpected frame magic: " + magic); + } + if (version != FRAME_VERSION) { + throw new IllegalArgumentException("Unsupported frame version: " + version); + } + if (payloadLength < 0 || payloadLength > MAX_FRAME_PAYLOAD_LENGTH) { + throw new IllegalArgumentException("Invalid frame payload length: " + payloadLength); + } + if (frame.length != FRAME_HEADER_SIZE + payloadLength) { + throw new IllegalArgumentException("Frame length does not match encoded payload length."); + } + + byte[] payload = new byte[payloadLength]; + System.arraycopy(frame, FRAME_HEADER_SIZE, payload, 0, payloadLength); + return new DecodedFrame(kind, decodePayload(payload)); + } + + public static byte[] encodePayload(String token, List packets) { + byte[] tokenBytes = token == null || token.isEmpty() ? new byte[0] : token.getBytes(StandardCharsets.UTF_8); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + writeInt(output, tokenBytes.length); + output.writeBytes(tokenBytes); + writeInt(output, packets.size()); + for (byte[] packet : packets) { + writeInt(output, packet.length); + output.writeBytes(packet); + } + return output.toByteArray(); + } + + public static DecodedPayload decodePayload(byte[] payload) { + ByteBuffer buffer = ByteBuffer.wrap(payload).order(ByteOrder.BIG_ENDIAN); + if (buffer.remaining() < 8) { + throw new IllegalArgumentException("Payload is too short."); + } + + int tokenLength = buffer.getInt(); + if (tokenLength < 0 || buffer.remaining() < tokenLength + 4) { + throw new IllegalArgumentException("Payload token section is invalid."); + } + + byte[] tokenBytes = new byte[tokenLength]; + buffer.get(tokenBytes); + String token = new String(tokenBytes, StandardCharsets.UTF_8); + + int packetCount = buffer.getInt(); + if (packetCount < 0) { + throw new IllegalArgumentException("Payload packet count is invalid."); + } + + List packets = new ArrayList<>(packetCount); + for (int i = 0; i < packetCount; i++) { + if (buffer.remaining() < 4) { + throw new IllegalArgumentException("Packet length prefix is missing."); + } + int packetLength = buffer.getInt(); + if (packetLength <= 0 || buffer.remaining() < packetLength) { + throw new IllegalArgumentException("Packet length is invalid."); + } + byte[] packet = new byte[packetLength]; + buffer.get(packet); + packets.add(packet); + } + + if (buffer.hasRemaining()) { + throw new IllegalArgumentException("Payload has trailing bytes."); + } + + return new DecodedPayload(token, packets); + } + + private static byte[] encodeFrame(short kind, byte[] payload) { + if (payload.length > MAX_FRAME_PAYLOAD_LENGTH) { + throw new IllegalArgumentException("Frame payload exceeds the maximum allowed size."); + } + + ByteBuffer buffer = ByteBuffer.allocate(FRAME_HEADER_SIZE + payload.length).order(ByteOrder.BIG_ENDIAN); + buffer.putInt(FRAME_MAGIC); + buffer.putShort(FRAME_VERSION); + buffer.putShort(kind); + buffer.putInt(payload.length); + buffer.put(payload); + return buffer.array(); + } + + private static void writeInt(ByteArrayOutputStream output, int value) { + output.write((value >>> 24) & 0xFF); + output.write((value >>> 16) & 0xFF); + output.write((value >>> 8) & 0xFF); + output.write(value & 0xFF); + } + + public record DecodedFrame(short kind, DecodedPayload payload) { + } + + public record DecodedPayload(String token, List packets) { + } +} diff --git a/modules/core/src/main/java/team/avion/protocol/NetDataReader.java b/modules/core/src/main/java/team/avion/protocol/NetDataReader.java new file mode 100644 index 0000000..85150a0 --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/NetDataReader.java @@ -0,0 +1,105 @@ +package team.avion.protocol; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +public final class NetDataReader { + private byte[] data; + private int dataSize; + private int offset; + + public NetDataReader() { + clear(); + } + + public NetDataReader(byte[] buffer) { + setBufferSource(buffer); + } + + public void setBufferSource(byte[] buffer) { + data = buffer == null ? new byte[0] : buffer; + dataSize = data.length; + offset = 0; + } + + public void clear() { + data = new byte[0]; + dataSize = 0; + offset = 0; + } + + public int availableBytes() { + return dataSize - offset; + } + + public byte[] copyData() { + return Arrays.copyOf(data, dataSize); + } + + public float getFloat() { + return littleEndian(4).getFloat(); + } + + public double getDouble() { + return littleEndian(8).getDouble(); + } + + public int getSbyte() { + return data[offset++]; + } + + public short getShort() { + return littleEndian(2).getShort(); + } + + public int getInt() { + return littleEndian(4).getInt(); + } + + public long getLong() { + return littleEndian(8).getLong(); + } + + public int getByte() { + return data[offset++] & 0xFF; + } + + public int getUshort() { + return getShort() & 0xFFFF; + } + + public boolean getBool() { + return getByte() == 1; + } + + public String getString(int maxLength) { + int encodedCount = getUshort(); + if (encodedCount == 0) { + return ""; + } + + int count = encodedCount - 1; + if (count < 0 || count > availableBytes()) { + throw new IllegalStateException("Invalid string size in packet"); + } + String value = new String(data, offset, count, StandardCharsets.UTF_8); + offset += count; + if (maxLength > 0 && value.length() > maxLength) { + return ""; + } + return value; + } + + public void getBytes(byte[] destination, int length) { + System.arraycopy(data, offset, destination, 0, length); + offset += length; + } + + private ByteBuffer littleEndian(int width) { + ByteBuffer buffer = ByteBuffer.wrap(data, offset, width).order(ByteOrder.LITTLE_ENDIAN); + offset += width; + return buffer; + } +} diff --git a/modules/core/src/main/java/team/avion/protocol/NetDataWriter.java b/modules/core/src/main/java/team/avion/protocol/NetDataWriter.java new file mode 100644 index 0000000..989e3c0 --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/NetDataWriter.java @@ -0,0 +1,104 @@ +package team.avion.protocol; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +public final class NetDataWriter { + private byte[] data; + private int offset; + + public NetDataWriter() { + this(new byte[0]); + } + + public NetDataWriter(byte[] initialBuffer) { + this.data = initialBuffer == null ? new byte[0] : initialBuffer; + this.offset = 0; + } + + public int length() { + return offset; + } + + public void reset() { + offset = 0; + } + + public byte[] copyData() { + return Arrays.copyOf(data, offset); + } + + public void putFloat(float value) { + putBytes(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putFloat(value).array(), 0, 4); + } + + public void putDouble(double value) { + putBytes(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putDouble(value).array(), 0, 8); + } + + public void putSbyte(int value) { + ensureCapacity(offset + 1); + data[offset++] = (byte) value; + } + + public void putShort(int value) { + putBytes(ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) value).array(), 0, 2); + } + + public void putInt(int value) { + putBytes(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(value).array(), 0, 4); + } + + public void putLong(long value) { + putBytes(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).array(), 0, 8); + } + + public void putByte(int value) { + ensureCapacity(offset + 1); + data[offset++] = (byte) (value & 0xFF); + } + + public void putUshort(int value) { + putShort(value & 0xFFFF); + } + + public void putBool(boolean value) { + putByte(value ? 1 : 0); + } + + public void putString(String value, int maxLength) { + if (value == null || value.isEmpty()) { + putUshort(0); + return; + } + + String limited = value; + if (maxLength > 0 && limited.length() > maxLength) { + limited = limited.substring(0, maxLength); + } + + byte[] encoded = limited.getBytes(StandardCharsets.UTF_8); + int encodedCount = encoded.length + 1; + if (encodedCount > 0xFFFF) { + throw new IllegalArgumentException("Exceeded allowed number of encoded bytes"); + } + putUshort(encodedCount); + putBytes(encoded, 0, encoded.length); + } + + public void putBytes(byte[] value, int sourceOffset, int length) { + ensureCapacity(offset + length); + System.arraycopy(value, sourceOffset, data, offset, length); + offset += length; + } + + private void ensureCapacity(int newSize) { + if (data.length >= newSize) { + return; + } + int resized = Math.max(newSize, Math.max(1, data.length * 2)); + data = Arrays.copyOf(data, resized); + } +} diff --git a/modules/core/src/main/java/team/avion/protocol/VoiceCraftProtocol.java b/modules/core/src/main/java/team/avion/protocol/VoiceCraftProtocol.java new file mode 100644 index 0000000..f3fb83b --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/VoiceCraftProtocol.java @@ -0,0 +1,15 @@ +package team.avion.protocol; + +import team.avion.protocol.McProtocolTypes.Version; + +public final class VoiceCraftProtocol { + public static final int MAJOR = 1; + public static final int MINOR = 6; + public static final int PATCH = 0; + + public static final Version VERSION = new Version(MAJOR, MINOR, PATCH); + public static final String VERSION_STRING = MAJOR + "." + MINOR + "." + PATCH; + + private VoiceCraftProtocol() { + } +} diff --git a/modules/core/src/main/java/team/avion/protocol/VoiceCraftRuntimeDefaults.java b/modules/core/src/main/java/team/avion/protocol/VoiceCraftRuntimeDefaults.java new file mode 100644 index 0000000..edad549 --- /dev/null +++ b/modules/core/src/main/java/team/avion/protocol/VoiceCraftRuntimeDefaults.java @@ -0,0 +1,11 @@ +package team.avion.protocol; + +public final class VoiceCraftRuntimeDefaults { + public static final String GITHUB_REPOSITORY = "AvionBlock/VoiceCraft"; + public static final String RELEASE = "latest"; + public static final String INSTALL_DIRECTORY = "voicecraft-runtime"; + public static final String EXECUTABLE_BASE_NAME = "VoiceCraft.Server"; + + private VoiceCraftRuntimeDefaults() { + } +} diff --git a/modules/core/src/main/java/team/avion/proxy/ProxyMessageCodec.java b/modules/core/src/main/java/team/avion/proxy/ProxyMessageCodec.java new file mode 100644 index 0000000..7281d3b --- /dev/null +++ b/modules/core/src/main/java/team/avion/proxy/ProxyMessageCodec.java @@ -0,0 +1,147 @@ +package team.avion.proxy; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +public final class ProxyMessageCodec { + public static final String CHANNEL_NAME = "geyservoice:main"; + + public static final String SNAPSHOT = "PlayerSnapshot"; + public static final String BIND_REQUEST = "BindRequest"; + public static final String UNBIND_REQUEST = "UnbindRequest"; + public static final String BIND_SYNC = "BindSync"; + + private ProxyMessageCodec() { + } + + public static byte[] encodeSnapshot(ProxyPlayerSnapshot snapshot) { + return write(writer -> { + writer.writeUTF(SNAPSHOT); + writer.writeUTF(snapshot.playerId()); + writer.writeUTF(snapshot.playerName()); + writer.writeUTF(snapshot.dimensionId()); + writer.writeDouble(snapshot.x()); + writer.writeDouble(snapshot.y()); + writer.writeDouble(snapshot.z()); + writer.writeDouble(snapshot.rotation()); + writer.writeDouble(snapshot.echoFactor()); + writer.writeBoolean(snapshot.muffled()); + writer.writeBoolean(snapshot.dead()); + }); + } + + public static ProxyPlayerSnapshot decodeSnapshot(byte[] message) { + return read(message, reader -> { + expect(reader.readUTF(), SNAPSHOT); + return new ProxyPlayerSnapshot( + reader.readUTF(), + reader.readUTF(), + reader.readUTF(), + reader.readDouble(), + reader.readDouble(), + reader.readDouble(), + reader.readDouble(), + reader.readDouble(), + reader.readBoolean(), + reader.readBoolean()); + }); + } + + public static byte[] encodeBindRequest(String playerId, String playerName, int bindingKey) { + return write(writer -> { + writer.writeUTF(BIND_REQUEST); + writer.writeUTF(playerId); + writer.writeUTF(playerName); + writer.writeInt(bindingKey); + }); + } + + public static BindRequest decodeBindRequest(byte[] message) { + return read(message, reader -> { + expect(reader.readUTF(), BIND_REQUEST); + return new BindRequest(reader.readUTF(), reader.readUTF(), reader.readInt()); + }); + } + + public static byte[] encodeUnbindRequest(String playerId, String playerName) { + return write(writer -> { + writer.writeUTF(UNBIND_REQUEST); + writer.writeUTF(playerId); + writer.writeUTF(playerName); + }); + } + + public static UnbindRequest decodeUnbindRequest(byte[] message) { + return read(message, reader -> { + expect(reader.readUTF(), UNBIND_REQUEST); + return new UnbindRequest(reader.readUTF(), reader.readUTF()); + }); + } + + public static byte[] encodeBindSync(String playerName, boolean bound) { + return write(writer -> { + writer.writeUTF(BIND_SYNC); + writer.writeUTF(playerName); + writer.writeBoolean(bound); + }); + } + + public static BindSync decodeBindSync(byte[] message) { + return read(message, reader -> { + expect(reader.readUTF(), BIND_SYNC); + return new BindSync(reader.readUTF(), reader.readBoolean()); + }); + } + + public static String peekType(byte[] message) { + return read(message, input -> input.readUTF()); + } + + private static byte[] write(Writer writer) { + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (DataOutputStream dataOutput = new DataOutputStream(output)) { + writer.write(dataOutput); + } + return output.toByteArray(); + } catch (IOException exception) { + throw new IllegalStateException("Failed to encode proxy message.", exception); + } + } + + private static T read(byte[] message, Reader reader) { + try (DataInputStream dataInput = new DataInputStream(new ByteArrayInputStream(message))) { + return reader.read(dataInput); + } catch (IOException exception) { + throw new IllegalArgumentException("Failed to decode proxy message.", exception); + } + } + + private static void expect(String actual, String expected) { + if (!expected.equals(actual)) { + throw new IllegalArgumentException( + "Unexpected proxy message type: " + actual + ", expected " + expected); + } + } + + @FunctionalInterface + private interface Writer { + void write(DataOutputStream output) throws IOException; + } + + @FunctionalInterface + private interface Reader { + T read(DataInputStream input) throws IOException; + } + + public record BindRequest(String playerId, String playerName, int bindingKey) { + } + + public record UnbindRequest(String playerId, String playerName) { + } + + public record BindSync(String playerName, boolean bound) { + } +} diff --git a/modules/core/src/main/java/team/avion/proxy/ProxyPlayerSnapshot.java b/modules/core/src/main/java/team/avion/proxy/ProxyPlayerSnapshot.java new file mode 100644 index 0000000..cd41866 --- /dev/null +++ b/modules/core/src/main/java/team/avion/proxy/ProxyPlayerSnapshot.java @@ -0,0 +1,19 @@ +package team.avion.proxy; + +public record ProxyPlayerSnapshot( + String playerId, + String playerName, + String dimensionId, + double x, + double y, + double z, + double rotation, + double echoFactor, + boolean muffled, + boolean dead) { + + public ProxyPlayerSnapshot withDimensionId(String newDimensionId) { + return new ProxyPlayerSnapshot(playerId, playerName, newDimensionId, x, y, z, rotation, echoFactor, muffled, + dead); + } +} diff --git a/modules/core/src/main/java/team/avion/proxy/VoiceCraftProxySessionManager.java b/modules/core/src/main/java/team/avion/proxy/VoiceCraftProxySessionManager.java new file mode 100644 index 0000000..a37c646 --- /dev/null +++ b/modules/core/src/main/java/team/avion/proxy/VoiceCraftProxySessionManager.java @@ -0,0 +1,485 @@ +package team.avion.proxy; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import team.avion.common.utils.BaseLogger; +import team.avion.protocol.McApiTcpClient; +import team.avion.protocol.McPackets.AcceptResponsePacket; +import team.avion.protocol.McPackets.ClearEffectsRequestPacket; +import team.avion.protocol.McPackets.DenyResponsePacket; +import team.avion.protocol.McPackets.McApiPacket; +import team.avion.protocol.McPackets.OnEntityDestroyedPacket; +import team.avion.protocol.McPackets.OnNetworkEntityCreatedPacket; +import team.avion.protocol.McPackets.PingRequestPacket; +import team.avion.protocol.McPackets.ProximityEffect; +import team.avion.protocol.McPackets.SetEffectRequestPacket; +import team.avion.protocol.McPackets.SetEntityCaveFactorRequestPacket; +import team.avion.protocol.McPackets.SetEntityDescriptionRequestPacket; +import team.avion.protocol.McPackets.SetEntityEffectBitmaskRequestPacket; +import team.avion.protocol.McPackets.SetEntityMuffleFactorRequestPacket; +import team.avion.protocol.McPackets.SetEntityNameRequestPacket; +import team.avion.protocol.McPackets.SetEntityPositionRequestPacket; +import team.avion.protocol.McPackets.SetEntityRotationRequestPacket; +import team.avion.protocol.McPackets.SetEntityWorldIdRequestPacket; +import team.avion.protocol.McProtocolTypes.Vector2; +import team.avion.protocol.McProtocolTypes.Vector3; + +public final class VoiceCraftProxySessionManager { + private static final int EFFECT_BITMASK_PROXIMITY = 1; + + private final McApiTcpClient client; + private final SecureRandom random = new SecureRandom(); + + private final Map unboundEntitiesByKey = new HashMap<>(); + private final Map unboundEntitiesById = new HashMap<>(); + private final Map boundEntitiesByPlayerId = new HashMap<>(); + private final Map snapshotsByPlayerId = new HashMap<>(); + + private String host = ""; + private int port; + private String loginKey = ""; + private int proximityDistance = 30; + private boolean proximityToggle = true; + private boolean voiceEffects = true; + + public VoiceCraftProxySessionManager(BaseLogger logger) { + this.client = new McApiTcpClient(logger); + } + + public synchronized void configure(String host, int port, String loginKey, int proximityDistance, + boolean proximityToggle, boolean voiceEffects) { + this.host = host == null ? "" : host; + this.port = port; + this.loginKey = loginKey == null ? "" : loginKey; + this.proximityDistance = proximityDistance; + this.proximityToggle = proximityToggle; + this.voiceEffects = voiceEffects; + } + + public synchronized boolean connect() { + if (host.isBlank() || loginKey.isBlank() || port <= 0) { + return false; + } + + clearEntityState(); + if (!client.connect(host, port, loginKey)) { + return false; + } + + applyGlobalSettings(); + processPackets(client.exchange(List.of(new PingRequestPacket()))); + return client.isConnected(); + } + + public synchronized void disconnect() { + client.logout(); + clearSessionState(); + } + + public synchronized void close() { + client.close(); + clearSessionState(); + } + + public synchronized boolean isConnected() { + return client.isConnected(); + } + + public synchronized String getSessionToken() { + return client.getSessionToken(); + } + + public synchronized boolean updateSettings(int proximityDistance, boolean proximityToggle, boolean voiceEffects) { + this.proximityDistance = proximityDistance; + this.proximityToggle = proximityToggle; + this.voiceEffects = voiceEffects; + + if (!client.isConnected()) { + return false; + } + + applyGlobalSettings(); + return client.isConnected(); + } + + public synchronized void updatePlayerSnapshot(ProxyPlayerSnapshot snapshot) { + snapshotsByPlayerId.put(snapshot.playerId(), snapshot); + } + + public synchronized void removePlayerSnapshot(String playerId) { + snapshotsByPlayerId.remove(playerId); + } + + public synchronized boolean bindPlayer(int bindingKey, String playerId, String playerName) { + if (!client.isConnected()) { + return false; + } + if (boundEntitiesByPlayerId.containsKey(playerId)) { + return true; + } + + PendingEntity pendingEntity = unboundEntitiesByKey.get(bindingKey); + if (pendingEntity == null) { + processPackets(client.exchange(List.of(new PingRequestPacket()))); + pendingEntity = unboundEntitiesByKey.get(bindingKey); + } + if (pendingEntity == null) { + return false; + } + + ProxyPlayerSnapshot snapshot = snapshotsByPlayerId.getOrDefault(playerId, + new ProxyPlayerSnapshot(playerId, playerName, "", 0.0, 0.0, 0.0, 0.0, 0.0, false, false)); + return bindEntityToPlayer(pendingEntity, snapshot); + } + + public synchronized boolean bindFakePlayer(int bindingKey, String playerId, String playerName) { + return bindPlayer(bindingKey, playerId, playerName); + } + + public synchronized boolean unbindPlayer(String playerId) { + BoundEntity boundEntity = boundEntitiesByPlayerId.remove(playerId); + if (boundEntity == null) { + return false; + } + + PendingEntity pendingEntity = new PendingEntity(boundEntity.entityId(), boundEntity.userGuid(), + boundEntity.serverUserGuid(), boundEntity.locale()); + assignBindingKey(pendingEntity); + return client.isConnected(); + } + + public synchronized boolean tick() { + if (!client.isConnected()) { + return false; + } + + List packets = new ArrayList<>(); + packets.add(new PingRequestPacket()); + + for (BoundEntity boundEntity : boundEntitiesByPlayerId.values()) { + ProxyPlayerSnapshot snapshot = snapshotsByPlayerId.get(boundEntity.playerId()); + if (snapshot == null) { + continue; + } + + if (!Objects.equals(boundEntity.playerName(), snapshot.playerName())) { + packets.add( + stringPacket(new SetEntityNameRequestPacket(), boundEntity.entityId(), snapshot.playerName())); + boundEntity.playerName = snapshot.playerName(); + } + + if (!Objects.equals(boundEntity.worldId(), snapshot.dimensionId())) { + packets.add(stringPacket(new SetEntityWorldIdRequestPacket(), boundEntity.entityId(), + snapshot.dimensionId())); + boundEntity.worldId = snapshot.dimensionId(); + } + + int currentEffectBitmask = getCurrentEffectBitmask(); + if (boundEntity.effectBitmask() != currentEffectBitmask) { + packets.add( + intPacket(new SetEntityEffectBitmaskRequestPacket(), boundEntity.entityId(), + currentEffectBitmask)); + boundEntity.effectBitmask = currentEffectBitmask; + } + + packets.add(vector3Packet(new SetEntityPositionRequestPacket(), boundEntity.entityId(), + new Vector3((float) snapshot.x(), (float) snapshot.y(), (float) snapshot.z()))); + packets.add(vector2Packet(new SetEntityRotationRequestPacket(), boundEntity.entityId(), + new Vector2(0.0f, (float) snapshot.rotation()))); + packets.add(floatPacket(new SetEntityCaveFactorRequestPacket(), boundEntity.entityId(), + voiceEffects ? (float) snapshot.echoFactor() : 0.0f)); + packets.add(floatPacket(new SetEntityMuffleFactorRequestPacket(), boundEntity.entityId(), + voiceEffects && snapshot.muffled() ? 1.0f : 0.0f)); + } + + processPackets(client.exchange(packets)); + return client.isConnected(); + } + + private void applyGlobalSettings() { + List packets = new ArrayList<>(); + packets.add(new ClearEffectsRequestPacket()); + + if (proximityToggle) { + ProximityEffect effect = new ProximityEffect(); + effect.MinRange = 0.0f; + effect.MaxRange = proximityDistance; + effect.WetDry = 1.0f; + + SetEffectRequestPacket packet = new SetEffectRequestPacket(); + packet.Bitmask = EFFECT_BITMASK_PROXIMITY; + packet.Effect = effect; + packets.add(packet); + } + + for (BoundEntity boundEntity : boundEntitiesByPlayerId.values()) { + packets.add(intPacket(new SetEntityEffectBitmaskRequestPacket(), boundEntity.entityId(), + getCurrentEffectBitmask())); + boundEntity.effectBitmask = getCurrentEffectBitmask(); + } + + processPackets(client.exchange(packets)); + } + + private void processPackets(List packets) { + for (McApiPacket packet : packets) { + if (packet instanceof AcceptResponsePacket || packet instanceof DenyResponsePacket) { + continue; + } + if (packet instanceof OnNetworkEntityCreatedPacket createdPacket) { + handleNetworkEntityCreated(createdPacket); + continue; + } + if (packet instanceof OnEntityDestroyedPacket destroyedPacket) { + handleEntityDestroyed(destroyedPacket.Id); + } + } + } + + private void handleNetworkEntityCreated(OnNetworkEntityCreatedPacket packet) { + assignBindingKey(new PendingEntity(packet.Id, packet.UserGuid.toString(), packet.ServerUserGuid.toString(), + packet.Locale)); + } + + private void handleEntityDestroyed(int entityId) { + PendingEntity pendingEntity = unboundEntitiesById.remove(entityId); + if (pendingEntity != null) { + unboundEntitiesByKey.remove(pendingEntity.bindingKey()); + } + + BoundEntity removed = null; + for (BoundEntity boundEntity : boundEntitiesByPlayerId.values()) { + if (boundEntity.entityId() == entityId) { + removed = boundEntity; + break; + } + } + + if (removed != null) { + boundEntitiesByPlayerId.remove(removed.playerId()); + } + } + + private boolean bindEntityToPlayer(PendingEntity pendingEntity, ProxyPlayerSnapshot snapshot) { + unboundEntitiesByKey.remove(pendingEntity.bindingKey()); + unboundEntitiesById.remove(pendingEntity.entityId()); + + List packets = new ArrayList<>(); + packets.add(stringPacket(new SetEntityNameRequestPacket(), pendingEntity.entityId(), snapshot.playerName())); + packets.add(stringPacket(new SetEntityDescriptionRequestPacket(), pendingEntity.entityId(), + "Bound to player " + snapshot.playerName())); + packets.add( + stringPacket(new SetEntityWorldIdRequestPacket(), pendingEntity.entityId(), snapshot.dimensionId())); + packets.add(intPacket(new SetEntityEffectBitmaskRequestPacket(), pendingEntity.entityId(), + getCurrentEffectBitmask())); + packets.add(vector3Packet(new SetEntityPositionRequestPacket(), pendingEntity.entityId(), + new Vector3((float) snapshot.x(), (float) snapshot.y(), (float) snapshot.z()))); + packets.add(vector2Packet(new SetEntityRotationRequestPacket(), pendingEntity.entityId(), + new Vector2(0.0f, (float) snapshot.rotation()))); + packets.add(floatPacket(new SetEntityCaveFactorRequestPacket(), pendingEntity.entityId(), + voiceEffects ? (float) snapshot.echoFactor() : 0.0f)); + packets.add(floatPacket(new SetEntityMuffleFactorRequestPacket(), pendingEntity.entityId(), + voiceEffects && snapshot.muffled() ? 1.0f : 0.0f)); + + processPackets(client.exchange(packets)); + if (!client.isConnected()) { + return false; + } + + boundEntitiesByPlayerId.put(snapshot.playerId(), new BoundEntity(snapshot.playerId(), snapshot.playerName(), + pendingEntity.entityId(), pendingEntity.userGuid(), pendingEntity.serverUserGuid(), + pendingEntity.locale(), snapshot.dimensionId(), getCurrentEffectBitmask())); + return true; + } + + private void assignBindingKey(PendingEntity pendingEntity) { + int bindingKey = nextBindingKey(); + pendingEntity.bindingKey = bindingKey; + unboundEntitiesByKey.put(bindingKey, pendingEntity); + unboundEntitiesById.put(pendingEntity.entityId(), pendingEntity); + + List packets = new ArrayList<>(); + packets.add(stringPacket(new SetEntityNameRequestPacket(), pendingEntity.entityId(), "New Client")); + packets.add(stringPacket(new SetEntityWorldIdRequestPacket(), pendingEntity.entityId(), "")); + packets.add(stringPacket(new SetEntityDescriptionRequestPacket(), pendingEntity.entityId(), + "Welcome! Your binding key is " + bindingKey)); + processPackets(client.exchange(packets)); + } + + private int nextBindingKey() { + int bindingKey = 10000 + random.nextInt(90000); + while (unboundEntitiesByKey.containsKey(bindingKey)) { + bindingKey = 10000 + random.nextInt(90000); + } + return bindingKey; + } + + private int getCurrentEffectBitmask() { + return proximityToggle ? EFFECT_BITMASK_PROXIMITY : 0; + } + + private void clearSessionState() { + clearEntityState(); + snapshotsByPlayerId.clear(); + } + + private void clearEntityState() { + unboundEntitiesByKey.clear(); + unboundEntitiesById.clear(); + boundEntitiesByPlayerId.clear(); + } + + private static SetEntityNameRequestPacket stringPacket(SetEntityNameRequestPacket packet, int entityId, + String value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityDescriptionRequestPacket stringPacket(SetEntityDescriptionRequestPacket packet, + int entityId, + String value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityWorldIdRequestPacket stringPacket(SetEntityWorldIdRequestPacket packet, int entityId, + String value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityEffectBitmaskRequestPacket intPacket(SetEntityEffectBitmaskRequestPacket packet, + int entityId, + int value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityPositionRequestPacket vector3Packet(SetEntityPositionRequestPacket packet, int entityId, + Vector3 value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityRotationRequestPacket vector2Packet(SetEntityRotationRequestPacket packet, int entityId, + Vector2 value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityCaveFactorRequestPacket floatPacket(SetEntityCaveFactorRequestPacket packet, int entityId, + float value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityMuffleFactorRequestPacket floatPacket(SetEntityMuffleFactorRequestPacket packet, + int entityId, + float value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static final class PendingEntity { + private final int entityId; + private final String userGuid; + private final String serverUserGuid; + private final String locale; + private int bindingKey; + + private PendingEntity(int entityId, String userGuid, String serverUserGuid, String locale) { + this.entityId = entityId; + this.userGuid = userGuid; + this.serverUserGuid = serverUserGuid; + this.locale = locale; + } + + private int entityId() { + return entityId; + } + + private String userGuid() { + return userGuid; + } + + private String serverUserGuid() { + return serverUserGuid; + } + + private String locale() { + return locale; + } + + private int bindingKey() { + return bindingKey; + } + } + + private static final class BoundEntity { + private final String playerId; + private String playerName; + private final int entityId; + private final String userGuid; + private final String serverUserGuid; + private final String locale; + private String worldId; + private int effectBitmask; + + private BoundEntity(String playerId, String playerName, int entityId, String userGuid, String serverUserGuid, + String locale, String worldId, int effectBitmask) { + this.playerId = playerId; + this.playerName = playerName; + this.entityId = entityId; + this.userGuid = userGuid; + this.serverUserGuid = serverUserGuid; + this.locale = locale; + this.worldId = worldId; + this.effectBitmask = effectBitmask; + } + + private String playerId() { + return playerId; + } + + private String playerName() { + return playerName; + } + + private int entityId() { + return entityId; + } + + private String userGuid() { + return userGuid; + } + + private String serverUserGuid() { + return serverUserGuid; + } + + private String locale() { + return locale; + } + + private String worldId() { + return worldId; + } + + private int effectBitmask() { + return effectBitmask; + } + } +} diff --git a/modules/paper/build.gradle b/modules/paper/build.gradle new file mode 100644 index 0000000..e678637 --- /dev/null +++ b/modules/paper/build.gradle @@ -0,0 +1,47 @@ +plugins { + id 'com.gradleup.shadow' version '9.4.1' +} + +description = 'Paper adapter for GeyserVoice.' + +dependencies { + implementation project(':core') + compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") + compileOnly('me.clip:placeholderapi:2.12.2') + compileOnly('org.projectlombok:lombok:1.18.44') + annotationProcessor('org.projectlombok:lombok:1.18.44') +} + +processResources { + inputs.properties([ + name : rootProject.ext.pluginName, + version : project.version, + description: rootProject.description, + url : rootProject.ext.pluginUrl, + author : rootProject.ext.pluginAuthor + ]) + + filesMatching('plugin.yml') { + expand( + name: rootProject.ext.pluginName, + version: project.version, + description: rootProject.description, + url: rootProject.ext.pluginUrl, + author: rootProject.ext.pluginAuthor + ) + } +} + +tasks.jar { + archiveBaseName.set("${rootProject.ext.pluginName}-paper") + archiveClassifier.set('thin') +} + +tasks.shadowJar { + archiveBaseName.set("${rootProject.ext.pluginName}-paper") + archiveClassifier.set('') +} + +tasks.build { + dependsOn tasks.shadowJar +} diff --git a/modules/paper/src/main/java/team/avion/paper/GeyserVoice.java b/modules/paper/src/main/java/team/avion/paper/GeyserVoice.java new file mode 100644 index 0000000..a315620 --- /dev/null +++ b/modules/paper/src/main/java/team/avion/paper/GeyserVoice.java @@ -0,0 +1,427 @@ +package team.avion.paper; + +import lombok.Getter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; + +import team.avion.common.BaseGeyserVoice; +import team.avion.common.config.ConfigTemplateWriter; +import team.avion.common.localization.PluginLocalization; +import team.avion.paper.commands.VoiceCommand; +import team.avion.paper.listeners.*; +import team.avion.paper.tasks.PositionsTask; +import team.avion.paper.utils.*; +import team.avion.paper.voicecraft.PaperVoiceCraftSessionManager; +import team.avion.paper.voicecraft.VoiceCraftProcessManager; +import team.avion.proxy.ProxyMessageCodec; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; +import java.util.Objects; +import java.util.Map; +import java.util.HashMap; + +/** + * Main plugin class for GeyserVoice. + */ +public class GeyserVoice extends JavaPlugin implements BaseGeyserVoice { + private static final String TRANSPORT_HOST_PATH = "config.voicecraft.transport.host"; + private static final String TRANSPORT_PORT_PATH = "config.voicecraft.transport.port"; + private static final String TRANSPORT_LOGIN_TOKEN_PATH = "config.voicecraft.transport.login-token"; + private static final String LEGACY_HOST_PATH = "config.voicecraft.host"; + private static final String LEGACY_PORT_PATH = "config.voicecraft.port"; + private static final String LEGACY_LOGIN_TOKEN_PATH = "config.voicecraft.login-token"; + private static final String MANAGED_VOICE_PORT_PATH = "config.voicecraft.voice.port"; + + private static @Getter GeyserVoice instance; + private @Getter boolean isConnected = false; + private @Getter String host = ""; + private @Getter int port = 0; + private @Getter String loginToken = ""; + private @Getter Map playerBinds = new HashMap<>(); + private @Getter String token = ""; + private String lang; + public boolean usesProxy = false; + private final @Getter PluginMessageHandler messageHandler = new PluginMessageHandler(this); + private @Getter VoiceCraftProcessManager voiceCraftProcessManager; + private @Getter PaperVoiceCraftSessionManager sessionManager; + private @Getter PositionsTask positionsTask; + + private BukkitTask taskRunner; + + public PaperLogger Logger = new PaperLogger(); + + /** + * Executes upon enabling the plugin. + */ + @Override + public void onEnable() { + instance = this; + ensureLocalizedConfigExists(); + reloadConfig(); + voiceCraftProcessManager = new VoiceCraftProcessManager(this); + sessionManager = new PaperVoiceCraftSessionManager(this); + + lang = resolveConfiguredLanguage(); + int positionTaskInterval = getConfig().getInt("config.voice.position-update-interval-ticks", 1); + Language.init(this); + + getServer().getMessenger().registerOutgoingPluginChannel(this, ProxyMessageCodec.CHANNEL_NAME); + getServer().getMessenger().registerIncomingPluginChannel(this, ProxyMessageCodec.CHANNEL_NAME, messageHandler); + + VoiceCommand voiceCommand = new VoiceCommand(this, lang); + getCommand("voice").setExecutor(voiceCommand); + getCommand("voice").setTabCompleter(voiceCommand); + positionsTask = new PositionsTask(this, lang); + taskRunner = positionsTask.runTaskTimer(this, 1, positionTaskInterval); + getServer().getPluginManager().registerEvents(new PlayerQuitHandler(this, lang), this); + + if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { + new Placeholder(this).register(); + } + + this.reload(); + } + + @Override + public void onDisable() { + disconnect("Plugin disabled"); + if (voiceCraftProcessManager != null) { + voiceCraftProcessManager.shutdown(); + } + getServer().getMessenger().unregisterOutgoingPluginChannel(this); + getServer().getMessenger().unregisterIncomingPluginChannel(this); + } + + /** + * Reloads the plugin configuration and initializes connections. + */ + public void reload() { + ensureLocalizedConfigExists(); + reloadConfig(); + lang = resolveConfiguredLanguage(); + Logger.info(Language.getMessage(lang, "plugin-config-loaded")); + Logger.info(Language.getMessage(lang, "plugin-command-executor")); + + boolean newUsesProxy = getConfig().getBoolean("config.proxy.enabled", false); + if (sessionManager.isConnected() && usesProxy != newUsesProxy) { + sessionManager.disconnect(); + } + usesProxy = newUsesProxy; + + host = getTransportHost(); + port = getTransportPort(); + loginToken = getTransportLoginToken(); + int proximityDistance = getConfig().getInt("config.voice.proximity-distance"); + boolean proximityToggle = getConfig().getBoolean("config.voice.proximity-toggle"); + boolean voiceEffects = getConfig().getBoolean("config.voice.voice-effects"); + sessionManager.configure(host, port, loginToken, proximityDistance, proximityToggle, voiceEffects); + + if (voiceCraftProcessManager != null) { + voiceCraftProcessManager.ensureRunning(); + } + + if (usesProxy) { + isConnected = true; + token = "proxy"; + } else if (getConfig().getBoolean("config.auto-reconnect")) { + isConnected = reconnect(true); + } else { + isConnected = false; + token = ""; + } + + int positionTaskInterval = getConfig().getInt("config.voice.position-update-interval-ticks", 1); + if (!taskRunner.isCancelled()) + taskRunner.cancel(); + positionsTask = new PositionsTask(this, lang); + taskRunner = positionsTask.runTaskTimer(this, 1, positionTaskInterval); + + updateSettings(proximityDistance, proximityToggle, voiceEffects); + } + + /** + * Connects to a new server. + * + * @param host The host to connect to. + * @param port The port to connect to. + * @param loginToken The VoiceCraft login token. + * @return True if connected successfully, otherwise false. + */ + public Boolean connect(String host, int port, String loginToken) { + if (Objects.nonNull(host) && Objects.nonNull(loginToken)) { + getConfig().set(TRANSPORT_HOST_PATH, host); + getConfig().set(TRANSPORT_PORT_PATH, port); + getConfig().set(TRANSPORT_LOGIN_TOKEN_PATH, loginToken); + saveConfig(); + reloadConfig(); + reload(); + + return isConnected; + } else { + Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); + return false; + } + } + + /** + * Connects to the server. + * + * @param force Indicates whether to force a connection. + * @return True if connected successfully, otherwise false. + */ + public Boolean reconnect(Boolean force) { + if (isConnected && !force) + return true; + if (isConnected) { + disconnect("Reconnecting to another server."); + } + + if (usesProxy) { + Logger.info(Language.getMessage(lang, "plugin-connect-proxy")); + isConnected = true; + token = "proxy"; + return true; + } + + if (Objects.nonNull(host) && Objects.nonNull(loginToken)) { + sessionManager.configure(host, port, loginToken, getConfig().getInt("config.voice.proximity-distance"), + getConfig().getBoolean("config.voice.proximity-toggle"), + getConfig().getBoolean("config.voice.voice-effects")); + boolean connected = sessionManager.connect(); + String Token = connected ? sessionManager.getSessionToken() : null; + if (Objects.nonNull(Token)) { + Logger.info(Language.getMessage(lang, "plugin-connect-connected")); + isConnected = true; + token = Token; + } else { + Logger.warn(Language.getMessage(lang, "plugin-connect-failed")); + } + return isConnected; + } else { + Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); + return false; + } + } + + /** + * Disconnects from the server. + * + * @param reason The reason why we disconnected + */ + public void disconnect(String reason) { + if (!isConnected) + return; + + if (sessionManager.isConnected()) { + sessionManager.disconnect(); + } + + isConnected = false; + token = ""; + + String disconnectMessage = Language.getMessage(lang, "plugin-connection-disconnect").replace("$reason", + reason); + Logger.info(disconnectMessage); + + boolean sendVoipDisconnectMessage = getConfig().getBoolean("config.voice.send-voicecraft-disconnect-message"); + if (sendVoipDisconnectMessage) { + Bukkit.broadcast(Component.text(disconnectMessage).color(NamedTextColor.YELLOW)); + } + } + + /** + * Disconnects from the server. + */ + public void disconnect() { + disconnect("N.A."); + } + + /** + * Binds a player to the voice chat server. + * + * @param playerKey The key associated with the player. + * @param player The player to bind. + * @return True if the binding was successful, otherwise false. + */ + public Boolean bind(int playerKey, Player player, int tries) { + if (!isConnected) + return false; + + if (playerBinds.containsKey(player.getName()) && playerBinds.get(player.getName())) { + return true; + } + if (usesProxy) { + return messageHandler.sendBindRequest(player, playerKey); + } + boolean bound = sessionManager.bindPlayer(playerKey, player); + if (!bound && tries == 0 && !sessionManager.isConnected()) { + Logger.info("VoiceCraft session dropped during bind, reconnecting..."); + isConnected = reconnect(true); + return bind(playerKey, player, 1); + } + if (!bound) { + return false; + } + + Logger.info(Language.getMessage(lang, "player-binded").replace("$player", player.getName())); + + boolean sendBindedMessage = getConfig().getBoolean("config.voice.send-bind-message"); + if (sendBindedMessage) { + Bukkit.broadcast( + Component.text(player.getName()).decorate(TextDecoration.BOLD) + .append(Component.text( + Language.getMessage(lang, "player-binded").replace("$player", "")) + .color(NamedTextColor.DARK_GREEN))); + } + return true; + } + + public Boolean bind(int playerKey, Player player) { + return bind(playerKey, player, 0); + } + + /** + * Bind a fake player + * + * @param bindKey + * @param name + * @return + */ + public Boolean bindFake(int playerKey, String name, int tries) { + if (!isConnected) + return false; + + boolean bound = sessionManager.bindFakePlayer(playerKey, name); + if (!bound && tries == 0 && !sessionManager.isConnected()) { + Logger.info("VoiceCraft session dropped during fake bind, reconnecting..."); + isConnected = reconnect(true); + return bindFake(playerKey, name, 1); + } + return bound; + } + + public Boolean bindFake(int playerKey, String name) { + return bindFake(playerKey, name, 0); + } + + /** + * Disconnects a player from the voice chat server. + * + * @param player The player to disconnect. + * @return True if the disconnection was successful, otherwise false. + */ + public Boolean disconnectPlayer(Player player, int tries) { + if (!isConnected) + return false; + + if (usesProxy) { + return messageHandler.sendUnbindRequest(player); + } + + boolean disconnected = sessionManager.unbindPlayer(player); + if (!disconnected && tries == 0 && !sessionManager.isConnected()) { + Logger.info("VoiceCraft session dropped during unbind, reconnecting..."); + isConnected = reconnect(true); + return disconnectPlayer(player, 1); + } + return disconnected; + } + + public Boolean disconnectPlayer(Player player) { + return disconnectPlayer(player, 0); + } + + /** + * Updates the voice chat settings. + * + * @param proximityDistance Proximity distance setting. + * @param proximityToggle Proximity toggle setting. + * @param voiceEffects Voice effects setting. + * @return True if settings were updated successfully, otherwise false. + */ + public Boolean updateSettings(int proximityDistance, Boolean proximityToggle, Boolean voiceEffects) { + if (!isConnected || usesProxy) + return false; + + return sessionManager.updateSettings(proximityDistance, proximityToggle, voiceEffects); + } + + public void setNotConnected() { + if (!isConnected) + return; + isConnected = false; + token = ""; + if (sessionManager.isConnected()) { + sessionManager.close(); + } + } + + public void saveResource(String resourcePath) { + File outFile = new File(getDataFolder(), resourcePath); + // Default Spigot saveResource gives a warning when the file already exists and + // when you don't override + // Now just skip it if the file exists + if (!outFile.exists()) { + saveResource(resourcePath, false); + } + } + + private String resolveConfiguredLanguage() { + return PluginLocalization.resolveConfiguredLanguage(getConfig().getString("config.lang", "system")); + } + + private void ensureLocalizedConfigExists() { + File configFile = new File(getDataFolder(), "config.yml"); + if (configFile.exists()) { + return; + } + + String language = PluginLocalization.resolveSystemLanguage(); + String templatePath = "config/" + language + ".yml"; + + getDataFolder().mkdirs(); + try (InputStream input = openConfigTemplate(templatePath)) { + if (input == null) { + throw new IOException("Missing embedded config template for language " + language); + } + + ConfigTemplateWriter.write(input, configFile.toPath(), + Map.of("__GENERATED_LOGIN_TOKEN__", UUID.randomUUID().toString())); + } catch (IOException exception) { + Logger.error("Could not create localized config.yml: " + exception.getMessage()); + } + } + + private InputStream openConfigTemplate(String templatePath) { + InputStream input = getResource(templatePath); + return input != null ? input : getResource("config/en.yml"); + } + + public int getManagedVoicePort() { + return getConfig().getInt(MANAGED_VOICE_PORT_PATH, 1111); + } + + private String getTransportHost() { + String value = getConfig().getString(TRANSPORT_HOST_PATH); + return value != null ? value : getConfig().getString(LEGACY_HOST_PATH); + } + + private int getTransportPort() { + int value = getConfig().getInt(TRANSPORT_PORT_PATH, -1); + return value > 0 ? value : getConfig().getInt(LEGACY_PORT_PATH); + } + + private String getTransportLoginToken() { + String value = getConfig().getString(TRANSPORT_LOGIN_TOKEN_PATH); + return value != null ? value : getConfig().getString(LEGACY_LOGIN_TOKEN_PATH); + } +} diff --git a/src/main/java/io/greitan/avion/paper/commands/VoiceCommand.java b/modules/paper/src/main/java/team/avion/paper/commands/VoiceCommand.java similarity index 69% rename from src/main/java/io/greitan/avion/paper/commands/VoiceCommand.java rename to modules/paper/src/main/java/team/avion/paper/commands/VoiceCommand.java index eba18ac..e6728e8 100644 --- a/src/main/java/io/greitan/avion/paper/commands/VoiceCommand.java +++ b/modules/paper/src/main/java/team/avion/paper/commands/VoiceCommand.java @@ -1,4 +1,4 @@ -package io.greitan.avion.paper.commands; +package team.avion.paper.commands; import java.util.List; @@ -11,13 +11,12 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; -import io.greitan.avion.paper.GeyserVoice; -import io.greitan.avion.paper.utils.Language; -import io.greitan.avion.common.commands.BaseVoiceCommand; -import io.greitan.avion.common.utils.IntegerOperation; -import io.greitan.avion.common.utils.StringOperation; -import io.greitan.avion.common.utils.DoubleStringOperation; -import io.greitan.avion.common.utils.EmptyOperation; +import team.avion.paper.GeyserVoice; +import team.avion.paper.utils.Language; +import team.avion.common.commands.BaseVoiceCommand; +import team.avion.common.utils.IntegerOperation; +import team.avion.common.utils.StringOperation; +import team.avion.common.utils.DoubleStringOperation; public class VoiceCommand implements CommandExecutor, TabCompleter { @@ -51,10 +50,9 @@ public boolean execute(String permission) { @Override public void execute(String text, String rawColor) { NamedTextColor color = NamedTextColor.RED; - if (rawColor == "red") color = NamedTextColor.RED; - else if (rawColor == "aqua") color = NamedTextColor.AQUA; - else if (rawColor == "green") color = NamedTextColor.GREEN; - else if (rawColor == "yellow") color = NamedTextColor.YELLOW; + if ("aqua".equals(rawColor)) color = NamedTextColor.AQUA; + else if ("green".equals(rawColor)) color = NamedTextColor.GREEN; + else if ("yellow".equals(rawColor)) color = NamedTextColor.YELLOW; var message = Component.text(Language.getMessage(lang, text)).color(color); if (sender instanceof Player) @@ -72,17 +70,6 @@ public boolean execute(int key) { } return false; } - }, - new EmptyOperation() { - @Override - public boolean execute() { - if (sender instanceof Player) { - Player player = (Player) sender; - plugin.getConfig().set("config.players." + player.getName(), null); - return true; - } - return false; - } } ); } diff --git a/src/main/java/io/greitan/avion/paper/listeners/PlayerQuitHandler.java b/modules/paper/src/main/java/team/avion/paper/listeners/PlayerQuitHandler.java similarity index 79% rename from src/main/java/io/greitan/avion/paper/listeners/PlayerQuitHandler.java rename to modules/paper/src/main/java/team/avion/paper/listeners/PlayerQuitHandler.java index b07fd83..88af83c 100644 --- a/src/main/java/io/greitan/avion/paper/listeners/PlayerQuitHandler.java +++ b/modules/paper/src/main/java/team/avion/paper/listeners/PlayerQuitHandler.java @@ -1,12 +1,12 @@ -package io.greitan.avion.paper.listeners; +package team.avion.paper.listeners; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; -import io.greitan.avion.paper.GeyserVoice; -import io.greitan.avion.paper.utils.Language; +import team.avion.paper.GeyserVoice; +import team.avion.paper.utils.Language; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -26,12 +26,9 @@ public void onPlayerQuit(PlayerQuitEvent event) { boolean isBound = plugin.getPlayerBinds().getOrDefault(player.getName(), false); if (isBound) { - if (!plugin.usesProxy && plugin.isConnected()) { + if (plugin.isConnected()) { handlePlayerDisconnect(player); - } else if (plugin.usesProxy) { - // this will make sure the player bind is removed on leave, even if it cannot be - // received... - // it will be set again if the player joins this server again... + } else { plugin.getPlayerBinds().remove(player.getName()); } } diff --git a/modules/paper/src/main/java/team/avion/paper/listeners/PluginMessageHandler.java b/modules/paper/src/main/java/team/avion/paper/listeners/PluginMessageHandler.java new file mode 100644 index 0000000..9e7c98e --- /dev/null +++ b/modules/paper/src/main/java/team/avion/paper/listeners/PluginMessageHandler.java @@ -0,0 +1,75 @@ +package team.avion.paper.listeners; + +import java.util.Objects; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; + +import team.avion.paper.GeyserVoice; +import team.avion.proxy.ProxyMessageCodec; +import team.avion.proxy.ProxyPlayerSnapshot; + +public class PluginMessageHandler implements PluginMessageListener { + private final GeyserVoice plugin; + + public PluginMessageHandler(GeyserVoice plugin) { + this.plugin = plugin; + } + + public boolean sendSnapshot(Player player, ProxyPlayerSnapshot snapshot) { + try { + player.sendPluginMessage(plugin, ProxyMessageCodec.CHANNEL_NAME, ProxyMessageCodec.encodeSnapshot(snapshot)); + return true; + } catch (Exception exception) { + plugin.Logger.debug("Failed to send proxy snapshot: " + exception.getMessage()); + return false; + } + } + + public boolean sendBindRequest(Player player, int bindingKey) { + try { + player.sendPluginMessage(plugin, ProxyMessageCodec.CHANNEL_NAME, + ProxyMessageCodec.encodeBindRequest(player.getUniqueId().toString(), player.getName(), bindingKey)); + return true; + } catch (Exception exception) { + plugin.Logger.debug("Failed to send proxy bind request: " + exception.getMessage()); + return false; + } + } + + public boolean sendUnbindRequest(Player player) { + try { + player.sendPluginMessage(plugin, ProxyMessageCodec.CHANNEL_NAME, + ProxyMessageCodec.encodeUnbindRequest(player.getUniqueId().toString(), player.getName())); + return true; + } catch (Exception exception) { + plugin.Logger.debug("Failed to send proxy unbind request: " + exception.getMessage()); + return false; + } + } + + @Override + public void onPluginMessageReceived(String channel, Player player, byte[] message) { + if (!ProxyMessageCodec.CHANNEL_NAME.equals(channel) || message == null || message.length == 0) { + return; + } + + String type = ProxyMessageCodec.peekType(Objects.requireNonNull(message, "message")); + if (!ProxyMessageCodec.BIND_SYNC.equals(type)) { + return; + } + + ProxyMessageCodec.BindSync bindSync = ProxyMessageCodec.decodeBindSync(message); + boolean previousState = plugin.getPlayerBinds().getOrDefault(bindSync.playerName(), false); + if (previousState == bindSync.bound()) { + return; + } + + plugin.getPlayerBinds().put(bindSync.playerName(), bindSync.bound()); + if (bindSync.bound()) { + plugin.Logger.info(bindSync.playerName() + " has joined the voicechat!"); + } else { + plugin.Logger.info(bindSync.playerName() + " has left the voicechat!"); + } + } +} diff --git a/modules/paper/src/main/java/team/avion/paper/tasks/PositionsTask.java b/modules/paper/src/main/java/team/avion/paper/tasks/PositionsTask.java new file mode 100644 index 0000000..5c7fd88 --- /dev/null +++ b/modules/paper/src/main/java/team/avion/paper/tasks/PositionsTask.java @@ -0,0 +1,188 @@ +package team.avion.paper.tasks; + +import org.bukkit.scheduler.BukkitRunnable; + +import team.avion.paper.GeyserVoice; +import team.avion.paper.utils.Language; +import team.avion.proxy.ProxyPlayerSnapshot; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import org.bukkit.Bukkit; +import org.bukkit.block.Block; +import org.bukkit.Location; + +import java.util.Arrays; +import java.util.List; +import org.bukkit.entity.Player; +import org.bukkit.World; +import org.bukkit.util.BlockIterator; +import org.bukkit.util.Vector; + +public class PositionsTask extends BukkitRunnable { + private final GeyserVoice plugin; + private final String lang; + private boolean isConnected = false; + private int reconnectRetries = 0; + private boolean reconnecting = false; + + public PositionsTask(GeyserVoice plugin, String lang) { + this.plugin = plugin; + this.lang = lang; + } + + @Override + public void run() { + if (plugin.usesProxy) { + isConnected = plugin.isConnected(); + if (!isConnected) { + return; + } + + for (Player player : plugin.getServer().getOnlinePlayers()) { + plugin.getMessageHandler().sendSnapshot(player, getPlayerSnapshot(player)); + } + return; + } + + if (reconnecting) { + reconnect(); + return; + } + + isConnected = plugin.isConnected(); + List snapshots = plugin.getServer().getOnlinePlayers().stream() + .map(this::getPlayerSnapshot) + .toList(); + if (isConnected && plugin.getSessionManager().tick(snapshots)) { + return; + } + + if (!isConnected) { + return; + } + + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-lost")); + plugin.setNotConnected(); + + if (plugin.getConfig().getBoolean("config.auto-reconnect")) { + if (plugin.getConfig().getBoolean("config.voice.send-connection-lost-message")) { + Bukkit.broadcast(Component.text(Language.getMessage(lang, "plugin-connection-lost-reconnect")) + .color(NamedTextColor.RED)); + } + reconnectRetries = 0; + reconnecting = true; + reconnect(); + return; + } + if (plugin.getConfig().getBoolean("config.voice.send-connection-lost-message")) { + Bukkit.broadcast(Component.text(Language.getMessage(lang, "plugin-connection-lost")) + .color(NamedTextColor.RED)); + } + cancel(); + } + + public double getCaveDensity(Player player) { + if (!isConnected) { + return 0.0; + } + + String[] caveBlocks = { + "STONE", + "DIORITE", + "GRANITE", + "DEEPSLATE", + "TUFF" + }; + + int blockCount = 0; + for (int x = -1; x <= 1; x++) { + for (int y = -1; y <= 1; y++) { + for (int z = -1; z <= 1; z++) { + if (x == 0 && y == 0 && z == 0) + continue; // a vector of 0,0,0 won't go anywhere, so skip it... + Vector direction = new Vector(x, y, z); + blockCount += castRayUntilBlock( + new BlockIterator(player.getWorld(), player.getLocation().toVector(), direction, 0, 50), + caveBlocks); + } + } + } + + // (3 * 3 * 3) - 1 = 26.0 + return blockCount / 26.0; // Total blocks checked + } + + private int castRayUntilBlock(BlockIterator blockIterator, String[] caveBlocks) { + while (blockIterator.hasNext()) { + Block block = blockIterator.next(); + if (block.getType().isSolid()) { + if (Arrays.asList(caveBlocks).contains(getBlockType(block))) { + return 1; + } + break; + } + } + return 0; + } + + private String getBlockType(Block block) { + return block.getType().toString(); + } + + public ProxyPlayerSnapshot getPlayerSnapshot(Player player) { + Location headLocation = player.getEyeLocation(); + return new ProxyPlayerSnapshot( + player.getUniqueId().toString(), + player.getName(), + getDimensionId(player), + headLocation.getX(), + headLocation.getY(), + headLocation.getZ(), + player.getLocation().getYaw(), + player.getWorld().getEnvironment() == World.Environment.NORMAL ? getCaveDensity(player) : 0.0, + player.isInWater(), + player.isDead()); + } + + private String getDimensionId(Player player) { + String worldName = player.getWorld().getName(); + return switch (worldName) { + case "world" -> "minecraft:overworld"; + case "world_nether" -> "minecraft:nether"; + case "world_the_end" -> "minecraft:the_end"; + default -> worldName; + }; + } + + private boolean reconnect() { + if (reconnectRetries >= 5) { + reconnecting = false; + plugin.Logger.error(Language.getMessage(lang, "plugin-connection-reconnecting-failed")); + if (plugin.getConfig().getBoolean("config.voice.send-connection-lost-message")) { + Bukkit.broadcast(Component.text(Language.getMessage(lang, "plugin-connection-reconnecting-failed")) + .color(NamedTextColor.RED)); + } + cancel(); + return false; + } + + reconnectRetries++; + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-attempt").replace("$attempt", + Integer.toString(reconnectRetries))); + + if (plugin.reconnect(true)) { + reconnecting = false; + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-success")); + if (plugin.getConfig().getBoolean("config.voice.send-connection-lost-message")) { + Bukkit.broadcast(Component.text(Language.getMessage(lang, "plugin-connection-reconnecting-success")) + .color(NamedTextColor.GREEN)); + } + return true; + } + + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-failed-retry")); + return false; + } +} diff --git a/src/main/java/io/greitan/avion/paper/utils/Language.java b/modules/paper/src/main/java/team/avion/paper/utils/Language.java similarity index 66% rename from src/main/java/io/greitan/avion/paper/utils/Language.java rename to modules/paper/src/main/java/team/avion/paper/utils/Language.java index 123726d..1d37a1c 100644 --- a/src/main/java/io/greitan/avion/paper/utils/Language.java +++ b/modules/paper/src/main/java/team/avion/paper/utils/Language.java @@ -1,8 +1,9 @@ -package io.greitan.avion.paper.utils; +package team.avion.paper.utils; import org.bukkit.configuration.file.YamlConfiguration; -import io.greitan.avion.paper.GeyserVoice; +import team.avion.common.localization.PluginLocalization; +import team.avion.paper.GeyserVoice; import java.io.File; import java.util.HashMap; @@ -10,18 +11,15 @@ public class Language { private static final Map languageConfigs = new HashMap<>(); - private static String defaultLanguage = "en"; + private static final String defaultLanguage = PluginLocalization.DEFAULT_LANGUAGE; public static void init(GeyserVoice plugin) { File languageFolder = new File(plugin.getDataFolder(), "locale"); - - if (!languageFolder.exists()) { - languageFolder.mkdirs(); - plugin.saveResource("locale/en.yml"); - plugin.saveResource("locale/ru.yml"); - plugin.saveResource("locale/nl.yml"); - plugin.saveResource("locale/ja.yml"); - } + languageFolder.mkdirs(); + plugin.saveResource("locale/en.yml"); + plugin.saveResource("locale/ru.yml"); + plugin.saveResource("locale/nl.yml"); + plugin.saveResource("locale/ja.yml"); loadLanguages(languageFolder.getAbsolutePath()); } @@ -41,8 +39,9 @@ private static void loadLanguages(String pluginFolder) { } public static String getMessage(String language, String key) { - if (languageConfigs.containsKey(language)) { - YamlConfiguration config = languageConfigs.get(language); + String resolvedLanguage = PluginLocalization.resolveConfiguredLanguage(language); + if (languageConfigs.containsKey(resolvedLanguage)) { + YamlConfiguration config = languageConfigs.get(resolvedLanguage); if (config.contains("messages." + key)) { return config.getString("messages." + key); } diff --git a/src/main/java/io/greitan/avion/paper/utils/PaperLogger.java b/modules/paper/src/main/java/team/avion/paper/utils/PaperLogger.java similarity index 96% rename from src/main/java/io/greitan/avion/paper/utils/PaperLogger.java rename to modules/paper/src/main/java/team/avion/paper/utils/PaperLogger.java index 110edd4..9864d3b 100644 --- a/src/main/java/io/greitan/avion/paper/utils/PaperLogger.java +++ b/modules/paper/src/main/java/team/avion/paper/utils/PaperLogger.java @@ -1,10 +1,10 @@ -package io.greitan.avion.paper.utils; +package team.avion.paper.utils; import org.bukkit.Bukkit; import org.bukkit.command.ConsoleCommandSender; -import io.greitan.avion.common.utils.BaseLogger; -import io.greitan.avion.paper.GeyserVoice; +import team.avion.common.utils.BaseLogger; +import team.avion.paper.GeyserVoice; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; diff --git a/src/main/java/io/greitan/avion/paper/utils/Placeholder.java b/modules/paper/src/main/java/team/avion/paper/utils/Placeholder.java similarity index 84% rename from src/main/java/io/greitan/avion/paper/utils/Placeholder.java rename to modules/paper/src/main/java/team/avion/paper/utils/Placeholder.java index 26badaf..8c7dd35 100644 --- a/src/main/java/io/greitan/avion/paper/utils/Placeholder.java +++ b/modules/paper/src/main/java/team/avion/paper/utils/Placeholder.java @@ -1,9 +1,9 @@ -package io.greitan.avion.paper.utils; +package team.avion.paper.utils; import org.bukkit.entity.Player; -import io.greitan.avion.common.utils.BasePlaceholder; -import io.greitan.avion.paper.GeyserVoice; +import team.avion.common.utils.BasePlaceholder; +import team.avion.paper.GeyserVoice; public class Placeholder extends BasePlaceholder { private final GeyserVoice plugin; diff --git a/modules/paper/src/main/java/team/avion/paper/voicecraft/PaperVoiceCraftSessionManager.java b/modules/paper/src/main/java/team/avion/paper/voicecraft/PaperVoiceCraftSessionManager.java new file mode 100644 index 0000000..5178082 --- /dev/null +++ b/modules/paper/src/main/java/team/avion/paper/voicecraft/PaperVoiceCraftSessionManager.java @@ -0,0 +1,540 @@ +package team.avion.paper.voicecraft; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.bukkit.entity.Player; + +import team.avion.paper.GeyserVoice; +import team.avion.proxy.ProxyPlayerSnapshot; +import team.avion.protocol.McApiTcpClient; +import team.avion.protocol.McPackets.AcceptResponsePacket; +import team.avion.protocol.McPackets.ClearEffectsRequestPacket; +import team.avion.protocol.McPackets.DenyResponsePacket; +import team.avion.protocol.McPackets.McApiPacket; +import team.avion.protocol.McPackets.OnEntityDestroyedPacket; +import team.avion.protocol.McPackets.OnNetworkEntityCreatedPacket; +import team.avion.protocol.McPackets.PingRequestPacket; +import team.avion.protocol.McPackets.ProximityEffect; +import team.avion.protocol.McPackets.SetEntityDeafenRequestPacket; +import team.avion.protocol.McPackets.SetEffectRequestPacket; +import team.avion.protocol.McPackets.SetEntityCaveFactorRequestPacket; +import team.avion.protocol.McPackets.SetEntityDescriptionRequestPacket; +import team.avion.protocol.McPackets.SetEntityEffectBitmaskRequestPacket; +import team.avion.protocol.McPackets.SetEntityMuffleFactorRequestPacket; +import team.avion.protocol.McPackets.SetEntityMuteRequestPacket; +import team.avion.protocol.McPackets.SetEntityNameRequestPacket; +import team.avion.protocol.McPackets.SetEntityPositionRequestPacket; +import team.avion.protocol.McPackets.SetEntityRotationRequestPacket; +import team.avion.protocol.McPackets.SetEntityWorldIdRequestPacket; +import team.avion.protocol.McProtocolTypes.Vector2; +import team.avion.protocol.McProtocolTypes.Vector3; + +public final class PaperVoiceCraftSessionManager { + private static final int EFFECT_BITMASK_PROXIMITY = 1; + + private final GeyserVoice plugin; + private final McApiTcpClient client; + private final SecureRandom random = new SecureRandom(); + + private final Map unboundEntitiesByKey = new HashMap<>(); + private final Map unboundEntitiesById = new HashMap<>(); + private final Map boundEntitiesByPlayerName = new HashMap<>(); + + private String host = ""; + private int port; + private String loginKey = ""; + private int proximityDistance = 30; + private boolean proximityToggle = true; + private boolean voiceEffects = true; + + public PaperVoiceCraftSessionManager(GeyserVoice plugin) { + this.plugin = plugin; + this.client = new McApiTcpClient(plugin.Logger); + } + + public synchronized void configure(String host, int port, String loginKey, int proximityDistance, + boolean proximityToggle, boolean voiceEffects) { + this.host = host == null ? "" : host; + this.port = port; + this.loginKey = loginKey == null ? "" : loginKey; + this.proximityDistance = proximityDistance; + this.proximityToggle = proximityToggle; + this.voiceEffects = voiceEffects; + } + + public synchronized boolean connect() { + if (host.isBlank() || loginKey.isBlank() || port <= 0) { + return false; + } + + clearSessionState(); + if (!client.connect(host, port, loginKey)) { + return false; + } + + applyGlobalSettings(); + processPackets(client.exchange(List.of(new PingRequestPacket()))); + return client.isConnected(); + } + + public synchronized void disconnect() { + client.logout(); + clearSessionState(); + } + + public synchronized void close() { + client.close(); + clearSessionState(); + } + + public synchronized boolean isConnected() { + return client.isConnected(); + } + + public synchronized String getSessionToken() { + return client.getSessionToken(); + } + + public synchronized boolean updateSettings(int proximityDistance, boolean proximityToggle, boolean voiceEffects) { + this.proximityDistance = proximityDistance; + this.proximityToggle = proximityToggle; + this.voiceEffects = voiceEffects; + + if (!client.isConnected()) { + return false; + } + + applyGlobalSettings(); + return client.isConnected(); + } + + public synchronized boolean tick(Collection snapshots) { + if (!client.isConnected()) { + return false; + } + + List packets = new ArrayList<>(); + packets.add(new PingRequestPacket()); + + for (ProxyPlayerSnapshot snapshot : snapshots) { + BoundEntity boundEntity = boundEntitiesByPlayerName.get(snapshot.playerName()); + if (boundEntity == null) { + continue; + } + + if (!Objects.equals(boundEntity.worldId(), snapshot.dimensionId())) { + packets.add(stringPacket(new SetEntityWorldIdRequestPacket(), boundEntity.entityId(), + snapshot.dimensionId())); + boundEntity.worldId = snapshot.dimensionId(); + } + + int currentEffectBitmask = getCurrentEffectBitmask(); + if (boundEntity.effectBitmask() != currentEffectBitmask) { + packets.add(intPacket(new SetEntityEffectBitmaskRequestPacket(), boundEntity.entityId(), + currentEffectBitmask)); + boundEntity.effectBitmask = currentEffectBitmask; + } + + packets.add(vector3Packet(new SetEntityPositionRequestPacket(), boundEntity.entityId(), + new Vector3((float) snapshot.x(), (float) snapshot.y(), (float) snapshot.z()))); + packets.add(vector2Packet(new SetEntityRotationRequestPacket(), boundEntity.entityId(), + new Vector2(0.0f, (float) snapshot.rotation()))); + packets.add(floatPacket(new SetEntityCaveFactorRequestPacket(), boundEntity.entityId(), + voiceEffects ? (float) snapshot.echoFactor() : 0.0f)); + packets.add(floatPacket(new SetEntityMuffleFactorRequestPacket(), boundEntity.entityId(), + voiceEffects && snapshot.muffled() ? 1.0f : 0.0f)); + } + + processPackets(client.exchange(packets)); + return client.isConnected(); + } + + public synchronized boolean bindPlayer(int bindingKey, Player player) { + if (!client.isConnected()) { + return false; + } + if (plugin.getPlayerBinds().getOrDefault(player.getName(), false)) { + return true; + } + + PendingEntity pendingEntity = unboundEntitiesByKey.get(bindingKey); + if (pendingEntity == null) { + processPackets(client.exchange(List.of(new PingRequestPacket()))); + pendingEntity = unboundEntitiesByKey.get(bindingKey); + } + if (pendingEntity == null) { + return false; + } + + return bindEntityToPlayer(pendingEntity, player); + } + + public synchronized boolean bindFakePlayer(int bindingKey, String name) { + if (!client.isConnected()) { + return false; + } + if (plugin.getPlayerBinds().getOrDefault(name, false)) { + return true; + } + + PendingEntity pendingEntity = unboundEntitiesByKey.get(bindingKey); + if (pendingEntity == null) { + processPackets(client.exchange(List.of(new PingRequestPacket()))); + pendingEntity = unboundEntitiesByKey.get(bindingKey); + } + if (pendingEntity == null) { + return false; + } + + return bindEntityToFakePlayer(pendingEntity, name); + } + + public synchronized boolean unbindPlayer(Player player) { + BoundEntity boundEntity = boundEntitiesByPlayerName.remove(player.getName()); + if (boundEntity == null) { + plugin.getPlayerBinds().remove(player.getName()); + return false; + } + + PendingEntity pendingEntity = new PendingEntity(boundEntity.entityId(), boundEntity.userGuid(), + boundEntity.serverUserGuid(), boundEntity.locale()); + assignBindingKey(pendingEntity); + plugin.getPlayerBinds().remove(player.getName()); + return true; + } + + private void applyGlobalSettings() { + List packets = new ArrayList<>(); + packets.add(new ClearEffectsRequestPacket()); + + if (proximityToggle) { + ProximityEffect effect = new ProximityEffect(); + effect.MinRange = 0.0f; + effect.MaxRange = proximityDistance; + effect.WetDry = 1.0f; + + SetEffectRequestPacket packet = new SetEffectRequestPacket(); + packet.Bitmask = EFFECT_BITMASK_PROXIMITY; + packet.Effect = effect; + packets.add(packet); + } + + for (BoundEntity boundEntity : boundEntitiesByPlayerName.values()) { + packets.add(intPacket(new SetEntityEffectBitmaskRequestPacket(), boundEntity.entityId(), getCurrentEffectBitmask())); + boundEntity.effectBitmask = getCurrentEffectBitmask(); + } + + processPackets(client.exchange(packets)); + } + + private void processPackets(List packets) { + for (McApiPacket packet : packets) { + if (packet instanceof AcceptResponsePacket || packet instanceof DenyResponsePacket) { + continue; + } + if (packet instanceof OnNetworkEntityCreatedPacket createdPacket) { + handleNetworkEntityCreated(createdPacket); + continue; + } + if (packet instanceof OnEntityDestroyedPacket destroyedPacket) { + handleEntityDestroyed(destroyedPacket.Id); + } + } + } + + private void handleNetworkEntityCreated(OnNetworkEntityCreatedPacket packet) { + PendingEntity pendingEntity = new PendingEntity(packet.Id, packet.UserGuid.toString(), + packet.ServerUserGuid.toString(), packet.Locale); + + assignBindingKey(pendingEntity); + } + + private void handleEntityDestroyed(int entityId) { + PendingEntity pendingEntity = unboundEntitiesById.remove(entityId); + if (pendingEntity != null) { + unboundEntitiesByKey.remove(pendingEntity.bindingKey()); + } + + BoundEntity removed = null; + for (BoundEntity boundEntity : boundEntitiesByPlayerName.values()) { + if (boundEntity.entityId() == entityId) { + removed = boundEntity; + break; + } + } + + if (removed != null) { + boundEntitiesByPlayerName.remove(removed.playerName()); + plugin.getPlayerBinds().remove(removed.playerName()); + } + } + + private boolean bindEntityToPlayer(PendingEntity pendingEntity, Player player) { + if (plugin.getPlayerBinds().getOrDefault(player.getName(), false)) { + return true; + } + + unboundEntitiesByKey.remove(pendingEntity.bindingKey()); + unboundEntitiesById.remove(pendingEntity.entityId()); + + List packets = new ArrayList<>(); + packets.add(stringPacket(new SetEntityNameRequestPacket(), pendingEntity.entityId(), player.getName())); + packets.add(stringPacket(new SetEntityDescriptionRequestPacket(), pendingEntity.entityId(), + "Bound to player " + player.getName())); + packets.add(stringPacket(new SetEntityWorldIdRequestPacket(), pendingEntity.entityId(), getDimensionId(player))); + packets.add(intPacket(new SetEntityEffectBitmaskRequestPacket(), pendingEntity.entityId(), getCurrentEffectBitmask())); + packets.add(vector3Packet(new SetEntityPositionRequestPacket(), pendingEntity.entityId(), + new Vector3((float) player.getLocation().getX(), (float) player.getLocation().getY(), + (float) player.getLocation().getZ()))); + packets.add(vector2Packet(new SetEntityRotationRequestPacket(), pendingEntity.entityId(), + new Vector2(player.getLocation().getPitch(), player.getLocation().getYaw()))); + packets.add(floatPacket(new SetEntityCaveFactorRequestPacket(), pendingEntity.entityId(), + voiceEffects ? (float) plugin.getPositionsTask().getCaveDensity(player) : 0.0f)); + packets.add(floatPacket(new SetEntityMuffleFactorRequestPacket(), pendingEntity.entityId(), + voiceEffects && player.isInWater() ? 1.0f : 0.0f)); + + processPackets(client.exchange(packets)); + if (!client.isConnected()) { + return false; + } + + boundEntitiesByPlayerName.put(player.getName(), new BoundEntity(player.getName(), pendingEntity.entityId(), + pendingEntity.userGuid(), pendingEntity.serverUserGuid(), pendingEntity.locale(), getDimensionId(player), + getCurrentEffectBitmask())); + plugin.getPlayerBinds().put(player.getName(), true); + return true; + } + + private boolean bindEntityToFakePlayer(PendingEntity pendingEntity, String name) { + unboundEntitiesByKey.remove(pendingEntity.bindingKey()); + unboundEntitiesById.remove(pendingEntity.entityId()); + + List packets = new ArrayList<>(); + packets.add(stringPacket(new SetEntityNameRequestPacket(), pendingEntity.entityId(), name)); + packets.add(stringPacket(new SetEntityDescriptionRequestPacket(), pendingEntity.entityId(), + "Bound to fake player " + name)); + packets.add(stringPacket(new SetEntityWorldIdRequestPacket(), pendingEntity.entityId(), "")); + packets.add(boolPacket(new SetEntityMuteRequestPacket(), pendingEntity.entityId(), false)); + packets.add(boolPacket(new SetEntityDeafenRequestPacket(), pendingEntity.entityId(), false)); + packets.add(intPacket(new SetEntityEffectBitmaskRequestPacket(), pendingEntity.entityId(), getCurrentEffectBitmask())); + packets.add(vector3Packet(new SetEntityPositionRequestPacket(), pendingEntity.entityId(), + new Vector3(0.0f, 0.0f, 0.0f))); + packets.add(vector2Packet(new SetEntityRotationRequestPacket(), pendingEntity.entityId(), + new Vector2(0.0f, 0.0f))); + packets.add(floatPacket(new SetEntityCaveFactorRequestPacket(), pendingEntity.entityId(), 0.0f)); + packets.add(floatPacket(new SetEntityMuffleFactorRequestPacket(), pendingEntity.entityId(), 0.0f)); + + processPackets(client.exchange(packets)); + if (!client.isConnected()) { + return false; + } + + boundEntitiesByPlayerName.put(name, new BoundEntity(name, pendingEntity.entityId(), + pendingEntity.userGuid(), pendingEntity.serverUserGuid(), pendingEntity.locale(), "", + getCurrentEffectBitmask())); + plugin.getPlayerBinds().put(name, true); + return true; + } + + private void assignBindingKey(PendingEntity pendingEntity) { + int bindingKey = nextBindingKey(); + pendingEntity.bindingKey = bindingKey; + unboundEntitiesByKey.put(bindingKey, pendingEntity); + unboundEntitiesById.put(pendingEntity.entityId(), pendingEntity); + + List packets = new ArrayList<>(); + packets.add(stringPacket(new SetEntityNameRequestPacket(), pendingEntity.entityId(), "New Client")); + packets.add(stringPacket(new SetEntityWorldIdRequestPacket(), pendingEntity.entityId(), "")); + packets.add(stringPacket(new SetEntityDescriptionRequestPacket(), pendingEntity.entityId(), + "Welcome! Your binding key is " + bindingKey)); + processPackets(client.exchange(packets)); + } + + private int nextBindingKey() { + int bindingKey = 10000 + random.nextInt(90000); + while (unboundEntitiesByKey.containsKey(bindingKey)) { + bindingKey = 10000 + random.nextInt(90000); + } + return bindingKey; + } + + private int getCurrentEffectBitmask() { + return proximityToggle ? EFFECT_BITMASK_PROXIMITY : 0; + } + + private String getDimensionId(Player player) { + String worldName = player.getWorld().getName(); + return switch (worldName) { + case "world" -> "minecraft:overworld"; + case "world_nether" -> "minecraft:nether"; + case "world_the_end" -> "minecraft:the_end"; + default -> worldName; + }; + } + + private void clearSessionState() { + unboundEntitiesByKey.clear(); + unboundEntitiesById.clear(); + boundEntitiesByPlayerName.clear(); + plugin.getPlayerBinds().clear(); + } + + private static SetEntityNameRequestPacket stringPacket(SetEntityNameRequestPacket packet, int entityId, String value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityDescriptionRequestPacket stringPacket(SetEntityDescriptionRequestPacket packet, int entityId, + String value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityWorldIdRequestPacket stringPacket(SetEntityWorldIdRequestPacket packet, int entityId, + String value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityEffectBitmaskRequestPacket intPacket(SetEntityEffectBitmaskRequestPacket packet, int entityId, + int value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityMuteRequestPacket boolPacket(SetEntityMuteRequestPacket packet, int entityId, + boolean value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityDeafenRequestPacket boolPacket(SetEntityDeafenRequestPacket packet, int entityId, + boolean value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityPositionRequestPacket vector3Packet(SetEntityPositionRequestPacket packet, int entityId, + Vector3 value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityRotationRequestPacket vector2Packet(SetEntityRotationRequestPacket packet, int entityId, + Vector2 value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityCaveFactorRequestPacket floatPacket(SetEntityCaveFactorRequestPacket packet, int entityId, + float value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static SetEntityMuffleFactorRequestPacket floatPacket(SetEntityMuffleFactorRequestPacket packet, int entityId, + float value) { + packet.Id = entityId; + packet.Value = value; + return packet; + } + + private static final class PendingEntity { + private final int entityId; + private final String userGuid; + private final String serverUserGuid; + private final String locale; + private int bindingKey; + + private PendingEntity(int entityId, String userGuid, String serverUserGuid, String locale) { + this.entityId = entityId; + this.userGuid = userGuid; + this.serverUserGuid = serverUserGuid; + this.locale = locale; + } + + private int entityId() { + return entityId; + } + + private String userGuid() { + return userGuid; + } + + private String serverUserGuid() { + return serverUserGuid; + } + + private String locale() { + return locale; + } + + private int bindingKey() { + return bindingKey; + } + } + + private static final class BoundEntity { + private final String playerName; + private final int entityId; + private final String userGuid; + private final String serverUserGuid; + private final String locale; + private String worldId; + private int effectBitmask; + + private BoundEntity(String playerName, int entityId, String userGuid, String serverUserGuid, String locale, + String worldId, int effectBitmask) { + this.playerName = playerName; + this.entityId = entityId; + this.userGuid = userGuid; + this.serverUserGuid = serverUserGuid; + this.locale = locale; + this.worldId = worldId; + this.effectBitmask = effectBitmask; + } + + private String playerName() { + return playerName; + } + + private int entityId() { + return entityId; + } + + private String userGuid() { + return userGuid; + } + + private String serverUserGuid() { + return serverUserGuid; + } + + private String locale() { + return locale; + } + + private String worldId() { + return worldId; + } + + private int effectBitmask() { + return effectBitmask; + } + } +} diff --git a/modules/paper/src/main/java/team/avion/paper/voicecraft/VoiceCraftProcessManager.java b/modules/paper/src/main/java/team/avion/paper/voicecraft/VoiceCraftProcessManager.java new file mode 100644 index 0000000..afe32f5 --- /dev/null +++ b/modules/paper/src/main/java/team/avion/paper/voicecraft/VoiceCraftProcessManager.java @@ -0,0 +1,406 @@ +package team.avion.paper.voicecraft; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import team.avion.paper.GeyserVoice; +import team.avion.protocol.VoiceCraftProtocol; +import team.avion.protocol.VoiceCraftRuntimeDefaults; + +public class VoiceCraftProcessManager { + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); + + private final GeyserVoice plugin; + + private Process process; + + public VoiceCraftProcessManager(GeyserVoice plugin) { + this.plugin = plugin; + } + + public synchronized void ensureRunning() { + if (!plugin.getConfig().getBoolean("config.voicecraft.auto-start", true)) { + return; + } + if (plugin.usesProxy) { + return; + } + + LaunchTarget launchTarget = null; + try { + launchTarget = resolveReleaseLaunchTarget(); + if (launchTarget == null) { + process = null; + return; + } + + ManagedConfigSyncResult initialSync = syncManagedServerProperties(launchTarget.installDirectory()); + + if (process != null && process.isAlive()) { + if (initialSync.changed()) { + restartManagedProcess(launchTarget, "Managed VoiceCraft config changed, restarting process..."); + } + return; + } + if (isEndpointReachable()) { + return; + } + + startManagedProcess(launchTarget); + + if (initialSync.missing()) { + ManagedConfigSyncResult postStartSync = syncManagedServerProperties(launchTarget.installDirectory()); + if (postStartSync.changed()) { + restartManagedProcess(launchTarget, + "Managed VoiceCraft config was generated and updated, restarting process..."); + } + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + plugin.Logger.error("VoiceCraft startup was interrupted: " + ex.getMessage()); + process = null; + } catch (IOException ex) { + plugin.Logger.error("Failed to start VoiceCraft process: " + ex.getMessage()); + plugin.Logger.error("VoiceCraft executable path: " + (launchTarget == null ? "" : launchTarget.command())); + process = null; + } + } + + public synchronized void shutdown() { + if (process == null) { + return; + } + + if (plugin.getConfig().getBoolean("config.voicecraft.shutdown-on-disable", true)) { + plugin.Logger.info("Stopping managed VoiceCraft process..."); + stopProcess(); + } + + process = null; + } + + private void startManagedProcess(LaunchTarget launchTarget) throws IOException { + List command = new ArrayList<>(); + command.add(launchTarget.command()); + command.addAll(launchTarget.arguments()); + appendRuntimeArguments(command); + + ProcessBuilder builder = new ProcessBuilder(command); + builder.directory(launchTarget.workingDirectory().toFile()); + configureRuntimeEnvironment(builder); + + plugin.Logger.info("Starting managed VoiceCraft process: " + launchTarget.command()); + process = builder.start(); + startLogPump(process.getInputStream(), false); + startLogPump(process.getErrorStream(), true); + waitUntilReachable(); + } + + private void restartManagedProcess(LaunchTarget launchTarget, String reason) throws IOException { + plugin.Logger.info(reason); + stopProcess(); + process = null; + startManagedProcess(launchTarget); + } + + private void stopProcess() { + if (process == null) { + return; + } + + process.destroy(); + try { + if (!process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS)) { + process.destroyForcibly(); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + process.destroyForcibly(); + } + } + + private Thread startLogPump(InputStream stream, boolean errorStream) { + Thread thread = Thread.ofVirtual().name(errorStream ? "voicecraft-stderr" : "voicecraft-stdout").start(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.isBlank()) { + continue; + } + if (errorStream) { + plugin.Logger.warn("[VoiceCraft] " + line); + } else { + plugin.Logger.info("[VoiceCraft] " + line); + } + } + } catch (IOException ignored) { + } + }); + return thread; + } + + private void appendRuntimeArguments(List command) { + command.add("--transport-mode"); + command.add("tcp"); + command.add("--transport-host"); + command.add(plugin.getHost()); + command.add("--transport-port"); + command.add(Integer.toString(plugin.getPort())); + command.add("--server-key"); + command.add(plugin.getLoginToken()); + } + + private void configureRuntimeEnvironment(ProcessBuilder builder) { + if (!plugin.getConfig().getBoolean("config.voicecraft.invariant-globalization", true)) { + return; + } + + builder.environment().put("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1"); + } + + private void waitUntilReachable() { + long timeoutMs = plugin.getConfig().getLong("config.voicecraft.ready-timeout-ms", 20000L); + Instant deadline = Instant.now().plus(Duration.ofMillis(timeoutMs)); + while (Instant.now().isBefore(deadline)) { + if (process == null || !process.isAlive()) { + plugin.Logger.error("Managed VoiceCraft process exited before it became reachable."); + process = null; + return; + } + + if (isEndpointReachable()) { + plugin.Logger.info("Managed VoiceCraft process is reachable."); + return; + } + + try { + Thread.sleep(250L); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return; + } + } + + plugin.Logger.warn("VoiceCraft process started, but the transport endpoint is not reachable yet."); + } + + private boolean isEndpointReachable() { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(plugin.getHost(), plugin.getPort()), 500); + return true; + } catch (IOException ignored) { + return false; + } + } + + private LaunchTarget resolveReleaseLaunchTarget() throws IOException, InterruptedException { + Path installDirectory = resolveInstallDirectory(); + Files.createDirectories(installDirectory); + + String platform = defaultPlatform(); + String architecture = defaultArchitecture(); + String assetName = resolveAssetName(platform, architecture); + String releaseId = VoiceCraftRuntimeDefaults.RELEASE + "|" + VoiceCraftProtocol.VERSION_STRING + "|" + assetName; + Path releaseMarker = installDirectory.resolve(".geyservoice-release"); + Path executablePath = resolveManagedExecutable(installDirectory, platform); + + if (!Files.exists(executablePath) || !releaseId.equals(readReleaseMarker(releaseMarker))) { + plugin.Logger.info("Preparing VoiceCraft runtime " + VoiceCraftRuntimeDefaults.RELEASE + + " for McApi protocol " + VoiceCraftProtocol.VERSION_STRING + + " (" + platform + "/" + architecture + ")."); + downloadAndExtractRelease(installDirectory, assetName); + writeReleaseMarker(releaseMarker, releaseId); + } + + if (!Files.exists(executablePath)) { + plugin.Logger.error("Managed VoiceCraft executable was not found after extraction: " + executablePath); + return null; + } + + executablePath.toFile().setExecutable(true); + Path absoluteExecutablePath = executablePath.toAbsolutePath().normalize(); + return new LaunchTarget(installDirectory, absoluteExecutablePath.getParent(), absoluteExecutablePath.toString(), + List.of()); + } + + private Path resolveInstallDirectory() { + String installDirectory = plugin.getConfig().getString("config.voicecraft.install-directory", + VoiceCraftRuntimeDefaults.INSTALL_DIRECTORY); + if (installDirectory == null || installDirectory.isBlank()) { + installDirectory = VoiceCraftRuntimeDefaults.INSTALL_DIRECTORY; + } + + Path path = Path.of(installDirectory); + if (path.isAbsolute()) { + return path; + } + return plugin.getDataFolder().toPath().resolve(path).toAbsolutePath().normalize(); + } + + private String resolveAssetName(String platform, String architecture) { + return VoiceCraftRuntimeDefaults.EXECUTABLE_BASE_NAME + "." + capitalize(platform) + "." + architecture + ".zip"; + } + + private Path resolveManagedExecutable(Path installDirectory, String platform) { + String fileName = "windows".equalsIgnoreCase(platform) + ? VoiceCraftRuntimeDefaults.EXECUTABLE_BASE_NAME + ".exe" + : VoiceCraftRuntimeDefaults.EXECUTABLE_BASE_NAME; + return installDirectory.resolve(fileName).normalize(); + } + + private ManagedConfigSyncResult syncManagedServerProperties(Path installDirectory) throws IOException { + Path serverPropertiesPath = installDirectory.resolve("config").resolve("ServerProperties.json").normalize(); + if (!Files.exists(serverPropertiesPath)) { + plugin.Logger.info("Managed VoiceCraft config file is not present yet: " + serverPropertiesPath); + return new ManagedConfigSyncResult(serverPropertiesPath, false, true); + } + + String content = Files.readString(serverPropertiesPath, StandardCharsets.UTF_8); + String updated = content.replaceFirst("\"Port\"\\s*:\\s*\\d+", "\"Port\": " + plugin.getManagedVoicePort()); + if (!updated.equals(content)) { + Files.writeString(serverPropertiesPath, updated, StandardCharsets.UTF_8, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + return new ManagedConfigSyncResult(serverPropertiesPath, true, false); + } + return new ManagedConfigSyncResult(serverPropertiesPath, false, false); + } + + Path getManagedServerPropertiesPath() { + return resolveInstallDirectory().resolve("config").resolve("ServerProperties.json").normalize(); + } + + private void downloadAndExtractRelease(Path installDirectory, String assetName) throws IOException, InterruptedException { + String downloadUrl = buildReleaseUrl(VoiceCraftRuntimeDefaults.GITHUB_REPOSITORY, + VoiceCraftRuntimeDefaults.RELEASE, assetName); + + plugin.Logger.info("Downloading VoiceCraft release asset " + assetName + "..."); + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(downloadUrl)).GET().build(); + HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() != 200) { + throw new IOException("Unexpected HTTP status " + response.statusCode() + " while downloading " + downloadUrl); + } + + deleteDirectoryContents(installDirectory); + try (InputStream body = response.body(); ZipInputStream zipInputStream = new ZipInputStream(body)) { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + Path target = installDirectory.resolve(entry.getName()).normalize(); + if (!target.startsWith(installDirectory)) { + throw new IOException("Refusing to extract outside of install directory: " + entry.getName()); + } + + if (entry.isDirectory()) { + Files.createDirectories(target); + continue; + } + + Files.createDirectories(target.getParent()); + try (OutputStream outputStream = Files.newOutputStream(target, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + zipInputStream.transferTo(outputStream); + } + } + } + } + + private static String buildReleaseUrl(String repo, String release, String assetName) { + String releasePath = "latest".equalsIgnoreCase(release) ? "latest/download/" : "download/" + release + "/"; + return "https://github.com/" + repo + "/releases/" + releasePath + assetName; + } + + private static void deleteDirectoryContents(Path directory) throws IOException { + if (!Files.exists(directory)) { + return; + } + + try (var stream = Files.walk(directory)) { + stream.sorted((left, right) -> right.getNameCount() - left.getNameCount()) + .filter(path -> !path.equals(directory)) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + } catch (RuntimeException ex) { + if (ex.getCause() instanceof IOException ioException) { + throw ioException; + } + throw ex; + } + } + + private static String readReleaseMarker(Path releaseMarker) throws IOException { + if (!Files.exists(releaseMarker)) { + return ""; + } + return Files.readString(releaseMarker, StandardCharsets.UTF_8).trim(); + } + + private static void writeReleaseMarker(Path releaseMarker, String releaseId) throws IOException { + try (BufferedWriter writer = Files.newBufferedWriter(releaseMarker, StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + writer.write(releaseId); + } + } + + private static String defaultPlatform() { + String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + if (os.contains("win")) { + return "windows"; + } + if (os.contains("mac")) { + return "macos"; + } + return "linux"; + } + + private static String defaultArchitecture() { + String arch = System.getProperty("os.arch", "").toLowerCase(Locale.ROOT); + if (arch.contains("aarch64") || arch.contains("arm64")) { + return "arm64"; + } + if (arch.contains("arm")) { + return "arm"; + } + if (arch.contains("86") && !arch.contains("64")) { + return "x86"; + } + return "x64"; + } + + private static String capitalize(String value) { + if (value.isEmpty()) { + return value; + } + return Character.toUpperCase(value.charAt(0)) + value.substring(1).toLowerCase(Locale.ROOT); + } + + private record LaunchTarget(Path installDirectory, Path workingDirectory, String command, List arguments) { + } + + private record ManagedConfigSyncResult(Path path, boolean changed, boolean missing) { + } +} diff --git a/modules/paper/src/main/resources/config.yml b/modules/paper/src/main/resources/config.yml new file mode 100644 index 0000000..b7a7db7 --- /dev/null +++ b/modules/paper/src/main/resources/config.yml @@ -0,0 +1,34 @@ +config: + debug: false + lang: "system" + auto-reconnect: true + + proxy: + enabled: false + + voicecraft: + transport: + host: "127.0.0.1" + port: 9050 + login-token: "__GENERATED_LOGIN_TOKEN__" + voice: + port: 1111 + auto-start: true + shutdown-on-disable: true + invariant-globalization: true + ready-timeout-ms: 20000 + install-directory: "voicecraft-runtime" + + voice: + proximity-distance: 30 + proximity-toggle: true + voice-effects: true + + not-in-voice-symbol: "\u2727" + in-voice-symbol: "\u2726" + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 diff --git a/modules/paper/src/main/resources/config/en.yml b/modules/paper/src/main/resources/config/en.yml new file mode 100644 index 0000000..90793c5 --- /dev/null +++ b/modules/paper/src/main/resources/config/en.yml @@ -0,0 +1,34 @@ +config: + debug: false # Enable additional debug logging + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # Reconnect to VoiceCraft after a dropped connection + + proxy: + enabled: false # Enable when this Paper server is behind Velocity/Bungee + + voicecraft: + transport: + host: "127.0.0.1" # VoiceCraft TCP host + port: 9050 # VoiceCraft TCP port + login-token: "__GENERATED_LOGIN_TOKEN__" # VoiceCraft LoginToken + voice: + port: 1111 # Managed VoiceCraft voice port + auto-start: true # Start managed VoiceCraft automatically + shutdown-on-disable: true # Stop managed VoiceCraft when the plugin disables + invariant-globalization: true # Run .NET without system ICU packages + ready-timeout-ms: 20000 # How long to wait for VoiceCraft startup + install-directory: "voicecraft-runtime" # Folder for downloaded VoiceCraft binaries + + voice: + proximity-distance: 30 # Maximum voice distance + proximity-toggle: true # Enable proximity processing + voice-effects: true # Enable cave/water voice effects + + not-in-voice-symbol: "\u2727" # Placeholder symbol when a player is not linked + in-voice-symbol: "\u2726" # Placeholder symbol when a player is linked + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # Position update interval in ticks diff --git a/modules/paper/src/main/resources/config/ja.yml b/modules/paper/src/main/resources/config/ja.yml new file mode 100644 index 0000000..32e7997 --- /dev/null +++ b/modules/paper/src/main/resources/config/ja.yml @@ -0,0 +1,34 @@ +config: + debug: false # 追加のデバッグログを有効化します + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # 切断時に VoiceCraft へ再接続します + + proxy: + enabled: false # この Paper サーバーが Velocity/Bungee 配下なら有効化します + + voicecraft: + transport: + host: "127.0.0.1" # VoiceCraft の TCP ホスト + port: 9050 # VoiceCraft の TCP ポート + login-token: "__GENERATED_LOGIN_TOKEN__" # VoiceCraft の LoginToken + voice: + port: 1111 # Managed VoiceCraft voice port + auto-start: true # 管理対象 VoiceCraft を自動起動します + shutdown-on-disable: true # プラグイン停止時に VoiceCraft を終了します + invariant-globalization: true # システム ICU なしで .NET を実行します + ready-timeout-ms: 20000 # VoiceCraft 起動待ち時間 + install-directory: "voicecraft-runtime" # VoiceCraft バイナリの保存先 + + voice: + proximity-distance: 30 # ボイスの最大距離 + proximity-toggle: true # 距離ベース処理を有効化します + voice-effects: true # 洞窟/水中エフェクトを有効化します + + not-in-voice-symbol: "\u2727" # 未リンク時のプレースホルダー記号 + in-voice-symbol: "\u2726" # リンク済み時のプレースホルダー記号 + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # 位置更新間隔(tick) diff --git a/modules/paper/src/main/resources/config/nl.yml b/modules/paper/src/main/resources/config/nl.yml new file mode 100644 index 0000000..9bc99b7 --- /dev/null +++ b/modules/paper/src/main/resources/config/nl.yml @@ -0,0 +1,34 @@ +config: + debug: false # Extra debuglogging inschakelen + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # Opnieuw verbinden met VoiceCraft na verbindingsverlies + + proxy: + enabled: false # Inschakelen als deze Paper-server achter Velocity/Bungee staat + + voicecraft: + transport: + host: "127.0.0.1" # VoiceCraft TCP-host + port: 9050 # VoiceCraft TCP-poort + login-token: "__GENERATED_LOGIN_TOKEN__" # VoiceCraft LoginToken + voice: + port: 1111 # Managed VoiceCraft voice port + auto-start: true # Beheerde VoiceCraft automatisch starten + shutdown-on-disable: true # VoiceCraft stoppen als de plugin uitschakelt + invariant-globalization: true # .NET draaien zonder systeem-ICU-pakketten + ready-timeout-ms: 20000 # Hoe lang op het opstarten van VoiceCraft wachten + install-directory: "voicecraft-runtime" # Map voor gedownloade VoiceCraft binaries + + voice: + proximity-distance: 30 # Maximale spraakafstand + proximity-toggle: true # Proximity-verwerking inschakelen + voice-effects: true # Grot- en watereffecten inschakelen + + not-in-voice-symbol: "\u2727" # Placeholder-symbool als een speler niet gekoppeld is + in-voice-symbol: "\u2726" # Placeholder-symbool als een speler gekoppeld is + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # Positie-update-interval in ticks diff --git a/modules/paper/src/main/resources/config/ru.yml b/modules/paper/src/main/resources/config/ru.yml new file mode 100644 index 0000000..2d8b56c --- /dev/null +++ b/modules/paper/src/main/resources/config/ru.yml @@ -0,0 +1,34 @@ +config: + debug: false # Включить дополнительное логирование + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # Переподключаться к VoiceCraft после потери соединения + + proxy: + enabled: false # Включить, если этот Paper-сервер стоит за Velocity/Bungee + + voicecraft: + transport: + host: "127.0.0.1" # TCP-хост VoiceCraft + port: 9050 # TCP-порт VoiceCraft + login-token: "__GENERATED_LOGIN_TOKEN__" # LoginToken сервера VoiceCraft + voice: + port: 1111 # Порт голосового сервера VoiceCraft + auto-start: true # Автоматически запускать управляемый VoiceCraft + shutdown-on-disable: true # Останавливать VoiceCraft при выключении плагина + invariant-globalization: true # Запускать .NET без системных ICU-библиотек + ready-timeout-ms: 10000 # Сколько ждать запуска VoiceCraft + install-directory: "voicecraft-runtime" # Папка для скачанных бинарников VoiceCraft + + voice: + proximity-distance: 30 # Максимальная дистанция голоса + proximity-toggle: true # Включить обработку proximity + voice-effects: true # Включить эффекты пещеры и воды + + not-in-voice-symbol: "\u2727" # Символ плейсхолдера, когда игрок не привязан + in-voice-symbol: "\u2726" # Символ плейсхолдера, когда игрок привязан + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # Интервал обновления позиций в тиках diff --git a/modules/paper/src/main/resources/locale/en.yml b/modules/paper/src/main/resources/locale/en.yml new file mode 100644 index 0000000..942820d --- /dev/null +++ b/modules/paper/src/main/resources/locale/en.yml @@ -0,0 +1,58 @@ +# en +messages: + commands: + connect: + invalid-args: "Invalid command usage. Usage: /voice connect " + + disconnect: + disconnecting: "Disconnecting from Server..." + already-disconnected: "Already disconnected from server." + invalid-args: "Invalid command usage. Usage: /voice disconnect" + + settings: + nametag: "VoiceCraft Server Settings" + lore: "Open VoiceCraft Settings" + message: "You have been given an item in your inventory. Right Click/Interact with the item to open the settings UI" + + bind: + binding: "Binding..." + binded: "Binding Successful!" + failed: "Binding Failed! Check your key and try again!" + broadcast: "$player has connected to VoiceCraft!" + binding-fake: "Binding fake player..." + invalid-args: "Invalid command usage. Usage: /voice bind " + fake-invalid-args: "Invalid command usage. Usage: /voice bindfake " + + reload: "Plugin reloaded!" + + cmd-invalid-args: "Invalid command usage. Usage: /voice " # Error message for invalid command usage. + + cmd-not-player: "Only players can use this command!" # Error message for command use restricted to players only. + cmd-not-exists: "This command is not exist." # Message indicating that the command does not exist. + + plugin-reload-pl: "Player $player issued plugin reload." # Player-triggered plugin reload message. + plugin-reload: "Player $player issued plugin reload." # Player-triggered plugin reload message. + + plugin-config-loaded: "Config loaded." # Message indicating successful loading of the configuration. + plugin-command-executor: "Commands executors enabled." # Message indicating successful enabling of command executors. + + plugin-connect-connecting: "Connecting/Linking Server..." # Successful connection to the voice chat server. + plugin-connect-connected: "Login Accepted. Server successfully linked!" # Successful connection to the voice chat server. + plugin-connect-failed: "Login Failed! Server not linked! Run /voice to reconnect." # Error message during connection to the voice chat server. + plugin-connect-proxy: "Proxy relay mode enabled. VoiceCraft connection is managed by the proxy." # Proxy based voice chat server connection. + plugin-connect-invalid-data: "Connection failed. Invalid config." # Error message indicating failed connection due to invalid configuration. + + plugin-connection-lost: "Lost connection from voice chat server." # Message when connection is lost. + plugin-connection-lost-reconnect: "Lost connection from voice chat server. Attempting reconnection..." # Message when connection is lost and when reconnecting + plugin-connection-reconnecting-attempt: "Reconnecting to server... Attempt: $attempt" # Message when reconnecting + plugin-connection-reconnecting-success: "Successfully reconnected to voice chat server." # Message when reconnected to the server + plugin-connection-reconnecting-failed-retry: "Connection failed, Retrying..." # Message when reconnecting failed and retrying it + plugin-connection-reconnecting-failed: "Failed to reconnect to voice chat server." # Message when reconnecting failed after 5 tries + plugin-connection-disconnecting: "Disconnecting from Server..." # Message when disconnecting + plugin-connection-already-disconnected: "Already disconnected from server." # Message when already disconnected + plugin-connection-disconnect: "Disconnected from VOIP Server, Reason: $reason" # Message when disconnect + + player-disconnect-success: "Player $player left from the voice chat." # Message indicating player leaving the voice chat. + player-disconnect-failed: "Player $player received an error when leaving the voice chat." # Error message during player leaving the voice chat. + + player-binded: "$player has connected to VoiceCraft." # Message indicating player joining the voice chat. diff --git a/modules/paper/src/main/resources/locale/ja.yml b/modules/paper/src/main/resources/locale/ja.yml new file mode 100644 index 0000000..8f1cc45 --- /dev/null +++ b/modules/paper/src/main/resources/locale/ja.yml @@ -0,0 +1,57 @@ +messages: + commands: + connect: + invalid-args: "無効なコマンドの使用法です。使用法: /voice connect " + + disconnect: + disconnecting: "サーバーから切断しています..." + already-disconnected: "すでにサーバーから切断されています。" + invalid-args: "コマンドの使用法が無効です。使用法: /voice disconnect" + + settings: + nametag: "VoiceCraft サーバー設定" + lore: "VoiceCraft 設定を開く" + message: "インベントリにアイテムが与えられました。アイテムを右クリック/操作して設定 UI を開きます" + + bind: + binding: "バインディング..." + binded: "バインディングに成功しました!" + failed: "バインディングに失敗しました! キーを確認してもう一度お試しください!" + broadcast: "$player が VoiceCraft に接続しまし!" + binding-fake: "偽のプレイヤーをバインディングしています..." + invalid-args: "コマンドの使用法が無効です。使用法: /voice bind " + fake-invalid-args: "コマンドの使用法が無効です。使用法: /voice bindfake " + + reload: "プラグインがリロードされました!" + + cmd-invalid-args: "コマンドの使用法が無効です。使用法: /voice " # 無効なコマンドの使用に関するエラー メッセージ。 + + cmd-not-player: "このコマンドはプレイヤーのみが使用できます!" # コマンドの使用がプレイヤーのみに制限されていることを示すエラー メッセージ。 + cmd-not-exists: "このコマンドは存在しません。" # コマンドが存在しないことを示すメッセージ。 + + plugin-reload-pl: "プレイヤー $player がプラグインの再読み込みを発行しました。" # プレイヤーがトリガーしたプラグインの再読み込みメッセージ。 + plugin-reload: "プレイヤー $player がプラグインの再読み込みを発行しました。" # プレイヤーがトリガーしたプラグインの再読み込みメッセージ。 + + plugin-config-loaded: "構成がロードされました。" # 構成が正常にロードされたことを示すメッセージ。 + plugin-command-executor: "コマンド エグゼキュータが有効になりました。" # コマンド エグゼキュータが正常に有効になったことを示すメッセージ。 + + plugin-connect-connecting: "サーバーを接続/リンクしています..." # ボイスチャット サーバーへの接続に成功しました。 + plugin-connect-connected: "ログインが受け入れられました。サーバーが正常にリンクされました!" # ボイスチャット サーバーへの接続に成功しました。 + plugin-connect-failed: "ログインに失敗しました。サーバーがリンクされていません。再接続するには、/voice を実行してください。" # ボイスチャット サーバーへの接続中にエラー メッセージが表示されます。 + plugin-connect-proxy: "プロキシ中継モードが有効です。VoiceCraft 接続はプロキシ側で管理されます。" # プロキシ ベースのボイスチャット接続。 + plugin-connect-invalid-data: "接続に失敗しました。無効な構成です。" # 無効な構成が原因で接続に失敗したことを示すエラー メッセージ。 + + plugin-connection-lost: "ボイスチャット サーバーからの接続が失われました。" # 接続が失われたときのメッセージ。 + plugin-connection-lost-reconnect: "ボイスチャットサーバーからの接続が失われました。再接続を試行しています..." # 接続が失われたときと再接続するときのメッセージ + plugin-connection-reconnecting-attempt: "サーバーに再接続しています... 試行: $attempt" # 再接続するときのメッセージ + plugin-connection-reconnecting-success: "ボイスチャットサーバーに正常に再接続しました。" # サーバーに再接続するときのメッセージ + plugin-connection-reconnecting-failed-retry: "接続に失敗しました。再試行しています..." # 再接続に失敗し再試行するときのメッセージ + plugin-connection-reconnecting-failed: "ボイスチャットサーバーへの再接続に失敗しました。" # 5 回試行しても再接続が失敗したときのメッセージ + plugin-connection-disconnecting: "サーバーから切断しています..." # 切断するときのメッセージ + plugin-connection-already-disconnected: "すでにサーバーから切断されています。" # すでに切断されている場合のメッセージ + plugin-connection-disconnect: "VOIP サーバーから切断されました。理由: $reason" # 切断されている場合のメッセージ + + player-disconnect-success: "プレイヤー $player がボイス チャットから退出しました。" # プレイヤーがボイス チャットを退出することを示すメッセージ。 + player-disconnect-failed: "プレイヤー $player がボイス チャットを退出するときにエラーを受信しました。" # プレイヤーがボイス チャットを退出するときのエラー メッセージ。 + + player-binded: "$player が VoiceCraft に接続しました。" # プレイヤーがボイス チャットに参加していることを示すメッセージ。 diff --git a/modules/paper/src/main/resources/locale/nl.yml b/modules/paper/src/main/resources/locale/nl.yml new file mode 100644 index 0000000..83bae02 --- /dev/null +++ b/modules/paper/src/main/resources/locale/nl.yml @@ -0,0 +1,57 @@ +# nl +messages: + commands: + connect: + invalid-args: "Ongeldig commando gebruik. Gebruik: /voice connect " + + disconnect: + disconnecting: "Verbinding met Server verbreken..." + already-disconnected: "De verbinding met de server is al verbroken." + invalid-args: "Ongeldig commando gebruik. Gebruik: /voice disconnect" + + settings: + nametag: "VoiceCraft Server Instellingen" + lore: "Open VoiceCraft Instellingen" + message: "Je hebt een item in je inventaris gekregen. Klik met de rechtermuisknop/communiceer met het item om de gebruikersinterface voor instellingen te openen" + + bind: + binding: "Koppelen..." + binded: "Koppelen Succesvol!" + failed: "Koppelen Mislukt! Check je key en probeer opnieuw!" + binding-fake: "Koppelen fake speler..." + invalid-args: "Ongeldig commando gebruik. Gebruik: /voice bind " + fake-invalid-args: "Ongeldig commando gebruik. Gebruik: /voice bindfake " + + reload: "Plugin herladen!" + + cmd-invalid-args: "Ongeldig commando gebruik. Gebruik: /voice " # Error message for invalid command usage. + + cmd-not-player: "Alleen spelers kunnen dit commando gebruiken!" # Error message for command use restricted to players only. + cmd-not-exists: "Dit commando bestaat niet." # Message indicating that the command does not exist. + + plugin-reload-pl: "Speler $player heeft de plugin herladen." # Player-triggered plugin reload message. + plugin-reload: "Speler $player heeft de plugin herladen." # Player-triggered plugin reload message. + + plugin-config-loaded: "Configuratie geladen." # Message indicating successful loading of the configuration. + plugin-command-executor: "Uitvoerders voor commando's ingeschakeld." # Message indicating successful enabling of command executors. + + plugin-connect-connecting: "Verbinden/Linken van Server..." # Successful connection to the voice chat server. + plugin-connect-connected: "Login Geaccepteerd. Server succesvol gelinkt." # Successful connection to the voice chat server. + plugin-connect-failed: "Login Mislukt! Server niet gelinkt! Voer /voice uit om opnieuw verbinding te makne." # Error message during connection to the voice chat server. + plugin-connect-proxy: "Proxy relay-modus is ingeschakeld. De VoiceCraft-verbinding wordt door de proxy beheerd." # Proxy based voice chat server connection. + plugin-connect-invalid-data: "Verbinding mislukt. Ongeldige configuratie." # Error message indicating failed connection due to invalid configuration. + + plugin-connection-lost: "Verbinding met voicechatserver verloren." # Message when connection is lost. + plugin-connection-lost-reconnect: "Verbinding met voicechatserver verloren. Opnieuw verbinding proberen te maken..." # Message when connection is lost and when reconnecting + plugin-connection-reconnecting-attempt: "Opnieuw verbinden met server... Poging: $attempt" # Message when reconnecting + plugin-connection-reconnecting-success: "Met succes herverbonden met voicechatserver." # Message when reconnected to the server + plugin-connection-reconnecting-failed-retry: "Connectie mislukt, Opnieuw proberen..." # Message when reconnecting failed and retrying it + plugin-connection-reconnecting-failed: "Opnieuw verbinden met voicechatserver mislukt." # Message when reconnecting failed after 5 tries + plugin-connection-disconnecting: "Verbinding met Server verbreken..." # Message when disconnecting + plugin-connection-already-disconnected: "Verbinding was al verbroken met server." # Message when already disconnected + plugin-connection-disconnect: "Verbinding met VOIP Server verbroken, Reden: $reason" # Message when disconnect + + player-disconnect-success: "Speler $player heeft de voicechat verlaten." # Message indicating player leaving the voice chat. + player-disconnect-failed: "Speler $player ontving een foutmelding bij het verlaten van de voicechat." # Error message during player leaving the voice chat. + + player-binded: "$player is verbonden met VoiceCraft." # Message indicating player joining the voice chat. diff --git a/modules/paper/src/main/resources/locale/ru.yml b/modules/paper/src/main/resources/locale/ru.yml new file mode 100644 index 0000000..d3c4522 --- /dev/null +++ b/modules/paper/src/main/resources/locale/ru.yml @@ -0,0 +1,58 @@ +# ru +messages: + commands: + connect: + invalid-args: "Неверное использование команды. Использование: /voice connect <хост> <порт> <ключ-сервера>" + + disconnect: + disconnecting: "Отключение от сервера..." + already-disconnected: "Вы уже отключены от сервера." + invalid-args: "Неверное использование команды. Использование: /voice disconnect" + + settings: + nametag: "Настройки сервера VoiceCraft" + lore: "Открыть настройки VoiceCraft" + message: "В ваш инвентарь добавлен предмет. Щелкните ПКМ / взаимодействуйте с предметом, чтобы открыть интерфейс настроек." + + bind: + binding: "Привязка..." + binded: "Привязка успешна!" + failed: "Ошибка привязки! Проверьте ваш ключ и попробуйте снова!" + broadcast: "$player подключился к VoiceCraft!" + binding-fake: "Привязка фейкового игрока..." + invalid-args: "Неверное использование команды. Использование: /voice bind <ключ>" + fake-invalid-args: "Неверное использование команды. Использование: /voice bindfake <ключ> <имя>" + + reload: "Плагин перезагружен!" + + cmd-invalid-args: "Неверное использование команды. Использование: /voice " + + cmd-not-player: "Эту команду могут использовать только игроки!" + cmd-not-exists: "Эта команда не существует." + + plugin-reload-pl: "Игрок $player перезагрузил плагин." + plugin-reload: "Игрок $player перезагрузил плагин." + + plugin-config-loaded: "Конфигурация загружена." + plugin-command-executor: "Исполнители команд активированы." + + plugin-connect-connecting: "Подключение/связь с сервером..." + plugin-connect-connected: "Вход принят. Сервер успешно связан!" + plugin-connect-failed: "Ошибка входа! Сервер не связан! Запустите /voice , чтобы повторить попытку." + plugin-connect-proxy: "Включен режим работы через прокси. Подключением к VoiceCraft управляет прокси-сервер." + plugin-connect-invalid-data: "Ошибка подключения. Некорректная конфигурация." + + plugin-connection-lost: "Соединение с голосовым сервером потеряно." + plugin-connection-lost-reconnect: "Соединение с голосовым сервером потеряно. Попытка переподключения..." + plugin-connection-reconnecting-attempt: "Переподключение к серверу... Попытка: $attempt" + plugin-connection-reconnecting-success: "Успешно переподключились к голосовому серверу." + plugin-connection-reconnecting-failed-retry: "Ошибка подключения, повторная попытка..." + plugin-connection-reconnecting-failed: "Не удалось переподключиться к голосовому серверу." + plugin-connection-disconnecting: "Отключение от сервера..." + plugin-connection-already-disconnected: "Вы уже отключены от сервера." + plugin-connection-disconnect: "Отключено от VOIP-сервера, причина: $reason" + + player-disconnect-success: "Игрок $player покинул голосовой чат." + player-disconnect-failed: "Игрок $player столкнулся с ошибкой при выходе из голосового чата." + + player-binded: "$player подключился к VoiceCraft." diff --git a/src/main/resource-templates/plugin.yml b/modules/paper/src/main/resources/plugin.yml similarity index 52% rename from src/main/resource-templates/plugin.yml rename to modules/paper/src/main/resources/plugin.yml index 2cc583e..1e63f4f 100644 --- a/src/main/resource-templates/plugin.yml +++ b/modules/paper/src/main/resources/plugin.yml @@ -1,15 +1,15 @@ -name: {{ name }} +name: GeyserVoice -version: {{ version }} +version: 0.1.0-SNAPSHOT -main: io.greitan.avion.paper.GeyserVoice -description: {{ description }} +main: team.avion.paper.GeyserVoice +description: Plugin that adds support for using VoiceCraft on Java servers. -author: Alpha -website: {{ url }} +author: lil-jon-crunk +website: https://geyservoice.avion.team -api-version: "1.20" -prefix: {{ name }} +api-version: "1.21.11" +prefix: GeyserVoice softdepend: - PlaceholderAPI @@ -17,7 +17,7 @@ softdepend: commands: voice: description: Voice chat main command. - usage: /voice + usage: /voice permission: voice.cmd permissions: @@ -29,8 +29,6 @@ permissions: default: op voice.disconnect: default: op - voice.settings: - default: op voice.bind: default: true voice.bindfake: diff --git a/modules/velocity/build.gradle b/modules/velocity/build.gradle new file mode 100644 index 0000000..ce36f24 --- /dev/null +++ b/modules/velocity/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'com.gradleup.shadow' version '9.4.1' +} + +description = 'Velocity adapter for GeyserVoice.' + +dependencies { + implementation project(':core') + implementation 'org.spongepowered:configurate-yaml:4.2.0' + compileOnly('com.velocitypowered:velocity-api:3.4.0-SNAPSHOT') + annotationProcessor('com.velocitypowered:velocity-api:3.4.0-SNAPSHOT') + compileOnly('org.projectlombok:lombok:1.18.44') + annotationProcessor('org.projectlombok:lombok:1.18.44') +} + +processResources { + inputs.properties([ + id : 'geyservoice', + name : rootProject.ext.pluginName, + version : project.version, + description: rootProject.description, + author : rootProject.ext.pluginAuthor + ]) + + filesMatching('velocity-plugin.json') { + expand( + id: 'geyservoice', + name: rootProject.ext.pluginName, + version: project.version, + description: rootProject.description, + author: rootProject.ext.pluginAuthor + ) + } +} + +tasks.jar { + archiveBaseName.set("${rootProject.ext.pluginName}-velocity") + archiveClassifier.set('thin') +} + +tasks.shadowJar { + archiveBaseName.set("${rootProject.ext.pluginName}-velocity") + archiveClassifier.set('') +} + +tasks.build { + dependsOn tasks.shadowJar +} diff --git a/modules/velocity/src/main/java/team/avion/velocity/GeyserVoice.java b/modules/velocity/src/main/java/team/avion/velocity/GeyserVoice.java new file mode 100644 index 0000000..2fa577e --- /dev/null +++ b/modules/velocity/src/main/java/team/avion/velocity/GeyserVoice.java @@ -0,0 +1,404 @@ +package team.avion.velocity; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.spongepowered.configurate.serialize.SerializationException; + +import com.google.inject.Inject; +import com.velocitypowered.api.command.CommandManager; +import com.velocitypowered.api.command.CommandMeta; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.scheduler.ScheduledTask; + +import lombok.Getter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import team.avion.common.BaseGeyserVoice; +import team.avion.common.config.ConfigTemplateWriter; +import team.avion.common.localization.PluginLocalization; +import team.avion.proxy.VoiceCraftProxySessionManager; +import team.avion.velocity.commands.VoiceCommand; +import team.avion.velocity.config.VelocityYamlConfig; +import team.avion.velocity.listeners.PlayerJoinHandler; +import team.avion.velocity.listeners.PlayerQuitHandler; +import team.avion.velocity.listeners.PluginMessageHandler; +import team.avion.velocity.tasks.PositionsTask; +import team.avion.velocity.utils.Language; +import team.avion.velocity.utils.VelocityLogger; + +@Plugin(id = "geyservoice", name = "GeyserVoice", version = "0.0.0", description = "VoiceCraft bridge for Velocity", authors = { "0xAlpha" }) +public class GeyserVoice implements BaseGeyserVoice { + private static final String TRANSPORT_HOST_PATH = "config.voicecraft.transport.host"; + private static final String TRANSPORT_PORT_PATH = "config.voicecraft.transport.port"; + private static final String TRANSPORT_LOGIN_TOKEN_PATH = "config.voicecraft.transport.login-token"; + private static final String LEGACY_HOST_PATH = "config.voicecraft.host"; + private static final String LEGACY_PORT_PATH = "config.voicecraft.port"; + private static final String LEGACY_LOGIN_TOKEN_PATH = "config.voicecraft.login-token"; + + private final @Getter ProxyServer proxy; + private final @Getter File dataFolder; + private static VelocityYamlConfig config; + + private static @Getter GeyserVoice instance; + private @Getter boolean isConnected = false; + private @Getter String host = ""; + private @Getter int port = 0; + private @Getter String loginToken = ""; + private final @Getter Map playerBinds = new HashMap<>(); + private @Getter String token = ""; + private String lang; + private final @Getter PluginMessageHandler messageHandler = new PluginMessageHandler(this); + private final @Getter VoiceCraftProxySessionManager sessionManager; + + private ScheduledTask taskRunner; + private PositionsTask positionsTask; + + public final VelocityLogger Logger = new VelocityLogger(); + + @Inject + public GeyserVoice(ProxyServer proxy, @DataDirectory Path dataDirectory) throws IOException { + instance = this; + this.proxy = proxy; + this.dataFolder = dataDirectory.toFile(); + this.sessionManager = new VoiceCraftProxySessionManager(Logger); + + ensureLocalizedConfigExists(); + saveResource("locale/en.yml"); + saveResource("locale/ru.yml"); + saveResource("locale/nl.yml"); + saveResource("locale/ja.yml"); + config = new VelocityYamlConfig(dataDirectory.resolve("config.yml")); + } + + @Subscribe + public void onProxyInitialization(ProxyInitializeEvent event) { + reloadConfig(); + lang = resolveConfiguredLanguage(); + Language.init(this); + + proxy.getEventManager().register(this, messageHandler); + proxy.getChannelRegistrar().register(PluginMessageHandler.CHANNEL); + + CommandManager commandManager = proxy.getCommandManager(); + CommandMeta commandMeta = commandManager.metaBuilder("voice").aliases("voicecraft").plugin(this).build(); + commandManager.register(commandMeta, new VoiceCommand(this, lang)); + + proxy.getEventManager().register(this, new PlayerJoinHandler(this, lang)); + proxy.getEventManager().register(this, new PlayerQuitHandler(this, lang)); + + reload(); + } + + @Override + public void reload() { + reloadConfig(); + lang = resolveConfiguredLanguage(); + Logger.info(Language.getMessage(lang, "plugin-config-loaded")); + Logger.info(Language.getMessage(lang, "plugin-command-executor")); + + host = getTransportHost(); + port = getTransportPort(); + loginToken = getTransportLoginToken(); + int proximityDistance = getConfig().getInt("config.voice.proximity-distance", 30); + boolean proximityToggle = getConfig().getBoolean("config.voice.proximity-toggle", true); + boolean voiceEffects = getConfig().getBoolean("config.voice.voice-effects", true); + sessionManager.configure(host, port, loginToken, proximityDistance, proximityToggle, voiceEffects); + + if (getConfig().getBoolean("config.auto-reconnect", true)) { + isConnected = reconnect(true); + } + + int positionTaskInterval = getConfig().getInt("config.voice.position-update-interval-ticks", 1); + if (taskRunner != null) { + taskRunner.cancel(); + } + positionsTask = new PositionsTask(this, lang); + taskRunner = proxy.getScheduler().buildTask(this, () -> { + if (!positionsTask.run()) { + taskRunner.cancel(); + } + }).repeat(positionTaskInterval * 50L, TimeUnit.MILLISECONDS).schedule(); + + updateSettings(proximityDistance, proximityToggle, voiceEffects); + } + + @Override + public Boolean connect(String host, int port, String loginToken) { + if (host == null || loginToken == null) { + Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); + return false; + } + + try { + getConfig().set(TRANSPORT_HOST_PATH, host); + getConfig().set(TRANSPORT_PORT_PATH, port); + getConfig().set(TRANSPORT_LOGIN_TOKEN_PATH, loginToken); + saveConfig(); + reload(); + return isConnected; + } catch (SerializationException exception) { + Logger.error("Failed to update config: " + exception.getMessage()); + return false; + } + } + + @Override + public Boolean reconnect(Boolean force) { + if (isConnected && !force) { + return true; + } + if (isConnected) { + disconnect("Reconnecting to another server."); + } + + if (Objects.nonNull(host) && Objects.nonNull(loginToken)) { + boolean connected = sessionManager.connect(); + String sessionToken = connected ? sessionManager.getSessionToken() : null; + if (sessionToken != null && !sessionToken.isBlank()) { + Logger.info(Language.getMessage(lang, "plugin-connect-connected")); + isConnected = true; + token = sessionToken; + } else { + Logger.warn(Language.getMessage(lang, "plugin-connect-failed")); + } + return isConnected; + } + + Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); + return false; + } + + @Override + public void disconnect(String reason) { + if (!isConnected) { + return; + } + + if (sessionManager.isConnected()) { + sessionManager.disconnect(); + } + isConnected = false; + token = ""; + playerBinds.clear(); + + String disconnectMessage = Language.getMessage(lang, "plugin-connection-disconnect").replace("$reason", reason); + Logger.info(disconnectMessage); + if (getConfig().getBoolean("config.voice.send-voicecraft-disconnect-message", true)) { + proxy.sendMessage(Component.text(disconnectMessage).color(NamedTextColor.YELLOW)); + } + } + + @Override + public void disconnect() { + disconnect("N.A."); + } + + public Boolean bind(int playerKey, Player player, int tries) { + if (!isConnected) { + return false; + } + if (playerBinds.getOrDefault(player.getUsername(), false)) { + return true; + } + + boolean bound = sessionManager.bindPlayer(playerKey, player.getUniqueId().toString(), player.getUsername()); + if (!bound && tries == 0 && !sessionManager.isConnected()) { + Logger.info("VoiceCraft session dropped during bind, reconnecting..."); + isConnected = reconnect(true); + return bind(playerKey, player, 1); + } + if (!bound) { + messageHandler.sendPlayerBindSync(player); + return false; + } + + playerBinds.put(player.getUsername(), true); + messageHandler.sendPlayerBindSync(player); + Logger.info(Language.getMessage(lang, "player-binded").replace("$player", player.getUsername())); + if (getConfig().getBoolean("config.voice.send-bind-message", true)) { + proxy.sendMessage(Component.text(player.getUsername()).decorate(TextDecoration.BOLD) + .append(Component.text(Language.getMessage(lang, "player-binded").replace("$player", "")).color(NamedTextColor.DARK_GREEN))); + } + return true; + } + + public Boolean bind(int playerKey, Player player) { + return bind(playerKey, player, 0); + } + + @Override + public Boolean bindFake(int playerKey, String name, int tries) { + if (!isConnected) { + return false; + } + + boolean bound = sessionManager.bindFakePlayer(playerKey, "fake:" + playerKey, name); + if (!bound && tries == 0 && !sessionManager.isConnected()) { + isConnected = reconnect(true); + return bindFake(playerKey, name, 1); + } + if (bound) { + playerBinds.put(name, true); + } + return bound; + } + + @Override + public Boolean bindFake(int playerKey, String name) { + return bindFake(playerKey, name, 0); + } + + public Boolean disconnectPlayer(Player player, int tries) { + if (!isConnected) { + return false; + } + + boolean disconnected = sessionManager.unbindPlayer(player.getUniqueId().toString()); + if (!disconnected && tries == 0 && !sessionManager.isConnected()) { + isConnected = reconnect(true); + return disconnectPlayer(player, 1); + } + if (disconnected) { + playerBinds.remove(player.getUsername()); + messageHandler.sendBindSync(player.getUsername(), false); + } + return disconnected; + } + + public Boolean disconnectPlayer(Player player) { + return disconnectPlayer(player, 0); + } + + public boolean handleProxyBindRequest(int bindingKey, String playerId, String playerName) { + boolean bound = sessionManager.bindPlayer(bindingKey, playerId, playerName); + if (bound) { + playerBinds.put(playerName, true); + } + return bound; + } + + public void handleProxyUnbindRequest(String playerId, String playerName) { + sessionManager.unbindPlayer(playerId); + playerBinds.remove(playerName); + } + + @Override + public Boolean updateSettings(int proximityDistance, Boolean proximityToggle, Boolean voiceEffects) { + if (!isConnected) { + return false; + } + return sessionManager.updateSettings(proximityDistance, proximityToggle, voiceEffects); + } + + @Override + public void setNotConnected() { + if (!isConnected) { + return; + } + isConnected = false; + token = ""; + playerBinds.clear(); + sessionManager.close(); + } + + public static VelocityYamlConfig getConfig() { + return config; + } + + @Override + public void saveResource(String resourcePath) { + Path target = dataFolder.toPath().resolve(resourcePath); + if (Files.exists(target)) { + return; + } + try { + Files.createDirectories(target.getParent()); + try (InputStream input = getClass().getClassLoader().getResourceAsStream(resourcePath)) { + if (input == null) { + return; + } + Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException exception) { + Logger.error("Could not save " + resourcePath + ": " + exception.getMessage()); + } + } + + @Override + public void saveConfig() { + try { + config.save(); + } catch (IOException exception) { + Logger.error("Could not save config: " + exception.getMessage()); + } + } + + @Override + public void reloadConfig() { + try { + config.reload(); + } catch (IOException exception) { + Logger.error("Could not reload config: " + exception.getMessage()); + } + } + + private String resolveConfiguredLanguage() { + return PluginLocalization.resolveConfiguredLanguage(getConfig().getString("config.lang", "system")); + } + + private void ensureLocalizedConfigExists() { + Path target = dataFolder.toPath().resolve("config.yml"); + if (Files.exists(target)) { + return; + } + + String language = PluginLocalization.resolveSystemLanguage(); + try { + try (InputStream input = openConfigTemplate(language)) { + if (input == null) { + throw new IOException("Missing embedded config template for language " + language); + } + ConfigTemplateWriter.write(input, target, + Map.of("__GENERATED_LOGIN_TOKEN__", UUID.randomUUID().toString())); + } + } catch (IOException exception) { + Logger.error("Could not create localized config.yml: " + exception.getMessage()); + } + } + + private InputStream openConfigTemplate(String language) { + InputStream input = getClass().getClassLoader().getResourceAsStream("config/" + language + ".yml"); + return input != null ? input : getClass().getClassLoader().getResourceAsStream("config/en.yml"); + } + + private String getTransportHost() { + String value = getConfig().getString(TRANSPORT_HOST_PATH); + return value != null ? value : getConfig().getString(LEGACY_HOST_PATH); + } + + private int getTransportPort() { + int value = getConfig().getInt(TRANSPORT_PORT_PATH, -1); + return value > 0 ? value : getConfig().getInt(LEGACY_PORT_PATH); + } + + private String getTransportLoginToken() { + String value = getConfig().getString(TRANSPORT_LOGIN_TOKEN_PATH); + return value != null ? value : getConfig().getString(LEGACY_LOGIN_TOKEN_PATH); + } +} diff --git a/modules/velocity/src/main/java/team/avion/velocity/commands/VoiceCommand.java b/modules/velocity/src/main/java/team/avion/velocity/commands/VoiceCommand.java new file mode 100644 index 0000000..2a2b536 --- /dev/null +++ b/modules/velocity/src/main/java/team/avion/velocity/commands/VoiceCommand.java @@ -0,0 +1,78 @@ +package team.avion.velocity.commands; + +import java.util.List; + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.command.SimpleCommand; +import com.velocitypowered.api.proxy.Player; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import team.avion.common.commands.BaseVoiceCommand; +import team.avion.common.utils.DoubleStringOperation; +import team.avion.common.utils.IntegerOperation; +import team.avion.common.utils.StringOperation; +import team.avion.velocity.GeyserVoice; +import team.avion.velocity.utils.Language; + +public final class VoiceCommand implements SimpleCommand { + private final BaseVoiceCommand voiceCommand; + private final GeyserVoice plugin; + private final String lang; + + public VoiceCommand(GeyserVoice plugin, String lang) { + this.voiceCommand = new BaseVoiceCommand(plugin); + this.plugin = plugin; + this.lang = lang; + } + + @Override + public void execute(Invocation invocation) { + CommandSource sender = invocation.source(); + String[] args = invocation.arguments(); + + voiceCommand.onCommand(args, plugin.isConnected(), sender instanceof Player, + permission -> !(sender instanceof Player) || sender.hasPermission(permission), + new DoubleStringOperation() { + @Override + public void execute(String text, String rawColor) { + NamedTextColor color = NamedTextColor.RED; + if ("aqua".equals(rawColor)) { + color = NamedTextColor.AQUA; + } else if ("green".equals(rawColor)) { + color = NamedTextColor.GREEN; + } else if ("yellow".equals(rawColor)) { + color = NamedTextColor.YELLOW; + } + + Component message = Component.text(Language.getMessage(lang, text)).color(color); + if (sender instanceof Player) { + sender.sendMessage(message); + } else { + plugin.Logger.log(message); + } + } + }, + new IntegerOperation() { + @Override + public boolean execute(int key) { + return sender instanceof Player player && plugin.bind(key, player); + } + }); + } + + @Override + public boolean hasPermission(Invocation invocation) { + return invocation.source().hasPermission("voice.cmd"); + } + + @Override + public List suggest(Invocation invocation) { + return voiceCommand.onTabComplete(invocation.arguments(), new StringOperation() { + @Override + public boolean execute(String permission) { + return invocation.source().hasPermission(permission); + } + }); + } +} diff --git a/modules/velocity/src/main/java/team/avion/velocity/config/VelocityYamlConfig.java b/modules/velocity/src/main/java/team/avion/velocity/config/VelocityYamlConfig.java new file mode 100644 index 0000000..8916ec9 --- /dev/null +++ b/modules/velocity/src/main/java/team/avion/velocity/config/VelocityYamlConfig.java @@ -0,0 +1,69 @@ +package team.avion.velocity.config; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +public final class VelocityYamlConfig { + private final YamlConfigurationLoader loader; + private CommentedConfigurationNode root; + + public VelocityYamlConfig(Path path) throws IOException { + Files.createDirectories(path.getParent()); + this.loader = YamlConfigurationLoader.builder().path(path).build(); + this.root = loader.load(); + } + + public void reload() throws IOException { + root = loader.load(); + } + + public void save() throws IOException { + loader.save(root); + } + + public String getString(String key) { + return node(key).getString(""); + } + + public String getString(String key, String fallback) { + return node(key).getString(fallback); + } + + public int getInt(String key) { + return node(key).getInt(); + } + + public int getInt(String key, int fallback) { + return node(key).getInt(fallback); + } + + public boolean getBoolean(String key) { + return node(key).getBoolean(); + } + + public boolean getBoolean(String key, boolean fallback) { + return node(key).getBoolean(fallback); + } + + public List getStringList(String key) throws SerializationException { + return node(key).getList(String.class, List.of()); + } + + public void set(String key, Object value) throws SerializationException { + node(key).set(value); + } + + public boolean contains(String key) { + return !node(key).virtual(); + } + + private CommentedConfigurationNode node(String key) { + return root.node((Object[]) key.split("\\.")); + } +} diff --git a/modules/velocity/src/main/java/team/avion/velocity/listeners/PlayerJoinHandler.java b/modules/velocity/src/main/java/team/avion/velocity/listeners/PlayerJoinHandler.java new file mode 100644 index 0000000..7d73a89 --- /dev/null +++ b/modules/velocity/src/main/java/team/avion/velocity/listeners/PlayerJoinHandler.java @@ -0,0 +1,19 @@ +package team.avion.velocity.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; + +import team.avion.velocity.GeyserVoice; + +public class PlayerJoinHandler { + private final GeyserVoice plugin; + + public PlayerJoinHandler(GeyserVoice plugin, String lang) { + this.plugin = plugin; + } + + @Subscribe + public void onPlayerConnect(ServerPostConnectEvent event) { + plugin.getMessageHandler().sendPlayerBindSync(event.getPlayer()); + } +} diff --git a/src/main/java/io/greitan/avion/velocity/listeners/PlayerQuitHandler.java b/modules/velocity/src/main/java/team/avion/velocity/listeners/PlayerQuitHandler.java similarity index 66% rename from src/main/java/io/greitan/avion/velocity/listeners/PlayerQuitHandler.java rename to modules/velocity/src/main/java/team/avion/velocity/listeners/PlayerQuitHandler.java index 80dbfdb..430592b 100644 --- a/src/main/java/io/greitan/avion/velocity/listeners/PlayerQuitHandler.java +++ b/modules/velocity/src/main/java/team/avion/velocity/listeners/PlayerQuitHandler.java @@ -1,15 +1,15 @@ -package io.greitan.avion.velocity.listeners; +package team.avion.velocity.listeners; -import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; -import io.greitan.avion.velocity.GeyserVoice; -import io.greitan.avion.velocity.utils.Language; +import com.velocitypowered.api.proxy.Player; + import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import team.avion.velocity.GeyserVoice; +import team.avion.velocity.utils.Language; public class PlayerQuitHandler { - private final GeyserVoice plugin; private final String lang; @@ -23,6 +23,7 @@ public void onPlayerQuit(DisconnectEvent event) { Player player = event.getPlayer(); boolean isBound = plugin.getPlayerBinds().getOrDefault(player.getUsername(), false); + plugin.getSessionManager().removePlayerSnapshot(player.getUniqueId().toString()); if (plugin.isConnected() && isBound) { handlePlayerDisconnect(player); } @@ -30,24 +31,16 @@ public void onPlayerQuit(DisconnectEvent event) { private void handlePlayerDisconnect(Player player) { boolean isDisconnected = plugin.disconnectPlayer(player); - - plugin.playerDataList.remove(player.getUniqueId().toString()); - - String playerName = player.getUsername(); - String disconnectMessage = Language.getMessage(lang, "player-disconnect-success").replace("$player", - playerName); + String disconnectMessage = Language.getMessage(lang, "player-disconnect-success").replace("$player", player.getUsername()); if (isDisconnected) { plugin.Logger.info(disconnectMessage); - - boolean sendDisconnectMessage = GeyserVoice.getConfig().getBoolean("config.voice.send-disconnect-message"); - if (sendDisconnectMessage) { - // @TODO send to servers + if (GeyserVoice.getConfig().getBoolean("config.voice.send-disconnect-message", true)) { plugin.getProxy().sendMessage(Component.text(disconnectMessage).color(NamedTextColor.YELLOW)); } } else { - plugin.Logger.error(Language.getMessage(lang, "player-disconnect-failed").replace("$player", playerName)); - plugin.getPlayerBinds().remove(playerName); + plugin.Logger.error(Language.getMessage(lang, "player-disconnect-failed").replace("$player", player.getUsername())); + plugin.getPlayerBinds().remove(player.getUsername()); } } } diff --git a/modules/velocity/src/main/java/team/avion/velocity/listeners/PluginMessageHandler.java b/modules/velocity/src/main/java/team/avion/velocity/listeners/PluginMessageHandler.java new file mode 100644 index 0000000..74a9d6f --- /dev/null +++ b/modules/velocity/src/main/java/team/avion/velocity/listeners/PluginMessageHandler.java @@ -0,0 +1,77 @@ +package team.avion.velocity.listeners; + +import java.util.Optional; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import com.velocitypowered.api.proxy.server.RegisteredServer; + +import team.avion.proxy.ProxyMessageCodec; +import team.avion.proxy.ProxyPlayerSnapshot; +import team.avion.velocity.GeyserVoice; + +public class PluginMessageHandler { + public static final MinecraftChannelIdentifier CHANNEL = MinecraftChannelIdentifier.from(ProxyMessageCodec.CHANNEL_NAME); + + private final GeyserVoice plugin; + + public PluginMessageHandler(GeyserVoice plugin) { + this.plugin = plugin; + } + + public void sendPlayerBindSync(Player player) { + sendBindSync(player.getUsername(), plugin.getPlayerBinds().getOrDefault(player.getUsername(), false)); + } + + public void sendBindSync(String playerName, boolean bound) { + byte[] message = ProxyMessageCodec.encodeBindSync(playerName, bound); + for (Player player : plugin.getProxy().getAllPlayers()) { + Optional connection = player.getCurrentServer(); + if (connection.isPresent()) { + try { + connection.get().sendPluginMessage(CHANNEL, message); + } catch (Exception ignored) { + } + } + } + + for (RegisteredServer server : plugin.getProxy().getAllServers()) { + try { + server.sendPluginMessage(CHANNEL, message); + } catch (Exception ignored) { + } + } + } + + @Subscribe + public void onMessageReceived(PluginMessageEvent event) { + if (event.getIdentifier() != CHANNEL) { + return; + } + if (!(event.getSource() instanceof ServerConnection backend)) { + return; + } + + byte[] data = event.getData(); + String type = ProxyMessageCodec.peekType(data); + String serverName = backend.getServerInfo().getName(); + + if (ProxyMessageCodec.SNAPSHOT.equals(type)) { + ProxyPlayerSnapshot snapshot = ProxyMessageCodec.decodeSnapshot(data); + plugin.getSessionManager().updatePlayerSnapshot(snapshot.withDimensionId(serverName + "_" + snapshot.dimensionId())); + } else if (ProxyMessageCodec.BIND_REQUEST.equals(type)) { + ProxyMessageCodec.BindRequest bindRequest = ProxyMessageCodec.decodeBindRequest(data); + boolean bound = plugin.handleProxyBindRequest(bindRequest.bindingKey(), bindRequest.playerId(), bindRequest.playerName()); + sendBindSync(bindRequest.playerName(), bound); + } else if (ProxyMessageCodec.UNBIND_REQUEST.equals(type)) { + ProxyMessageCodec.UnbindRequest unbindRequest = ProxyMessageCodec.decodeUnbindRequest(data); + plugin.handleProxyUnbindRequest(unbindRequest.playerId(), unbindRequest.playerName()); + sendBindSync(unbindRequest.playerName(), false); + } + + event.setResult(PluginMessageEvent.ForwardResult.handled()); + } +} diff --git a/modules/velocity/src/main/java/team/avion/velocity/tasks/PositionsTask.java b/modules/velocity/src/main/java/team/avion/velocity/tasks/PositionsTask.java new file mode 100644 index 0000000..bd9a4d1 --- /dev/null +++ b/modules/velocity/src/main/java/team/avion/velocity/tasks/PositionsTask.java @@ -0,0 +1,78 @@ +package team.avion.velocity.tasks; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import team.avion.velocity.GeyserVoice; +import team.avion.velocity.utils.Language; + +public class PositionsTask { + private final GeyserVoice plugin; + private final String lang; + private int reconnectRetries = 0; + private boolean reconnecting = false; + + public PositionsTask(GeyserVoice plugin, String lang) { + this.plugin = plugin; + this.lang = lang; + } + + public boolean run() { + if (reconnecting) { + return reconnect(); + } + + if (plugin.isConnected() && plugin.getSessionManager().tick()) { + return true; + } + + if (!plugin.isConnected()) { + return true; + } + + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-lost")); + plugin.setNotConnected(); + + if (GeyserVoice.getConfig().getBoolean("config.auto-reconnect", true)) { + if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message", true)) { + plugin.getProxy().sendMessage(Component.text(Language.getMessage(lang, "plugin-connection-lost-reconnect")).color(NamedTextColor.RED)); + } + reconnectRetries = 0; + reconnecting = true; + return reconnect(); + } + + if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message", true)) { + plugin.getProxy().sendMessage(Component.text(Language.getMessage(lang, "plugin-connection-lost")).color(NamedTextColor.RED)); + } + return false; + } + + private boolean reconnect() { + if (reconnectRetries >= 5) { + reconnecting = false; + plugin.Logger.error(Language.getMessage(lang, "plugin-connection-reconnecting-failed")); + if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message", true)) { + plugin.getProxy().sendMessage(Component.text(Language.getMessage(lang, "plugin-connection-reconnecting-failed")).color(NamedTextColor.RED)); + } + return false; + } + + reconnectRetries++; + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-attempt").replace("$attempt", Integer.toString(reconnectRetries))); + + if (plugin.reconnect(true)) { + reconnecting = false; + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-success")); + if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message", true)) { + plugin.getProxy().sendMessage(Component.text(Language.getMessage(lang, "plugin-connection-reconnecting-success")).color(NamedTextColor.GREEN)); + } + return true; + } + + if (reconnectRetries < 5) { + plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-failed-retry")); + return true; + } + return reconnect(); + } +} diff --git a/modules/velocity/src/main/java/team/avion/velocity/utils/Language.java b/modules/velocity/src/main/java/team/avion/velocity/utils/Language.java new file mode 100644 index 0000000..98bbdc0 --- /dev/null +++ b/modules/velocity/src/main/java/team/avion/velocity/utils/Language.java @@ -0,0 +1,51 @@ +package team.avion.velocity.utils; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import team.avion.common.localization.PluginLocalization; +import team.avion.velocity.GeyserVoice; +import team.avion.velocity.config.VelocityYamlConfig; + +public final class Language { + private static final Map languageConfigs = new HashMap<>(); + private static final String DEFAULT_LANGUAGE = PluginLocalization.DEFAULT_LANGUAGE; + + private Language() { + } + + public static void init(GeyserVoice plugin) { + File languageFolder = new File(plugin.getDataFolder(), "locale"); + languageFolder.mkdirs(); + plugin.saveResource("locale/en.yml"); + plugin.saveResource("locale/ru.yml"); + plugin.saveResource("locale/nl.yml"); + plugin.saveResource("locale/ja.yml"); + + loadLanguages(languageFolder); + } + + private static void loadLanguages(File languageFolder) { + languageConfigs.clear(); + File[] files = languageFolder.listFiles((dir, name) -> name.endsWith(".yml")); + if (files == null) { + return; + } + + for (File file : files) { + String language = file.getName().replace(".yml", ""); + try { + languageConfigs.put(language, new VelocityYamlConfig(file.toPath())); + } catch (IOException ignored) { + } + } + } + + public static String getMessage(String language, String key) { + String resolvedLanguage = PluginLocalization.resolveConfiguredLanguage(language); + VelocityYamlConfig config = languageConfigs.getOrDefault(resolvedLanguage, languageConfigs.get(DEFAULT_LANGUAGE)); + return config == null ? key : config.getString("messages." + key, key); + } +} diff --git a/modules/velocity/src/main/java/team/avion/velocity/utils/VelocityLogger.java b/modules/velocity/src/main/java/team/avion/velocity/utils/VelocityLogger.java new file mode 100644 index 0000000..8768d70 --- /dev/null +++ b/modules/velocity/src/main/java/team/avion/velocity/utils/VelocityLogger.java @@ -0,0 +1,48 @@ +package team.avion.velocity.utils; + +import com.velocitypowered.api.proxy.ConsoleCommandSource; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import team.avion.common.utils.BaseLogger; +import team.avion.velocity.GeyserVoice; + +public class VelocityLogger extends BaseLogger { + @Override + public void log(Component msg) { + console().sendMessage(prefix().append(msg)); + } + + @Override + public void info(String msg) { + console().sendMessage(prefix().append(Component.text(msg).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))); + } + + @Override + public void warn(String msg) { + console().sendMessage(prefix().append(Component.text(msg).color(NamedTextColor.YELLOW).decorate(TextDecoration.BOLD))); + } + + @Override + public void error(String msg) { + console().sendMessage(prefix().append(Component.text(msg).color(NamedTextColor.RED).decorate(TextDecoration.BOLD))); + } + + @Override + public void debug(String msg) { + if (GeyserVoice.getConfig().getBoolean("config.debug", false)) { + console().sendMessage(prefix().append(Component.text(msg).color(NamedTextColor.BLUE).decorate(TextDecoration.BOLD))); + } + } + + private ConsoleCommandSource console() { + return GeyserVoice.getInstance().getProxy().getConsoleCommandSource(); + } + + private Component prefix() { + return Component.text("[").color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD) + .append(Component.text("GeyserVoice").color(NamedTextColor.LIGHT_PURPLE).decorate(TextDecoration.BOLD)) + .append(Component.text("] ").color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD)); + } +} diff --git a/modules/velocity/src/main/resources/config.yml b/modules/velocity/src/main/resources/config.yml new file mode 100644 index 0000000..69b64c0 --- /dev/null +++ b/modules/velocity/src/main/resources/config.yml @@ -0,0 +1,23 @@ +config: + debug: false + lang: "system" + auto-reconnect: true + + voicecraft: + transport: + host: "127.0.0.1" + port: 9050 + login-token: "__GENERATED_LOGIN_TOKEN__" + voice: + port: 1111 + + voice: + proximity-distance: 30 + proximity-toggle: true + voice-effects: true + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 diff --git a/modules/velocity/src/main/resources/config/en.yml b/modules/velocity/src/main/resources/config/en.yml new file mode 100644 index 0000000..3767bfd --- /dev/null +++ b/modules/velocity/src/main/resources/config/en.yml @@ -0,0 +1,23 @@ +config: + debug: false # Enable additional debug logging + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # Reconnect to VoiceCraft after a dropped connection + + voicecraft: + transport: + host: "127.0.0.1" # VoiceCraft TCP host + port: 9050 # VoiceCraft TCP port + login-token: "__GENERATED_LOGIN_TOKEN__" # VoiceCraft LoginToken + voice: + port: 1111 # VoiceCraft voice port + + voice: + proximity-distance: 30 # Maximum voice distance + proximity-toggle: true # Enable proximity processing + voice-effects: true # Enable cave/water voice effects + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # Position update interval in ticks diff --git a/modules/velocity/src/main/resources/config/ja.yml b/modules/velocity/src/main/resources/config/ja.yml new file mode 100644 index 0000000..740e1b5 --- /dev/null +++ b/modules/velocity/src/main/resources/config/ja.yml @@ -0,0 +1,23 @@ +config: + debug: false # 追加のデバッグログを有効化します + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # 切断時に VoiceCraft へ再接続します + + voicecraft: + transport: + host: "127.0.0.1" # VoiceCraft の TCP ホスト + port: 9050 # VoiceCraft の TCP ポート + login-token: "__GENERATED_LOGIN_TOKEN__" # VoiceCraft の LoginToken + voice: + port: 1111 # VoiceCraft voice port + + voice: + proximity-distance: 30 # ボイスの最大距離 + proximity-toggle: true # 距離ベース処理を有効化します + voice-effects: true # 洞窟/水中エフェクトを有効化します + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # 位置更新間隔(tick) diff --git a/modules/velocity/src/main/resources/config/nl.yml b/modules/velocity/src/main/resources/config/nl.yml new file mode 100644 index 0000000..db987fe --- /dev/null +++ b/modules/velocity/src/main/resources/config/nl.yml @@ -0,0 +1,23 @@ +config: + debug: false # Extra debuglogging inschakelen + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # Opnieuw verbinden met VoiceCraft na verbindingsverlies + + voicecraft: + transport: + host: "127.0.0.1" # VoiceCraft TCP-host + port: 9050 # VoiceCraft TCP-poort + login-token: "__GENERATED_LOGIN_TOKEN__" # VoiceCraft LoginToken + voice: + port: 1111 # VoiceCraft voice port + + voice: + proximity-distance: 30 # Maximale spraakafstand + proximity-toggle: true # Proximity-verwerking inschakelen + voice-effects: true # Grot- en watereffecten inschakelen + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # Positie-update-interval in ticks diff --git a/modules/velocity/src/main/resources/config/ru.yml b/modules/velocity/src/main/resources/config/ru.yml new file mode 100644 index 0000000..889f579 --- /dev/null +++ b/modules/velocity/src/main/resources/config/ru.yml @@ -0,0 +1,23 @@ +config: + debug: false # Включить дополнительное логирование + lang: "system" # system, en, ru, nl, ja + auto-reconnect: true # Переподключаться к VoiceCraft после потери соединения + + voicecraft: + transport: + host: "127.0.0.1" # TCP-хост VoiceCraft + port: 9050 # TCP-порт VoiceCraft + login-token: "__GENERATED_LOGIN_TOKEN__" # LoginToken сервера VoiceCraft + voice: + port: 1111 # Порт голосового сервера VoiceCraft + + voice: + proximity-distance: 30 # Максимальная дистанция голоса + proximity-toggle: true # Включить обработку proximity + voice-effects: true # Включить эффекты пещеры и воды + + send-bind-message: true # Show a message when a player links to VoiceCraft + send-disconnect-message: true # Show a message when a player leaves voice chat + send-voicecraft-disconnect-message: true # Show a message when the VoiceCraft connection closes + send-connection-lost-message: true # Show a message when the VoiceCraft connection is lost + position-update-interval-ticks: 5 # Интервал обновления позиций в тиках diff --git a/modules/velocity/src/main/resources/locale/en.yml b/modules/velocity/src/main/resources/locale/en.yml new file mode 100644 index 0000000..0d90ac5 --- /dev/null +++ b/modules/velocity/src/main/resources/locale/en.yml @@ -0,0 +1,57 @@ +# en +messages: + commands: + connect: + invalid-args: "Invalid command usage. Usage: /voice connect " + + disconnect: + disconnecting: "Disconnecting from Server..." + already-disconnected: "Already disconnected from server." + invalid-args: "Invalid command usage. Usage: /voice disconnect" + + settings: + nametag: "VoiceCraft Server Settings" + lore: "Open VoiceCraft Settings" + message: "You have been given an item in your inventory. Right Click/Interact with the item to open the settings UI" + + bind: + binding: "Binding..." + binded: "Binding Successful!" + failed: "Binding Failed! Check your key and try again!" + broadcast: "$player has connected to VoiceCraft!" + binding-fake: "Binding fake player..." + invalid-args: "Invalid command usage. Usage: /voice bind " + fake-invalid-args: "Invalid command usage. Usage: /voice bindfake " + + reload: "Plugin reloaded!" + + cmd-invalid-args: "Invalid command usage. Usage: /voice " # Error message for invalid command usage. + + cmd-not-player: "Only players can use this command!" # Error message for command use restricted to players only. + cmd-not-exists: "This command is not exist." # Message indicating that the command does not exist. + + plugin-reload-pl: "Player $player issued plugin reload." # Player-triggered plugin reload message. + plugin-reload: "Player $player issued plugin reload." # Player-triggered plugin reload message. + + plugin-config-loaded: "Config loaded." # Message indicating successful loading of the configuration. + plugin-command-executor: "Commands executors enabled." # Message indicating successful enabling of command executors. + + plugin-connect-connecting: "Connecting/Linking Server..." # Successful connection to the voice chat server. + plugin-connect-connected: "Login Accepted. Server successfully linked!" # Successful connection to the voice chat server. + plugin-connect-failed: "Login Failed! Server not linked! Run /voice to reconnect." # Error message during connection to the voice chat server. + plugin-connect-invalid-data: "Connection failed. Invalid config." # Error message indicating failed connection due to invalid configuration. + + plugin-connection-lost: "Lost connection from voice chat server." # Message when connection is lost. + plugin-connection-lost-reconnect: "Lost connection from voice chat server. Attempting reconnection..." # Message when connection is lost and when reconnecting + plugin-connection-reconnecting-attempt: "Reconnecting to server... Attempt: $attempt" # Message when reconnecting + plugin-connection-reconnecting-success: "Successfully reconnected to voice chat server." # Message when reconnected to the server + plugin-connection-reconnecting-failed-retry: "Connection failed, Retrying..." # Message when reconnecting failed and retrying it + plugin-connection-reconnecting-failed: "Failed to reconnect to voice chat server." # Message when reconnecting failed after 5 tries + plugin-connection-disconnecting: "Disconnecting from Server..." # Message when disconnecting + plugin-connection-already-disconnected: "Already disconnected from server." # Message when already disconnected + plugin-connection-disconnect: "Disconnected from VOIP Server, Reason: $reason" # Message when disconnect + + player-disconnect-success: "Player $player left from the voice chat." # Message indicating player leaving the voice chat. + player-disconnect-failed: "Player $player received an error when leaving the voice chat." # Error message during player leaving the voice chat. + + player-binded: "$player has connected to VoiceCraft." # Message indicating player joining the voice chat. diff --git a/modules/velocity/src/main/resources/locale/ja.yml b/modules/velocity/src/main/resources/locale/ja.yml new file mode 100644 index 0000000..253ea23 --- /dev/null +++ b/modules/velocity/src/main/resources/locale/ja.yml @@ -0,0 +1,56 @@ +messages: + commands: + connect: + invalid-args: "無効なコマンドの使用法です。使用法: /voice connect " + + disconnect: + disconnecting: "サーバーから切断しています..." + already-disconnected: "すでにサーバーから切断されています。" + invalid-args: "コマンドの使用法が無効です。使用法: /voice disconnect" + + settings: + nametag: "VoiceCraft サーバー設定" + lore: "VoiceCraft 設定を開く" + message: "インベントリにアイテムが与えられました。アイテムを右クリック/操作して設定 UI を開きます" + + bind: + binding: "バインディング..." + binded: "バインディングに成功しました!" + failed: "バインディングに失敗しました! キーを確認してもう一度お試しください!" + broadcast: "$player が VoiceCraft に接続しまし!" + binding-fake: "偽のプレイヤーをバインディングしています..." + invalid-args: "コマンドの使用法が無効です。使用法: /voice bind " + fake-invalid-args: "コマンドの使用法が無効です。使用法: /voice bindfake " + + reload: "プラグインがリロードされました!" + + cmd-invalid-args: "コマンドの使用法が無効です。使用法: /voice " # 無効なコマンドの使用に関するエラー メッセージ。 + + cmd-not-player: "このコマンドはプレイヤーのみが使用できます!" # コマンドの使用がプレイヤーのみに制限されていることを示すエラー メッセージ。 + cmd-not-exists: "このコマンドは存在しません。" # コマンドが存在しないことを示すメッセージ。 + + plugin-reload-pl: "プレイヤー $player がプラグインの再読み込みを発行しました。" # プレイヤーがトリガーしたプラグインの再読み込みメッセージ。 + plugin-reload: "プレイヤー $player がプラグインの再読み込みを発行しました。" # プレイヤーがトリガーしたプラグインの再読み込みメッセージ。 + + plugin-config-loaded: "構成がロードされました。" # 構成が正常にロードされたことを示すメッセージ。 + plugin-command-executor: "コマンド エグゼキュータが有効になりました。" # コマンド エグゼキュータが正常に有効になったことを示すメッセージ。 + + plugin-connect-connecting: "サーバーを接続/リンクしています..." # ボイスチャット サーバーへの接続に成功しました。 + plugin-connect-connected: "ログインが受け入れられました。サーバーが正常にリンクされました!" # ボイスチャット サーバーへの接続に成功しました。 + plugin-connect-failed: "ログインに失敗しました。サーバーがリンクされていません。再接続するには、/voice を実行してください。" # ボイスチャット サーバーへの接続中にエラー メッセージが表示されます。 + plugin-connect-invalid-data: "接続に失敗しました。無効な構成です。" # 無効な構成が原因で接続に失敗したことを示すエラー メッセージ。 + + plugin-connection-lost: "ボイスチャット サーバーからの接続が失われました。" # 接続が失われたときのメッセージ。 + plugin-connection-lost-reconnect: "ボイスチャットサーバーからの接続が失われました。再接続を試行しています..." # 接続が失われたときと再接続するときのメッセージ + plugin-connection-reconnecting-attempt: "サーバーに再接続しています... 試行: $attempt" # 再接続するときのメッセージ + plugin-connection-reconnecting-success: "ボイスチャットサーバーに正常に再接続しました。" # サーバーに再接続するときのメッセージ + plugin-connection-reconnecting-failed-retry: "接続に失敗しました。再試行しています..." # 再接続に失敗し再試行するときのメッセージ + plugin-connection-reconnecting-failed: "ボイスチャットサーバーへの再接続に失敗しました。" # 5 回試行しても再接続が失敗したときのメッセージ + plugin-connection-disconnecting: "サーバーから切断しています..." # 切断するときのメッセージ + plugin-connection-already-disconnected: "すでにサーバーから切断されています。" # すでに切断されている場合のメッセージ + plugin-connection-disconnect: "VOIP サーバーから切断されました。理由: $reason" # 切断されている場合のメッセージ + + player-disconnect-success: "プレイヤー $player がボイス チャットから退出しました。" # プレイヤーがボイス チャットを退出することを示すメッセージ。 + player-disconnect-failed: "プレイヤー $player がボイス チャットを退出するときにエラーを受信しました。" # プレイヤーがボイス チャットを退出するときのエラー メッセージ。 + + player-binded: "$player が VoiceCraft に接続しました。" # プレイヤーがボイス チャットに参加していることを示すメッセージ。 diff --git a/modules/velocity/src/main/resources/locale/nl.yml b/modules/velocity/src/main/resources/locale/nl.yml new file mode 100644 index 0000000..9689ad0 --- /dev/null +++ b/modules/velocity/src/main/resources/locale/nl.yml @@ -0,0 +1,56 @@ +# nl +messages: + commands: + connect: + invalid-args: "Ongeldig commando gebruik. Gebruik: /voice connect " + + disconnect: + disconnecting: "Verbinding met Server verbreken..." + already-disconnected: "De verbinding met de server is al verbroken." + invalid-args: "Ongeldig commando gebruik. Gebruik: /voice disconnect" + + settings: + nametag: "VoiceCraft Server Instellingen" + lore: "Open VoiceCraft Instellingen" + message: "Je hebt een item in je inventaris gekregen. Klik met de rechtermuisknop/communiceer met het item om de gebruikersinterface voor instellingen te openen" + + bind: + binding: "Koppelen..." + binded: "Koppelen Succesvol!" + failed: "Koppelen Mislukt! Check je key en probeer opnieuw!" + binding-fake: "Koppelen fake speler..." + invalid-args: "Ongeldig commando gebruik. Gebruik: /voice bind " + fake-invalid-args: "Ongeldig commando gebruik. Gebruik: /voice bindfake " + + reload: "Plugin herladen!" + + cmd-invalid-args: "Ongeldig commando gebruik. Gebruik: /voice " # Error message for invalid command usage. + + cmd-not-player: "Alleen spelers kunnen dit commando gebruiken!" # Error message for command use restricted to players only. + cmd-not-exists: "Dit commando bestaat niet." # Message indicating that the command does not exist. + + plugin-reload-pl: "Speler $player heeft de plugin herladen." # Player-triggered plugin reload message. + plugin-reload: "Speler $player heeft de plugin herladen." # Player-triggered plugin reload message. + + plugin-config-loaded: "Configuratie geladen." # Message indicating successful loading of the configuration. + plugin-command-executor: "Uitvoerders voor commando's ingeschakeld." # Message indicating successful enabling of command executors. + + plugin-connect-connecting: "Verbinden/Linken van Server..." # Successful connection to the voice chat server. + plugin-connect-connected: "Login Geaccepteerd. Server succesvol gelinkt." # Successful connection to the voice chat server. + plugin-connect-failed: "Login Mislukt! Server niet gelinkt! Voer /voice uit om opnieuw verbinding te makne." # Error message during connection to the voice chat server. + plugin-connect-invalid-data: "Verbinding mislukt. Ongeldige configuratie." # Error message indicating failed connection due to invalid configuration. + + plugin-connection-lost: "Verbinding met voicechatserver verloren." # Message when connection is lost. + plugin-connection-lost-reconnect: "Verbinding met voicechatserver verloren. Opnieuw verbinding proberen te maken..." # Message when connection is lost and when reconnecting + plugin-connection-reconnecting-attempt: "Opnieuw verbinden met server... Poging: $attempt" # Message when reconnecting + plugin-connection-reconnecting-success: "Met succes herverbonden met voicechatserver." # Message when reconnected to the server + plugin-connection-reconnecting-failed-retry: "Connectie mislukt, Opnieuw proberen..." # Message when reconnecting failed and retrying it + plugin-connection-reconnecting-failed: "Opnieuw verbinden met voicechatserver mislukt." # Message when reconnecting failed after 5 tries + plugin-connection-disconnecting: "Verbinding met Server verbreken..." # Message when disconnecting + plugin-connection-already-disconnected: "Verbinding was al verbroken met server." # Message when already disconnected + plugin-connection-disconnect: "Verbinding met VOIP Server verbroken, Reden: $reason" # Message when disconnect + + player-disconnect-success: "Speler $player heeft de voicechat verlaten." # Message indicating player leaving the voice chat. + player-disconnect-failed: "Speler $player ontving een foutmelding bij het verlaten van de voicechat." # Error message during player leaving the voice chat. + + player-binded: "$player is verbonden met VoiceCraft." # Message indicating player joining the voice chat. diff --git a/modules/velocity/src/main/resources/locale/ru.yml b/modules/velocity/src/main/resources/locale/ru.yml new file mode 100644 index 0000000..75fc126 --- /dev/null +++ b/modules/velocity/src/main/resources/locale/ru.yml @@ -0,0 +1,57 @@ +# ru +messages: + commands: + connect: + invalid-args: "Неверное использование команды. Использование: /voice connect <хост> <порт> <ключ-сервера>" + + disconnect: + disconnecting: "Отключение от сервера..." + already-disconnected: "Вы уже отключены от сервера." + invalid-args: "Неверное использование команды. Использование: /voice disconnect" + + settings: + nametag: "Настройки сервера VoiceCraft" + lore: "Открыть настройки VoiceCraft" + message: "В ваш инвентарь добавлен предмет. Щелкните ПКМ / взаимодействуйте с предметом, чтобы открыть интерфейс настроек." + + bind: + binding: "Привязка..." + binded: "Привязка успешна!" + failed: "Ошибка привязки! Проверьте ваш ключ и попробуйте снова!" + broadcast: "$player подключился к VoiceCraft!" + binding-fake: "Привязка фейкового игрока..." + invalid-args: "Неверное использование команды. Использование: /voice bind <ключ>" + fake-invalid-args: "Неверное использование команды. Использование: /voice bindfake <ключ> <имя>" + + reload: "Плагин перезагружен!" + + cmd-invalid-args: "Неверное использование команды. Использование: /voice " + + cmd-not-player: "Эту команду могут использовать только игроки!" + cmd-not-exists: "Эта команда не существует." + + plugin-reload-pl: "Игрок $player перезагрузил плагин." + plugin-reload: "Игрок $player перезагрузил плагин." + + plugin-config-loaded: "Конфигурация загружена." + plugin-command-executor: "Исполнители команд активированы." + + plugin-connect-connecting: "Подключение/связь с сервером..." + plugin-connect-connected: "Вход принят. Сервер успешно связан!" + plugin-connect-failed: "Ошибка входа! Сервер не связан! Запустите /voice , чтобы повторить попытку." + plugin-connect-invalid-data: "Ошибка подключения. Некорректная конфигурация." + + plugin-connection-lost: "Соединение с голосовым сервером потеряно." + plugin-connection-lost-reconnect: "Соединение с голосовым сервером потеряно. Попытка переподключения..." + plugin-connection-reconnecting-attempt: "Переподключение к серверу... Попытка: $attempt" + plugin-connection-reconnecting-success: "Успешно переподключились к голосовому серверу." + plugin-connection-reconnecting-failed-retry: "Ошибка подключения, повторная попытка..." + plugin-connection-reconnecting-failed: "Не удалось переподключиться к голосовому серверу." + plugin-connection-disconnecting: "Отключение от сервера..." + plugin-connection-already-disconnected: "Вы уже отключены от сервера." + plugin-connection-disconnect: "Отключено от VOIP-сервера, причина: $reason" + + player-disconnect-success: "Игрок $player покинул голосовой чат." + player-disconnect-failed: "Игрок $player столкнулся с ошибкой при выходе из голосового чата." + + player-binded: "$player подключился к VoiceCraft." diff --git a/settings.gradle b/settings.gradle index 276f676..ca6bcf6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,10 @@ -rootProject.name = 'GeyserVoice' \ No newline at end of file +rootProject.name = 'GeyserVoice' +include 'core' +include 'paper' +include 'velocity' +include 'bungeecord' + +project(':core').projectDir = file('modules/core') +project(':paper').projectDir = file('modules/paper') +project(':velocity').projectDir = file('modules/velocity') +project(':bungeecord').projectDir = file('modules/bungeecord') diff --git a/src/main/java-templates/io/greitan/avion/common/utils/Constants.java b/src/main/java-templates/io/greitan/avion/common/utils/Constants.java deleted file mode 100644 index 799c8d1..0000000 --- a/src/main/java-templates/io/greitan/avion/common/utils/Constants.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.greitan.avion.common.utils; - -public class Constants { - public final static String NAME = "{{ name }}"; - public final static String VERSION = "{{ version}}"; - public final static String DESCRIPTION = "{{ description }}"; - public final static String URL = "{{ url }}"; -} \ No newline at end of file diff --git a/src/main/java/io/greitan/avion/bungeecord/GeyserVoice.java b/src/main/java/io/greitan/avion/bungeecord/GeyserVoice.java deleted file mode 100644 index 317e558..0000000 --- a/src/main/java/io/greitan/avion/bungeecord/GeyserVoice.java +++ /dev/null @@ -1,428 +0,0 @@ -package io.greitan.avion.bungeecord; - -import lombok.Getter; - -import net.md_5.bungee.api.plugin.Plugin; -import net.md_5.bungee.api.scheduler.ScheduledTask; -import net.md_5.bungee.config.ConfigurationProvider; -import net.md_5.bungee.config.Configuration; -import net.md_5.bungee.config.YamlConfiguration; -import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.md_5.bungee.api.connection.ProxiedPlayer; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.greitan.avion.bungeecord.commands.VoiceCommand; -import io.greitan.avion.bungeecord.listeners.*; -import io.greitan.avion.common.BaseGeyserVoice; -import io.greitan.avion.common.network.Network; -import io.greitan.avion.common.network.Payloads.PlayerData; -import io.greitan.avion.bungeecord.tasks.PositionsTask; -import io.greitan.avion.bungeecord.utils.*; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.FileOutputStream; -import java.util.concurrent.TimeUnit; -import java.util.Objects; -import java.util.Map; -import java.util.HashMap; - -public class GeyserVoice extends Plugin implements BaseGeyserVoice { - private static @Getter Configuration config; - - private static @Getter GeyserVoice instance; - private @Getter boolean isConnected = false; - private @Getter String host = ""; - private @Getter int port = 0; - private @Getter String serverKey = ""; - private @Getter Map playerBinds = new HashMap<>(); - private @Getter String token = ""; - private String lang; - private @Getter PluginMessageHandler messageHandler = new PluginMessageHandler(this); - public Map playerDataList = new HashMap<>(); - - private @Getter ScheduledTask taskRunner; - - public BungeecordLogger Logger = new BungeecordLogger(); - public Network network = new Network(Logger); - - public static final ObjectMapper objectMapper = new ObjectMapper(); - - /** - * Executes upon enabling the plugin. - */ - @Override - public void onEnable() { - instance = this; - - this.reloadConfig(); - lang = getConfig().getString("config.lang"); - int positionTaskInterval = getConfig().getInt("config.voice.position-task-interval", 1); - Language.init(this); - - getProxy().registerChannel(PluginMessageHandler.channelName); - getProxy().getPluginManager().registerListener(this, messageHandler); - - getProxy().getPluginManager().registerCommand(this, new VoiceCommand(this, lang)); - - // 1s / 20 ticks = 0.050s = 50ms - taskRunner = getProxy().getScheduler().schedule(this, new PositionsTask(this, lang), 1, - 50 * positionTaskInterval, TimeUnit.MILLISECONDS); - - getProxy().getPluginManager().registerListener(this, new PlayerJoinHandler(this, lang)); - getProxy().getPluginManager().registerListener(this, new PlayerQuitHandler(this, lang)); - - this.reload(); - } - - @Override - public void onDisable() { - // make sure to unregister the registered channels in case of a reload - getProxy().unregisterChannel(PluginMessageHandler.channelName); - taskRunner.cancel(); - } - - /** - * Reloads the plugin configuration and initializes connections. - */ - public void reload() { - // saveDefaultConfig(); - reloadConfig(); - Logger.info(Language.getMessage(lang, "plugin-config-loaded")); - Logger.info(Language.getMessage(lang, "plugin-command-executor")); - - host = getConfig().getString("config.host"); - port = getConfig().getInt("config.port"); - serverKey = getConfig().getString("config.server-key"); - - if (getConfig().getBoolean("config.auto-reconnect")) - isConnected = reconnect(true); - - int positionTaskInterval = getConfig().getInt("config.voice.position-task-interval", 1); - taskRunner.cancel(); - // 1s / 20 ticks = 0.050s = 50ms - taskRunner = getProxy().getScheduler().schedule(this, new PositionsTask(this, lang), 1, - 50 * positionTaskInterval, TimeUnit.MILLISECONDS); - - int proximityDistance = getConfig().getInt("config.voice.proximity-distance"); - Boolean proximityToggle = getConfig().getBoolean("config.voice.proximity-toggle"); - Boolean voiceEffects = getConfig().getBoolean("config.voice.voice-effects"); - - updateSettings(proximityDistance, proximityToggle, voiceEffects); - } - - /** - * Connects to a new server. - * - * @param host The host to connect to. - * @param port The port to connect to. - * @param serverKey The server key. - * @return True if connected successfully, otherwise false. - */ - public Boolean connect(String host, int port, String serverKey) { - if (Objects.nonNull(host) && Objects.nonNull(serverKey)) { - getConfig().set("config.host", host); - getConfig().set("config.port", port); - getConfig().set("config.server-key", serverKey); - saveConfig(); - reloadConfig(); - reload(); - - return isConnected; - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); - return false; - } - } - - /** - * Connects to the server. - * - * @param force Indicates whether to force a connection. - * @return True if connected successfully, otherwise false. - */ - public Boolean reconnect(Boolean force) { - if (isConnected && !force) - return true; - if (isConnected) { - disconnect("Reconnecting to another server."); - } - - if (Objects.nonNull(host) && Objects.nonNull(serverKey)) { - String link = "http://" + host + ":" + port; - String Token = network.sendLoginRequest(link, serverKey); - if (Objects.nonNull(Token)) { - Logger.info(Language.getMessage(lang, "plugin-connect-connected")); - isConnected = true; - token = Token; - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-failed")); - } - return isConnected; - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); - return false; - } - } - - /** - * Disconnects from the server. - * - * @param reason The reason why we disconnected - */ - public void disconnect(String reason) { - if (!isConnected) - return; - - if (Objects.nonNull(host) && Objects.nonNull(serverKey)) { - String link = "http://" + host + ":" + port; - network.sendLogoutRequest(link, token); - isConnected = false; - - String disconnectMessage = Language.getMessage(lang, "plugin-connection-disconnect").replace("$reason", reason); - Logger.info(disconnectMessage); - - boolean sendVoipDisconnectMessage = getConfig().getBoolean("config.voice.send-voip-disconnect-message"); - if (sendVoipDisconnectMessage) { - getProxy().broadcast(new ComponentBuilder(disconnectMessage).color(ChatColor.YELLOW).create()); - } - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); - } - } - - /** - * Disconnects from the server. - */ - public void disconnect() { - disconnect("N.A."); - } - - /** - * Binds a player to the voice chat server. - * - * @param playerKey The key associated with the player. - * @param player The player to bind. - * @return True if the binding was successful, otherwise false. - */ - public Boolean bind(int playerKey, ProxiedPlayer player, int tries) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return false; - - if (playerBinds.containsKey(player.getName()) && playerBinds.get(player.getName())) { - return true; - } - - String link = "http://" + host + ":" + port; - - getConfig().set("config.players." + player.getName(), playerKey); - saveConfig(); - - String result = network.sendBindRequest(link, token, playerKey, player.getUniqueId().toString(), - player.getName()); - playerBinds.put(player.getName(), false); - if (result != null) { - if (result == "SUCCESS") { - playerBinds.put(player.getName(), true); - messageHandler.sendPlayerBindSync(player); - - Logger.info(Language.getMessage(lang, "player-binded").replace("$player",player.getName())); - - boolean sendBindedMessage = getConfig().getBoolean("config.voice.send-binded-message"); - if (sendBindedMessage) { - getProxy().broadcast( - new ComponentBuilder(player.getName()).bold(true) - .append( - new ComponentBuilder( - Language.getMessage(lang, "player-binded") - .replace("$player", "") - ) - .color(ChatColor.DARK_GREEN) - .create() - ).create() - ); - } - return true; - } else if (result == "Invalid Token!" && tries == 0) { - Logger.info("Invalid Token detected, reconnecting..."); - isConnected = reconnect(true); - return bind(playerKey, player, 1); - } - } - messageHandler.sendPlayerBindSync(player); - return false; - } - - public Boolean bind(int playerKey, ProxiedPlayer player) { - return bind(playerKey, player, 0); - } - - /** - * Bind a fake player - * @param bindKey - * @param name - * @return - */ - public Boolean bindFake(int playerKey, String name, int tries) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return false; - - if (playerBinds.containsKey(name) && playerBinds.get(name)) { - return true; - } - - String link = "http://" + host + ":" + port; - - String result = network.sendBindRequest(link, token, playerKey, String.format("%0", playerKey), name); - playerBinds.put(name, false); - if (result != null) { - if (result == "SUCCESS") { - playerBinds.put(name, true); - // messageHandler.sendPlayerBindSync(player); - - Logger.info(Language.getMessage(lang, "player-binded").replace("$player", name)); - - boolean sendBindedMessage = getConfig().getBoolean("config.voice.send-binded-message"); - if (sendBindedMessage) { - getProxy().broadcast( - new ComponentBuilder(name).bold(true) - .append( - new ComponentBuilder( - Language.getMessage(lang, "player-binded") - .replace("$player", "") - ) - .color(ChatColor.DARK_GREEN) - .create() - ).create() - ); - } - return true; - } else if (result == "Invalid Token!" && tries == 0) { - Logger.info("Invalid Token detected, reconnecting..."); - isConnected = reconnect(true); - return bindFake(playerKey, name, 1); - } - } - // messageHandler.sendPlayerBindSync(player); - return false; - } - - public Boolean bindFake(int playerKey, String name) { - return bindFake(playerKey, name, 0); - } - - /** - * Disconnects a player from the voice chat server. - * - * @param player The player to disconnect. - * @return True if the disconnection was successful, otherwise false. - */ - public Boolean disconnectPlayer(ProxiedPlayer player, int tries) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return false; - String link = "http://" + host + ":" + port; - - String result = network.sendDisconnectRequest(link, token, player.getUniqueId().toString(), player.getName()); - if (result != null) { - if (result == "SUCCESS") { - playerBinds.remove(player.getName()); - messageHandler.sendPlayerBindSync(player); - return true; - } else if (result == "Invalid Token!" && tries == 0) { - Logger.info("Invalid Token detected, reconnecting..."); - isConnected = reconnect(true); - return disconnectPlayer(player, 1); - } - } - return false; - } - - public Boolean disconnectPlayer(ProxiedPlayer player) { - return disconnectPlayer(player, 0); - } - - /** - * Updates the voice chat settings. - * - * @param proximityDistance Proximity distance setting. - * @param proximityToggle Proximity toggle setting. - * @param voiceEffects Voice effects setting. - * @return True if settings were updated successfully, otherwise false. - */ - public Boolean updateSettings(int proximityDistance, Boolean proximityToggle, Boolean voiceEffects) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return false; - String link = "http://" + host + ":" + port; - - return network.sendUpdateSettingsRequest(link, token, proximityDistance, proximityToggle, voiceEffects); - } - - public void setNotConnected() { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return; - isConnected = false; - } - - public void reloadConfig() { - saveResource("config.yml"); - - File configFile = new File(getDataFolder(), "config.yml"); - try { - config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public void saveConfig() { - try { - ConfigurationProvider.getProvider(YamlConfiguration.class).save(config, - new File(getDataFolder(), "config.yml")); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public void saveResource(String resourcePath, boolean replace) { - if (resourcePath == null || resourcePath.equals("")) { - throw new IllegalArgumentException("ResourcePath cannot be null or empty"); - } - - resourcePath = resourcePath.replace("\\", "/"); - InputStream in = getResourceAsStream(resourcePath); - if (in == null) { - throw new IllegalArgumentException("The embedded resource '" + resourcePath + "' cannot be found"); - } - - File outFile = new File(getDataFolder(), resourcePath); - int lastIndex = resourcePath.lastIndexOf("/"); - File outDir = new File(getDataFolder(), resourcePath.substring(0, lastIndex >= 0 ? lastIndex : 0)); - - if (!outDir.exists()) { - outDir.mkdirs(); - } - - try { - if (!outFile.exists() || replace) { - OutputStream out = new FileOutputStream(outFile); - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - out.close(); - in.close(); - } - } catch (IOException ex) { - Logger.error("Could not save " + outFile.getName() + " to " + outFile); - } - } - - public void saveResource(String resourcePath) { - saveResource(resourcePath, false); - } -} diff --git a/src/main/java/io/greitan/avion/bungeecord/commands/VoiceCommand.java b/src/main/java/io/greitan/avion/bungeecord/commands/VoiceCommand.java deleted file mode 100644 index d0011de..0000000 --- a/src/main/java/io/greitan/avion/bungeecord/commands/VoiceCommand.java +++ /dev/null @@ -1,97 +0,0 @@ -package io.greitan.avion.bungeecord.commands; - -import net.md_5.bungee.api.plugin.Command; -import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.plugin.TabExecutor; -import net.md_5.bungee.api.connection.ProxiedPlayer; - -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.md_5.bungee.api.ChatColor; - -import io.greitan.avion.bungeecord.GeyserVoice; -import io.greitan.avion.bungeecord.utils.Language; -import io.greitan.avion.common.commands.BaseVoiceCommand; -import io.greitan.avion.common.utils.IntegerOperation; -import io.greitan.avion.common.utils.StringOperation; -import io.greitan.avion.common.utils.DoubleStringOperation; -import io.greitan.avion.common.utils.EmptyOperation; - -public class VoiceCommand extends Command implements TabExecutor { - - private final BaseVoiceCommand voiceCommand; - private final GeyserVoice plugin; - private final String lang; - - // Get the plugin and lang interfaces. - public VoiceCommand(GeyserVoice plugin, String lang) { - super("voice"); - this.voiceCommand = new BaseVoiceCommand(plugin); - this.plugin = plugin; - this.lang = lang; - } - - @Override - public void execute(CommandSender sender, String[] args) { - this.voiceCommand.onCommand( - args, - plugin.isConnected(), - sender instanceof ProxiedPlayer, - new StringOperation() { - @Override - public boolean execute(String permission) { - if (sender instanceof ProxiedPlayer) - return sender.hasPermission(permission); - else - return true; - } - }, - new DoubleStringOperation() { - @Override - public void execute(String text, String rawColor) { - ChatColor color = ChatColor.RED; - if (rawColor == "red") color = ChatColor.RED; - else if (rawColor == "aqua") color = ChatColor.AQUA; - else if (rawColor == "green") color = ChatColor.GREEN; - else if (rawColor == "yellow") color = ChatColor.YELLOW; - - var message = new ComponentBuilder(Language.getMessage(lang, text)).color(color).create(); - if (sender instanceof ProxiedPlayer) - sender.sendMessage(message); - else - plugin.Logger.log(message); - } - }, - new IntegerOperation() { - @Override - public boolean execute(int key) { - if (sender instanceof ProxiedPlayer) { - ProxiedPlayer player = (ProxiedPlayer) sender; - return plugin.bind(key, player); - } - return false; - } - }, - new EmptyOperation() { - @Override - public boolean execute() { - if (sender instanceof ProxiedPlayer) { - ProxiedPlayer player = (ProxiedPlayer) sender; - GeyserVoice.getConfig().set("config.players." + player.getName(), null); - return true; - } - return false; - } - } - ); - } - - @Override - public Iterable onTabComplete(CommandSender sender, String[] args) { - return voiceCommand.onTabComplete(args, new StringOperation() { - @Override - public boolean execute(String permission) { - return sender.hasPermission(permission); - } - }); - } -} diff --git a/src/main/java/io/greitan/avion/bungeecord/listeners/PlayerJoinHandler.java b/src/main/java/io/greitan/avion/bungeecord/listeners/PlayerJoinHandler.java deleted file mode 100644 index 9421cd8..0000000 --- a/src/main/java/io/greitan/avion/bungeecord/listeners/PlayerJoinHandler.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.greitan.avion.bungeecord.listeners; - -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.event.EventHandler; -import net.md_5.bungee.api.plugin.Listener; -import net.md_5.bungee.api.event.PostLoginEvent; -import net.md_5.bungee.api.event.ServerConnectedEvent; -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.md_5.bungee.api.ChatColor; - -import io.greitan.avion.bungeecord.GeyserVoice; -import io.greitan.avion.bungeecord.utils.Language; - -import java.util.Objects; - -public class PlayerJoinHandler implements Listener { - - private final GeyserVoice plugin; - private final String lang; - - public PlayerJoinHandler(GeyserVoice plugin, String lang) { - this.plugin = plugin; - this.lang = lang; - } - - @EventHandler - public void onPlayerJoin(PostLoginEvent event) { - boolean isConnected = plugin.isConnected(); - ProxiedPlayer player = event.getPlayer(); - int playerBindKey = GeyserVoice.getConfig().getInt("config.players." + player.getName(), -1); - - if (isConnected && Objects.nonNull(playerBindKey) && playerBindKey != -1) { - handleAutoBind(playerBindKey, player); - } - - plugin.getMessageHandler().sendPlayerBindSync(player); - } - - @EventHandler - public void onPlayerConnect(ServerConnectedEvent event) { - ProxiedPlayer player = event.getPlayer(); - // Just send the message again, in case it didn't got send before... - plugin.getMessageHandler().sendPlayerBindSync(player); - } - - private void handleAutoBind(int playerBindKey, ProxiedPlayer player) { - player.sendMessage( - new ComponentBuilder(Language.getMessage(lang, "plugin-autobind-enabled")).color(ChatColor.GREEN) - .append(new ComponentBuilder(" ").create()) - .append( - new ComponentBuilder(Language.getMessage(lang, "plugin-autobind-binding")).color(ChatColor.YELLOW).create() - ).create()); - - boolean isBound = plugin.bind(playerBindKey, player); - - if (!isBound) { - player.sendMessage(new ComponentBuilder(Language.getMessage(lang, "plugin-autobind-failed")) - .color(ChatColor.RED).create()); - } - } -} diff --git a/src/main/java/io/greitan/avion/bungeecord/listeners/PluginMessageHandler.java b/src/main/java/io/greitan/avion/bungeecord/listeners/PluginMessageHandler.java deleted file mode 100644 index ca6ef01..0000000 --- a/src/main/java/io/greitan/avion/bungeecord/listeners/PluginMessageHandler.java +++ /dev/null @@ -1,98 +0,0 @@ -package io.greitan.avion.bungeecord.listeners; - -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteStreams; -import com.fasterxml.jackson.core.JsonProcessingException; - -import net.md_5.bungee.api.connection.Server; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.api.config.ServerInfo; -import net.md_5.bungee.api.plugin.Listener; -import net.md_5.bungee.event.EventHandler; -import net.md_5.bungee.api.event.PluginMessageEvent; - -import io.greitan.avion.common.network.Payloads.PlayerData; -import io.greitan.avion.bungeecord.GeyserVoice; - -import java.util.Arrays; -import java.util.List; - -public class PluginMessageHandler implements Listener { - - private final GeyserVoice plugin; - public static final String channelName = "geyservoice:main"; - - public PluginMessageHandler(GeyserVoice plugin) { - this.plugin = plugin; - } - - public void sendPlayerBindSync(ProxiedPlayer player) { - boolean isBound = plugin.isConnected() && plugin.getPlayerBinds().getOrDefault(player.getName(), false); - - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("PlayerBindSync"); - out.writeUTF(player.getName()); - out.writeBoolean(isBound); - - for (ServerInfo server : plugin.getProxy().getServers().values()) { - server.sendData(channelName, out.toByteArray(), true); - } - // plugin.getProxy().getServers().entrySet().iterator().next().getValue().sendData(channelName, - // out.toByteArray(), true); - plugin.Logger.info("Send PariticipantJoined message"); - } - - @EventHandler - public void onPluginMessageReceived(PluginMessageEvent event) { - // Ensure the identifier is what you expect before trying to handle the data - if (!event.getTag().equals(channelName)) { - return; - } - - String serverName = ""; - if (event.getSender() instanceof Server) { - Server backend = (Server) event.getSender(); - serverName = backend.getInfo().getName(); - } else if (event.getSender() instanceof ProxiedPlayer) { - ProxiedPlayer player = (ProxiedPlayer) event.getSender(); - serverName = player.getServer().getInfo().getName(); - } else { - return; - } - - ByteArrayDataInput in = ByteStreams.newDataInput(event.getData()); - String subchannel = in.readUTF(); - if (subchannel.equals("PlayerDataList")) { - String rawPlayerDataList = in.readUTF(); - plugin.Logger.debug("Received playerdatalist: " + rawPlayerDataList); - try { - List playerDataList = Arrays - .asList(GeyserVoice.objectMapper.readValue(rawPlayerDataList, PlayerData[].class)); - for (PlayerData playerData : playerDataList) { - playerData.DimensionId = serverName + "_" + playerData.DimensionId; - plugin.playerDataList.put(playerData.PlayerId, playerData); - } - } catch (JsonProcessingException e) { - } - } else if (subchannel.equals("PlayerData")) { - PlayerData playerData = new PlayerData(); - playerData.PlayerId = in.readUTF(); - playerData.DimensionId = in.readUTF(); - playerData.Location.x = in.readDouble(); - playerData.Location.y = in.readDouble(); - playerData.Location.z = in.readDouble(); - playerData.Rotation = in.readDouble(); - playerData.EchoFactor = in.readDouble(); - playerData.Muffled = in.readBoolean(); - playerData.IsDead = in.readBoolean(); - - playerData.DimensionId = serverName + "_" + playerData.DimensionId; - plugin.playerDataList.put(playerData.PlayerId, playerData); - } - - // Make sure to cancel the event after we finished handling it, else the player - // will also receive our messages... - event.setCancelled(true); - } -} diff --git a/src/main/java/io/greitan/avion/bungeecord/tasks/PositionsTask.java b/src/main/java/io/greitan/avion/bungeecord/tasks/PositionsTask.java deleted file mode 100644 index 3d68b3a..0000000 --- a/src/main/java/io/greitan/avion/bungeecord/tasks/PositionsTask.java +++ /dev/null @@ -1,142 +0,0 @@ -package io.greitan.avion.bungeecord.tasks; - -import io.greitan.avion.bungeecord.GeyserVoice; -import io.greitan.avion.bungeecord.utils.Language; -import io.greitan.avion.common.network.Payloads.PacketType; -import io.greitan.avion.common.network.Payloads.PlayerData; -import io.greitan.avion.common.network.Payloads.MCCommPacket; -import io.greitan.avion.common.network.Payloads.UpdatePacket; -import io.greitan.avion.common.network.Payloads.DenyPacket; - -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.md_5.bungee.api.ChatColor; - -import java.util.concurrent.TimeUnit; -import java.util.Map; -import java.util.ArrayList; -import java.util.List; -import java.lang.Runnable; - -public class PositionsTask implements Runnable { - private final GeyserVoice plugin; - private final String lang; - private boolean isConnected = false; - private Integer ReconnectRetries = 0; - - public PositionsTask(GeyserVoice plugin, String lang) { - this.plugin = plugin; - this.lang = lang; - } - - @Override - public void run() { - isConnected = plugin.isConnected(); - String host = plugin.getHost(); - int port = plugin.getPort(); - String token = plugin.getToken(); - String link = "http://" + host + ":" + port; - - if (isConnected) { - if (host != null && token != null) { - UpdatePacket updatePacket = new UpdatePacket(); - updatePacket.Token = token; - updatePacket.Players = getPlayerDataList(plugin.playerDataList); - - MCCommPacket response = plugin.network.sendPostRequest(link, updatePacket); - if (response != null) { - if (response.PacketId == PacketType.AckUpdate.ordinal()) { - // AckUpdatePacket packetData = plugin.objectMapper.convertValue(response, - // AckUpdatePacket.class); - // You can do stuff with the AckUpdate packet data here... - return; - } else if (response.PacketId == PacketType.Deny.ordinal()) { - DenyPacket packetData = GeyserVoice.objectMapper.convertValue(response, DenyPacket.class); - plugin.Logger.error(packetData.Reason); - if (!packetData.Reason.equals("Invalid Token!")) { - plugin.setNotConnected(); - // http.cancelAll(packetData.Reason); - cancel(); - return; - } - } else { - return; - } - } - if (!isConnected) - return; // do nothing. - - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-lost")); - plugin.setNotConnected(); - - if (GeyserVoice.getConfig().getBoolean("config.auto-reconnect")) { - if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - plugin.getProxy().broadcast( - new ComponentBuilder(Language.getMessage(lang, "plugin-connection-lost-reconnect")) - .color(ChatColor.RED).create()); - } - ReconnectRetries = 0; - Reconnect(); - return; - } - if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - plugin.getProxy() - .broadcast(new ComponentBuilder(Language.getMessage(lang, "plugin-connection-lost")) - .color(ChatColor.RED).create()); - } - cancel(); - } - } - } - - public List getPlayerDataList(Map allPlayerDataList) { - List playerDataList = new ArrayList<>(); - - for (String playerId : allPlayerDataList.keySet()) { - playerDataList.add(allPlayerDataList.get(playerId)); - } - - return playerDataList; - } - - private Boolean Reconnect() { - if (ReconnectRetries < 5) { - ReconnectRetries++; - - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-attempt").replace("$attempt", - ReconnectRetries.toString())); - - if (plugin.reconnect(true)) { - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-success")); - - if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - plugin.getProxy().broadcast( - new ComponentBuilder(Language.getMessage(lang, "plugin-connection-reconnecting-success")) - .color(ChatColor.GREEN).create()); - } - return true; - } else { - if (ReconnectRetries < 5) { - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-failed-retry")); - try { - TimeUnit.SECONDS.sleep(1); - } catch (Exception e) { - } - return Reconnect(); - } - plugin.Logger.error(Language.getMessage(lang, "plugin-connection-reconnecting-failed")); - - if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - plugin.getProxy().broadcast( - new ComponentBuilder(Language.getMessage(lang, "plugin-connection-reconnecting-failed")) - .color(ChatColor.RED).create()); - } - cancel(); - } - } - return false; - } - - private void cancel() { - plugin.getTaskRunner().cancel(); - } -} diff --git a/src/main/java/io/greitan/avion/bungeecord/utils/BungeecordLogger.java b/src/main/java/io/greitan/avion/bungeecord/utils/BungeecordLogger.java deleted file mode 100644 index 87837aa..0000000 --- a/src/main/java/io/greitan/avion/bungeecord/utils/BungeecordLogger.java +++ /dev/null @@ -1,100 +0,0 @@ -package io.greitan.avion.bungeecord.utils; - -import net.md_5.bungee.api.CommandSender; - -import io.greitan.avion.common.utils.BaseLogger; -import io.greitan.avion.bungeecord.GeyserVoice; -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.md_5.bungee.api.chat.BaseComponent; -import net.md_5.bungee.api.ChatColor; - -public class BungeecordLogger extends BaseLogger { - - public void log(BaseComponent[] msg) { - CommandSender console = GeyserVoice.getInstance().getProxy().getConsole(); - BaseComponent[] coloredLogo = new ComponentBuilder("[") - .color(ChatColor.WHITE) - .bold(true) - .append("GeyserVoice") - .color(ChatColor.LIGHT_PURPLE) - .bold(true) - .append("] ") - .color(ChatColor.WHITE) - .bold(true) - .append(msg) - .create(); - - console.sendMessage(coloredLogo); - } - - public void info(String msg) { - CommandSender console = GeyserVoice.getInstance().getProxy().getConsole(); - BaseComponent[] coloredLogo = new ComponentBuilder("[") - .color(ChatColor.WHITE) - .bold(true) - .append("GeyserVoice") - .color(ChatColor.LIGHT_PURPLE) - .bold(true) - .append("] ") - .color(ChatColor.WHITE) - .bold(true) - .append(msg).color(ChatColor.WHITE).bold(true) - .create(); - - console.sendMessage(coloredLogo); - } - - public void warn(String msg) { - CommandSender console = GeyserVoice.getInstance().getProxy().getConsole(); - BaseComponent[] coloredLogo = new ComponentBuilder("[") - .color(ChatColor.WHITE) - .bold(true) - .append("GeyserVoice") - .color(ChatColor.LIGHT_PURPLE) - .bold(true) - .append("] ") - .color(ChatColor.WHITE) - .bold(true) - .append(msg).color(ChatColor.YELLOW).bold(true) - .create(); - - console.sendMessage(coloredLogo); - } - - public void error(String msg) { - CommandSender console = GeyserVoice.getInstance().getProxy().getConsole(); - BaseComponent[] coloredLogo = new ComponentBuilder("[") - .color(ChatColor.WHITE) - .bold(true) - .append("GeyserVoice") - .color(ChatColor.LIGHT_PURPLE) - .bold(true) - .append("] ") - .color(ChatColor.WHITE) - .bold(true) - .append(msg).color(ChatColor.RED).bold(true) - .create(); - - console.sendMessage(coloredLogo); - } - - public void debug(String msg) { - CommandSender console = GeyserVoice.getInstance().getProxy().getConsole(); - Boolean isDebug = GeyserVoice.getConfig().getBoolean("config.debug"); - if (isDebug) { - BaseComponent[] coloredLogo = new ComponentBuilder("[") - .color(ChatColor.WHITE) - .bold(true) - .append("GeyserVoice") - .color(ChatColor.LIGHT_PURPLE) - .bold(true) - .append("] ") - .color(ChatColor.WHITE) - .bold(true) - .append(msg).color(ChatColor.BLUE).bold(true) - .create(); - - console.sendMessage(coloredLogo); - } - } -} diff --git a/src/main/java/io/greitan/avion/bungeecord/utils/Language.java b/src/main/java/io/greitan/avion/bungeecord/utils/Language.java deleted file mode 100644 index 9d2b55b..0000000 --- a/src/main/java/io/greitan/avion/bungeecord/utils/Language.java +++ /dev/null @@ -1,79 +0,0 @@ -package io.greitan.avion.bungeecord.utils; - -import net.md_5.bungee.config.ConfigurationProvider; -import net.md_5.bungee.config.Configuration; -import net.md_5.bungee.config.YamlConfiguration; - -import io.greitan.avion.bungeecord.GeyserVoice; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -public class Language { - private static final Map languageConfigs = new HashMap<>(); - private static String defaultLanguage = "en"; - - public static void init(GeyserVoice plugin) { - File languageFolder = new File(plugin.getDataFolder(), "locale"); - - if (!languageFolder.exists()) { - languageFolder.mkdirs(); - plugin.saveResource("locale/en.yml"); - plugin.saveResource("locale/ru.yml"); - plugin.saveResource("locale/nl.yml"); - plugin.saveResource("locale/ja.yml"); - } - - loadLanguages(languageFolder.getAbsolutePath()); - } - - private static void loadLanguages(String pluginFolder) { - File languageFolder = new File(pluginFolder); - - if (languageFolder.exists() && languageFolder.isDirectory()) { - - for (File file : languageFolder.listFiles()) { - - if (file.getName().endsWith(".yml")) { - String language = file.getName().replace(".yml", ""); - try { - Configuration config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file); - languageConfigs.put(language, config); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - } - - /* - * private static void copyResource(Plugin plugin, String resourceName, File - * destination) - * { - * try (InputStream inputStream = plugin.getResourceAsStream(resourceName)) - * { - * if (inputStream != null) - * { - * Files.copy(inputStream, destination.toPath(), - * StandardCopyOption.REPLACE_EXISTING); - * } - * } catch (IOException e) - * { - * e.printStackTrace(); - * } - * } - */ - - public static String getMessage(String language, String key) { - if (languageConfigs.containsKey(language)) { - Configuration config = languageConfigs.get(language); - if (config.contains("messages." + key)) { - return config.getString("messages." + key); - } - } - return languageConfigs.get(defaultLanguage).getString("messages." + key); - } -} diff --git a/src/main/java/io/greitan/avion/common/network/Network.java b/src/main/java/io/greitan/avion/common/network/Network.java deleted file mode 100644 index e1c9e94..0000000 --- a/src/main/java/io/greitan/avion/common/network/Network.java +++ /dev/null @@ -1,195 +0,0 @@ -package io.greitan.avion.common.network; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.greitan.avion.common.utils.BaseLogger; -import io.greitan.avion.common.network.Payloads.PacketType; -import io.greitan.avion.common.network.Payloads.MCCommPacket; -import io.greitan.avion.common.network.Payloads.LoginPacket; -import io.greitan.avion.common.network.Payloads.LogoutPacket; -import io.greitan.avion.common.network.Payloads.AcceptPacket; -import io.greitan.avion.common.network.Payloads.DenyPacket; -import io.greitan.avion.common.network.Payloads.BindPacket; -import io.greitan.avion.common.network.Payloads.DisconnectParticipantPacket; -import io.greitan.avion.common.network.Payloads.SetDefaultSettingsPacket; - -public class Network { - private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final HttpClient httpClient = HttpClient.newHttpClient(); - private BaseLogger Logger; - - private static String Version = "1.0.0"; - - public Network(T logger) { - this.Logger = logger; - } - - public MCCommPacket sendPostRequest(String url, MCCommPacket data) { - try { - String jsonData = objectMapper.writeValueAsString(data); - Logger.debug("Request: " + jsonData.toString()); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(5)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(jsonData)) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - int statusCode = response.statusCode(); - String body = response.body(); - - Logger.debug("Response: " + body); - - if (statusCode == 200) { - return objectMapper.readValue(body, MCCommPacket.class); - } else { - Logger.error("Sending HTTP Packet Failed, Reason: HTTP_EXCEPTION, STATUS_CODE: " + statusCode); - // throw new Exception("Sending HTTP Packet Failed, Reason: HTTP_EXCEPTION, STATUS_CODE: " + statusCode); - return null; - } - } catch (Exception e) { - String message = e.getMessage(); - if (message == null) message = e.toString(); - Logger.error("Can't connect to voice chat server! " + message); - return null; - } - } - - /** - * Sends the login request to the server. - * - * @param link HTTP POST link - * @param serverKey The server key - * @return Token if connected successfully, otherwise null. - */ - public String sendLoginRequest(String link, String serverKey) { - // Create request data object. - LoginPacket loginPacket = new LoginPacket(); - loginPacket.LoginKey = serverKey; - loginPacket.Version = Version; - - MCCommPacket response = sendPostRequest(link, loginPacket); - if (response != null) { - if (response.PacketId == PacketType.Accept.ordinal()) { - AcceptPacket packetData = objectMapper.convertValue(response, AcceptPacket.class); - return packetData.Token; - } - else if (response.PacketId == PacketType.Deny.ordinal() || response instanceof DenyPacket) - { - DenyPacket packetData = objectMapper.convertValue(response, DenyPacket.class); - Logger.error( - "Login Denied. Server denied link request! Reason: " + packetData.Reason); - } - } else { - Logger.error("Could not contact server. Please check if your IPAddress and Port are correct!"); - } - return null; - } - - /** - * Sends the logout request to the server. - * - * @param link HTTP POST link - * @param token The session token - */ - public void sendLogoutRequest(String link, String token) { - // Create request data object. - LogoutPacket logoutPacket = new LogoutPacket(); - logoutPacket.Token = token; - - sendPostRequest(link, logoutPacket); - } - - /** - * Sends the bind request to the server. - * - * @param link HTTP POST link - * @param token The token from the login - * @param playerKey The bind key for the player - * @param playerId The unique but consistent ID of the player - * @param playerName The name of the player - * @return "SUCCESS" if binded successfully, otherwise null or reason for - * failure. - */ - public String sendBindRequest(String link, String token, Integer playerKey, String playerId, String playerName) { - // Create request data object. - BindPacket bindPacket = new BindPacket(); - bindPacket.PlayerId = playerId; - bindPacket.PlayerKey = playerKey; - bindPacket.Gamertag = playerName; - bindPacket.Token = token; - - MCCommPacket bindStatus = sendPostRequest(link, bindPacket); - if (bindStatus == null) - return null; - - if (bindStatus.PacketId == PacketType.Accept.ordinal()) { - return "SUCCESS"; - } else if (bindStatus instanceof DenyPacket) { - DenyPacket packetData = objectMapper.convertValue(bindStatus, DenyPacket.class); - Logger.error( - "Binding " + bindPacket.Gamertag + " to " + playerKey + " failed. Reason: " + packetData.Reason); - return packetData.Reason; - } - return null; - } - - /** - * Sends the disconnect request to the server. - * - * @param link HTTP POST link - * @param token The token from the login - * @param playerId The unique but consistent ID of the player - * @param playerName The name of the player - * @return "SUCCESS" if disconnected successfully, otherwise null or reason for - * failure. - */ - public String sendDisconnectRequest(String link, String token, String playerId, String playerName) { - // Create request data object. - DisconnectParticipantPacket disconnectParticipantPacket = new DisconnectParticipantPacket(); - disconnectParticipantPacket.Token = token; - disconnectParticipantPacket.PlayerId = playerId; - - MCCommPacket disconnectStatus = sendPostRequest(link, disconnectParticipantPacket); - if (disconnectStatus == null) - return null; - - if (disconnectStatus.PacketId == PacketType.Accept.ordinal()) { - return "SUCCESS"; - } else if (disconnectStatus instanceof DenyPacket) { - DenyPacket packetData = objectMapper.convertValue(disconnectStatus, DenyPacket.class); - Logger.error("Disconnecting player " + playerName + " failed. Reason: " + packetData.Reason); - return packetData.Reason; - } - return null; - } - - /** - * Updates the voice chat settings. - * - * @param link HTTP POST link - * @param token The token from the login - * @param proximityDistance Proximity distance setting. - * @param proximityToggle Proximity toggle setting. - * @param voiceEffects Voice effects setting. - * @return True if settings were updated successfully, otherwise false. - */ - public Boolean sendUpdateSettingsRequest(String link, String token, int proximityDistance, Boolean proximityToggle, - Boolean voiceEffects) { - // Create request data object. - SetDefaultSettingsPacket setDefaultSettingsPacket = new SetDefaultSettingsPacket(); - setDefaultSettingsPacket.ProximityDistance = proximityDistance; - setDefaultSettingsPacket.ProximityToggle = proximityToggle; - setDefaultSettingsPacket.VoiceEffects = voiceEffects; - setDefaultSettingsPacket.Token = token; - - return sendPostRequest(link, setDefaultSettingsPacket) != null; - } -} diff --git a/src/main/java/io/greitan/avion/common/network/Payloads.java b/src/main/java/io/greitan/avion/common/network/Payloads.java deleted file mode 100644 index 32d87f3..0000000 --- a/src/main/java/io/greitan/avion/common/network/Payloads.java +++ /dev/null @@ -1,234 +0,0 @@ -package io.greitan.avion.common.network; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import java.util.List; -import java.util.Map; - -public class Payloads { - public static enum PacketType { - Login, // 0 - Logout, // 1 - Accept, // 2 - Deny, // 3 - Bind, // 4 - Update, // 5 - AckUpdate, // 6 - GetChannels, // 7 - GetChannelSettings, // 8 - SetChannelSettings, // 9 - GetDefaultSettings, // 10 - SetDefaultSettings, // 11 - - // Participant Stuff - GetParticipants, // 12 - DisconnectParticipant, // 13 - GetParticipantBitmask, // 14 - SetParticipantBitmask, // 15 - MuteParticipant, // 16 - UnmuteParticipant, // 17 - DeafenParticipant, // 18 - UndeafenParticipant, // 19 - - ANDModParticipantBitmask, // 20 - ORModParticipantBitmask, // 21 - XORModParticipantBitmask, // 22 - - ChannelMove; // 23 - - public static PacketType fromId(int id) { - for (PacketType type : PacketType.values()) { - if (type.ordinal() == id) { - return type; - } - } - throw new IllegalArgumentException("Unknown packet id: " + id); - } - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "PacketId", visible = true) - @JsonSubTypes({ - @JsonSubTypes.Type(value = LoginPacket.class, name = "0"), - @JsonSubTypes.Type(value = LogoutPacket.class, name = "1"), - @JsonSubTypes.Type(value = AcceptPacket.class, name = "2"), - @JsonSubTypes.Type(value = DenyPacket.class, name = "3"), - @JsonSubTypes.Type(value = BindPacket.class, name = "4"), - @JsonSubTypes.Type(value = UpdatePacket.class, name = "5"), - @JsonSubTypes.Type(value = AckUpdatePacket.class, name = "6"), - @JsonSubTypes.Type(value = GetChannelsPacket.class, name = "7"), - @JsonSubTypes.Type(value = GetChannelSettingsPacket.class, name = "8"), - @JsonSubTypes.Type(value = SetChannelSettingsPacket.class, name = "9"), - @JsonSubTypes.Type(value = GetDefaultSettingsPacket.class, name = "10"), - @JsonSubTypes.Type(value = SetDefaultSettingsPacket.class, name = "11"), - @JsonSubTypes.Type(value = GetParticipantsPacket.class, name = "12"), - @JsonSubTypes.Type(value = DisconnectParticipantPacket.class, name = "13"), - }) - public static abstract class MCCommPacket { - public int PacketId; - public String Token = ""; - } - - public static class LoginPacket extends MCCommPacket { - public LoginPacket() { - this.PacketId = PacketType.Login.ordinal(); - } - - public String LoginKey = ""; - public String Version = ""; - } - - public static class LogoutPacket extends MCCommPacket { - public LogoutPacket() { - this.PacketId = PacketType.Logout.ordinal(); - } - } - - public static class AcceptPacket extends MCCommPacket { - public AcceptPacket() { - this.PacketId = PacketType.Accept.ordinal(); - } - } - - public static class DenyPacket extends MCCommPacket { - public DenyPacket() { - this.PacketId = PacketType.Deny.ordinal(); - } - - public String Reason = ""; - } - - public static class BindPacket extends MCCommPacket { - public BindPacket() { - this.PacketId = PacketType.Bind.ordinal(); - } - - public String PlayerId = ""; - public int PlayerKey = 0; - public String Gamertag = ""; - } - - public static class UpdatePacket extends MCCommPacket { - public UpdatePacket() { - this.PacketId = PacketType.Update.ordinal(); - } - - public List Players; - } - - public static class AckUpdatePacket extends MCCommPacket { - public AckUpdatePacket() { - this.PacketId = PacketType.AckUpdate.ordinal(); - } - - public List SpeakingPlayers; - } - - public static class GetChannelsPacket extends MCCommPacket { - public GetChannelsPacket() { - this.PacketId = PacketType.GetChannels.ordinal(); - } - - public Map Channels; - } - - public static class GetChannelSettingsPacket extends MCCommPacket { - public GetChannelSettingsPacket() { - this.PacketId = PacketType.GetChannelSettings.ordinal(); - } - - public int ChannelId = 0; - public int ProximityDistance = 30; - public boolean ProximityToggle = true; - public boolean VoiceEffects = true; - } - - public static class SetChannelSettingsPacket extends MCCommPacket { - public SetChannelSettingsPacket() { - this.PacketId = PacketType.SetChannelSettings.ordinal(); - } - - public int ChannelId = 0; - public int ProximityDistance = 30; - public boolean ProximityToggle = true; - public boolean VoiceEffects = true; - public boolean ClearSettings = true; - } - - public static class GetDefaultSettingsPacket extends MCCommPacket { - public GetDefaultSettingsPacket() { - this.PacketId = PacketType.GetDefaultSettings.ordinal(); - } - - public int ProximityDistance = 30; - public boolean ProximityToggle = true; - public boolean VoiceEffects = true; - } - - public static class SetDefaultSettingsPacket extends MCCommPacket { - public SetDefaultSettingsPacket() { - this.PacketId = PacketType.SetDefaultSettings.ordinal(); - } - - public int ProximityDistance = 30; - public boolean ProximityToggle = true; - public boolean VoiceEffects = true; - } - - public static class GetParticipantsPacket extends MCCommPacket { - public GetParticipantsPacket() { - this.PacketId = PacketType.GetParticipants.ordinal(); - } - - public List Players; - } - - public static class DisconnectParticipantPacket extends MCCommPacket { - public DisconnectParticipantPacket() { - this.PacketId = PacketType.DisconnectParticipant.ordinal(); - } - - public String PlayerId = ""; - } - - public static class LocationData { - public double x = 0; - public double y = 0; - public double z = 0; - } - - public static class PlayerData { - public String PlayerId = ""; - public String DimensionId = ""; - public LocationData Location = new LocationData(); - public double Rotation = 0.0; - public double EchoFactor = 0.0; - public boolean Muffled = false; - public boolean IsDead = false; - - public PlayerData clone() { - PlayerData e = new PlayerData(); - e.PlayerId = this.PlayerId; - e.DimensionId = this.DimensionId; - e.Location = this.Location; - e.Rotation = this.Rotation; - e.EchoFactor = this.EchoFactor; - e.Muffled = this.Muffled; - e.IsDead = this.IsDead; - return e; - } - } - - public static class ChannelOverrideData { - public int ProximityDistance = 30; - public boolean ProximityToggle = true; - public boolean VoiceEffects = true; - } - - public static class ChannelData { - public String Name = ""; - public String Password = ""; - public boolean Locked = false; - public boolean Hidden = false; - public ChannelOverrideData OverrideSettings = null; - } -} diff --git a/src/main/java/io/greitan/avion/common/utils/EmptyOperation.java b/src/main/java/io/greitan/avion/common/utils/EmptyOperation.java deleted file mode 100644 index a935e7a..0000000 --- a/src/main/java/io/greitan/avion/common/utils/EmptyOperation.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.greitan.avion.common.utils; - -public interface EmptyOperation { - boolean execute(); -} diff --git a/src/main/java/io/greitan/avion/paper/GeyserVoice.java b/src/main/java/io/greitan/avion/paper/GeyserVoice.java deleted file mode 100644 index b834622..0000000 --- a/src/main/java/io/greitan/avion/paper/GeyserVoice.java +++ /dev/null @@ -1,382 +0,0 @@ -package io.greitan.avion.paper; - -import lombok.Getter; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextDecoration; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitTask; -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.greitan.avion.common.BaseGeyserVoice; -import io.greitan.avion.paper.commands.VoiceCommand; -import io.greitan.avion.paper.listeners.*; -import io.greitan.avion.common.network.Network; -import io.greitan.avion.paper.tasks.PositionsTask; -import io.greitan.avion.paper.utils.*; - -import java.io.File; -import java.util.Objects; -import java.util.Map; -import java.util.HashMap; - -/** - * Main plugin class for GeyserVoice. - */ -public class GeyserVoice extends JavaPlugin implements BaseGeyserVoice { - private static @Getter GeyserVoice instance; - private @Getter boolean isConnected = false; - private @Getter String host = ""; - private @Getter int port = 0; - private @Getter String serverKey = ""; - private @Getter Map playerBinds = new HashMap<>(); - private @Getter String token = ""; - private String lang; - public boolean usesProxy = false; - private @Getter PluginMessageHandler messageHandler = new PluginMessageHandler(this); - - private BukkitTask taskRunner; - - public PaperLogger Logger = new PaperLogger(); - public Network network = new Network(Logger); - - public static final ObjectMapper objectMapper = new ObjectMapper(); - - /** - * Executes upon enabling the plugin. - */ - @Override - public void onEnable() { - instance = this; - - lang = getConfig().getString("config.lang"); - int positionTaskInterval = getConfig().getInt("config.voice.position-task-interval", 1); - Language.init(this); - - getServer().getMessenger().registerOutgoingPluginChannel(this, PluginMessageHandler.channelName); - getServer().getMessenger().registerIncomingPluginChannel(this, PluginMessageHandler.channelName, - messageHandler); - if (!getServer().getMessenger().isOutgoingChannelRegistered(this, PluginMessageHandler.channelName)) - Logger.warn("Outgoing Channel failed to register!"); - if (!getServer().getMessenger().isIncomingChannelRegistered(this, PluginMessageHandler.channelName)) - Logger.warn("Incoming Channel failed to register!"); - - usesProxy = getConfig().getBoolean("config.server-behind-proxy", false); - // if - // (getServer().spigot().getConfig().getConfigurationSection("settings").getBoolean("bungeecord")) - // usesProxy = true; - - VoiceCommand voiceCommand = new VoiceCommand(this, lang); - getCommand("voice").setExecutor(voiceCommand); - getCommand("voice").setTabCompleter(voiceCommand); - taskRunner = new PositionsTask(this, lang).runTaskTimer(this, 1, positionTaskInterval); - getServer().getPluginManager().registerEvents(new PlayerJoinHandler(this, lang), this); - getServer().getPluginManager().registerEvents(new PlayerQuitHandler(this, lang), this); - - if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { - new Placeholder(this).register(); - } - - this.reload(); - } - - @Override - public void onDisable() { - // make sure to unregister the registered channels in case of a reload - getServer().getMessenger().unregisterOutgoingPluginChannel(this); - getServer().getMessenger().unregisterIncomingPluginChannel(this); - } - - /** - * Reloads the plugin configuration and initializes connections. - */ - public void reload() { - saveDefaultConfig(); - reloadConfig(); - Logger.info(Language.getMessage(lang, "plugin-config-loaded")); - Logger.info(Language.getMessage(lang, "plugin-command-executor")); - - usesProxy = getConfig().getBoolean("config.server-behind-proxy", false); - - host = getConfig().getString("config.host"); - port = getConfig().getInt("config.port"); - serverKey = getConfig().getString("config.server-key"); - - if (getConfig().getBoolean("config.auto-reconnect")) - isConnected = reconnect(true); - - int positionTaskInterval = getConfig().getInt("config.voice.position-task-interval", 1); - if (!taskRunner.isCancelled()) - taskRunner.cancel(); - taskRunner = new PositionsTask(this, lang).runTaskTimer(this, 1, positionTaskInterval); - - int proximityDistance = getConfig().getInt("config.voice.proximity-distance"); - Boolean proximityToggle = getConfig().getBoolean("config.voice.proximity-toggle"); - Boolean voiceEffects = getConfig().getBoolean("config.voice.voice-effects"); - - updateSettings(proximityDistance, proximityToggle, voiceEffects); - } - - /** - * Connects to a new server. - * - * @param host The host to connect to. - * @param port The port to connect to. - * @param serverKey The server key. - * @return True if connected successfully, otherwise false. - */ - public Boolean connect(String host, int port, String serverKey) { - if (Objects.nonNull(host) && Objects.nonNull(serverKey)) { - getConfig().set("config.host", host); - getConfig().set("config.port", port); - getConfig().set("config.server-key", serverKey); - saveConfig(); - reloadConfig(); - reload(); - - return isConnected; - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); - return false; - } - } - - /** - * Connects to the server. - * - * @param force Indicates whether to force a connection. - * @return True if connected successfully, otherwise false. - */ - public Boolean reconnect(Boolean force) { - if (isConnected && !force) - return true; - if (isConnected) { - disconnect("Reconnecting to another server."); - } - - if (usesProxy) { - Logger.info(Language.getMessage(lang, "plugin-connect-proxy")); - return false; - } - - if (Objects.nonNull(host) && Objects.nonNull(serverKey)) { - String link = "http://" + host + ":" + port; - String Token = network.sendLoginRequest(link, serverKey); - if (Objects.nonNull(Token)) { - Logger.info(Language.getMessage(lang, "plugin-connect-connected")); - isConnected = true; - token = Token; - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-failed")); - } - return isConnected; - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); - return false; - } - } - - /** - * Disconnects from the server. - * - * @param reason The reason why we disconnected - */ - public void disconnect(String reason) { - if (!isConnected) - return; - - if (Objects.nonNull(host) && Objects.nonNull(serverKey)) { - String link = "http://" + host + ":" + port; - network.sendLogoutRequest(link, token); - isConnected = false; - - String disconnectMessage = Language.getMessage(lang, "plugin-connection-disconnect").replace("$reason", reason); - Logger.info(disconnectMessage); - - boolean sendVoipDisconnectMessage = getConfig().getBoolean("config.voice.send-voip-disconnect-message"); - if (sendVoipDisconnectMessage) { - Bukkit.broadcast(Component.text(disconnectMessage).color(NamedTextColor.YELLOW)); - } - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); - } - } - - /** - * Disconnects from the server. - */ - public void disconnect() { - disconnect("N.A."); - } - - /** - * Binds a player to the voice chat server. - * - * @param playerKey The key associated with the player. - * @param player The player to bind. - * @return True if the binding was successful, otherwise false. - */ - public Boolean bind(int playerKey, Player player, int tries) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey) || usesProxy) - return false; - - if (playerBinds.containsKey(player.getName()) && playerBinds.get(player.getName())) { - return true; - } - - String link = "http://" + host + ":" + port; - - getConfig().set("config.players." + player.getName(), playerKey); - saveConfig(); - - String result = network.sendBindRequest(link, token, playerKey, player.getUniqueId().toString(), - player.getName()); - playerBinds.put(player.getName(), false); - if (result != null) { - if (result == "SUCCESS") { - playerBinds.put(player.getName(), true); - - Logger.info(Language.getMessage(lang, "player-binded").replace("$player",player.getName())); - - boolean sendBindedMessage = getConfig().getBoolean("config.voice.send-binded-message"); - if (sendBindedMessage) { - Bukkit.broadcast( - Component.text(player.getName()).decorate(TextDecoration.BOLD) - .append( - Component.text( - Language.getMessage(lang, "player-binded") - .replace("$player", "") - ) - .color(NamedTextColor.DARK_GREEN) - ) - ); - } - return true; - } else if (result == "Invalid Token!" && tries == 0) { - Logger.info("Invalid Token detected, reconnecting..."); - isConnected = reconnect(true); - return bind(playerKey, player, 1); - } - } - return false; - } - - public Boolean bind(int playerKey, Player player) { - return bind(playerKey, player, 0); - } - - /** - * Bind a fake player - * @param bindKey - * @param name - * @return - */ - public Boolean bindFake(int playerKey, String name, int tries) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return false; - - if (playerBinds.containsKey(name) && playerBinds.get(name)) { - return true; - } - - String link = "http://" + host + ":" + port; - - String result = network.sendBindRequest(link, token, playerKey, String.format("%0", playerKey), name); - playerBinds.put(name, false); - if (result != null) { - if (result == "SUCCESS") { - playerBinds.put(name, true); - - Logger.info(Language.getMessage(lang, "player-binded").replace("$player", name)); - - boolean sendBindedMessage = getConfig().getBoolean("config.voice.send-binded-message"); - if (sendBindedMessage) { - Bukkit.broadcast( - Component.text(name).decorate(TextDecoration.BOLD) - .append( - Component.text( - Language.getMessage(lang, "player-binded") - .replace("$player", "") - ) - .color(NamedTextColor.DARK_GREEN) - ) - ); - } - return true; - } else if (result == "Invalid Token!" && tries == 0) { - Logger.info("Invalid Token detected, reconnecting..."); - isConnected = reconnect(true); - return bindFake(playerKey, name, 1); - } - } - return false; - } - - public Boolean bindFake(int playerKey, String name) { - return bindFake(playerKey, name, 0); - } - - /** - * Disconnects a player from the voice chat server. - * - * @param player The player to disconnect. - * @return True if the disconnection was successful, otherwise false. - */ - public Boolean disconnectPlayer(Player player, int tries) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey) || usesProxy) - return false; - String link = "http://" + host + ":" + port; - - String result = network.sendDisconnectRequest(link, token, player.getUniqueId().toString(), player.getName()); - if (result != null) { - if (result == "SUCCESS") { - playerBinds.remove(player.getName()); - return true; - } else if (result == "Invalid Token!" && tries == 0) { - Logger.info("Invalid Token detected, reconnecting..."); - isConnected = reconnect(true); - return disconnectPlayer(player, 1); - } - } - return false; - } - - public Boolean disconnectPlayer(Player player) { - return disconnectPlayer(player, 0); - } - - /** - * Updates the voice chat settings. - * - * @param proximityDistance Proximity distance setting. - * @param proximityToggle Proximity toggle setting. - * @param voiceEffects Voice effects setting. - * @return True if settings were updated successfully, otherwise false. - */ - public Boolean updateSettings(int proximityDistance, Boolean proximityToggle, Boolean voiceEffects) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey) || usesProxy) - return false; - String link = "http://" + host + ":" + port; - - return network.sendUpdateSettingsRequest(link, token, proximityDistance, proximityToggle, voiceEffects); - } - - public void setNotConnected() { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return; - isConnected = false; - } - - public void saveResource(String resourcePath) { - File outFile = new File(getDataFolder(), resourcePath); - // Default Spigot saveResource gives a warning when the file already exists and - // when you don't override - // Now just skip it if the file exists - if (!outFile.exists()) { - saveResource(resourcePath, false); - } - } -} diff --git a/src/main/java/io/greitan/avion/paper/listeners/PlayerJoinHandler.java b/src/main/java/io/greitan/avion/paper/listeners/PlayerJoinHandler.java deleted file mode 100644 index 61580c7..0000000 --- a/src/main/java/io/greitan/avion/paper/listeners/PlayerJoinHandler.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.greitan.avion.paper.listeners; - -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; - -import io.greitan.avion.paper.GeyserVoice; -import io.greitan.avion.paper.utils.Language; - -import java.util.Objects; - -public class PlayerJoinHandler implements Listener { - - private final GeyserVoice plugin; - private final String lang; - - public PlayerJoinHandler(GeyserVoice plugin, String lang) { - this.plugin = plugin; - this.lang = lang; - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - boolean isConnected = plugin.isConnected(); - Player player = event.getPlayer(); - int playerBindKey = plugin.getConfig().getInt("config.players." + player.getName(), -1); - - if (!plugin.usesProxy && isConnected && Objects.nonNull(playerBindKey) && playerBindKey != -1) { - handleAutoBind(playerBindKey, player); - } - } - - private void handleAutoBind(int playerBindKey, Player player) { - player.sendMessage( - Component.text(Language.getMessage(lang, "plugin-autobind-enabled")).color(NamedTextColor.GREEN) - .append(Component.text(" ")) - .append( - Component.text(Language.getMessage(lang, "plugin-autobind-binding")).color(NamedTextColor.YELLOW) - )); - - boolean isBound = plugin.bind(playerBindKey, player); - - if (!isBound) { - player.sendMessage( - Component.text(Language.getMessage(lang, "plugin-autobind-failed")).color(NamedTextColor.RED)); - } - } - - /* - * @EventHandler - * public void onBroadcastMessage(AsyncChatEvent event) { - * plugin.Logger.log(Component.text("Received message ").append(event.message()) - * ); - * } - */ -} diff --git a/src/main/java/io/greitan/avion/paper/listeners/PluginMessageHandler.java b/src/main/java/io/greitan/avion/paper/listeners/PluginMessageHandler.java deleted file mode 100644 index bfde85a..0000000 --- a/src/main/java/io/greitan/avion/paper/listeners/PluginMessageHandler.java +++ /dev/null @@ -1,73 +0,0 @@ -package io.greitan.avion.paper.listeners; - -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteStreams; -import com.fasterxml.jackson.core.JsonProcessingException; - -import org.bukkit.Server; -import org.bukkit.entity.Player; -import org.bukkit.plugin.messaging.PluginMessageListener; - -import io.greitan.avion.common.network.Payloads.PlayerData; -import io.greitan.avion.paper.GeyserVoice; - -import java.util.List; - -public class PluginMessageHandler implements PluginMessageListener { - - private final GeyserVoice plugin; - public static final String channelName = "geyservoice:main"; - - public PluginMessageHandler(GeyserVoice plugin) { - this.plugin = plugin; - } - - public void sendPlayerData(Player player, PlayerData playerData) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("PlayerData"); - out.writeUTF(playerData.PlayerId); - out.writeUTF(playerData.DimensionId); - out.writeDouble(playerData.Location.x); - out.writeDouble(playerData.Location.y); - out.writeDouble(playerData.Location.z); - out.writeDouble(playerData.Rotation); - out.writeDouble(playerData.EchoFactor); - out.writeBoolean(playerData.Muffled); - out.writeBoolean(playerData.IsDead); - player.sendPluginMessage(plugin, channelName, out.toByteArray()); - } - - public void sendPlayerDataList(Server server, List playerDataList) { - try { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("PlayerDataList"); - out.writeUTF(GeyserVoice.objectMapper.writeValueAsString(playerDataList)); - server.sendPluginMessage(plugin, channelName, out.toByteArray()); - // The response will be handled in onPluginMessageReceived - } catch (JsonProcessingException e) { - } - } - - @Override - public void onPluginMessageReceived(String channel, Player player, byte[] message) { - if (!channel.equals(channelName)) { - return; - } - - ByteArrayDataInput in = ByteStreams.newDataInput(message); - String subchannel = in.readUTF(); - if (subchannel.equals("PlayerBindSync")) { - String username = in.readUTF(); - boolean isBound = in.readBoolean(); - if (plugin.getPlayerBinds().getOrDefault(username, false) != isBound) { - if (isBound) { - plugin.Logger.info(username + " has joined the voicechat!"); - } else { - plugin.Logger.info(username + " has left the voicechat!"); - } - plugin.getPlayerBinds().put(username, isBound); - } - } - } -} diff --git a/src/main/java/io/greitan/avion/paper/tasks/PositionsTask.java b/src/main/java/io/greitan/avion/paper/tasks/PositionsTask.java deleted file mode 100644 index 1423cd0..0000000 --- a/src/main/java/io/greitan/avion/paper/tasks/PositionsTask.java +++ /dev/null @@ -1,237 +0,0 @@ -package io.greitan.avion.paper.tasks; - -import org.bukkit.scheduler.BukkitRunnable; - -import io.greitan.avion.paper.GeyserVoice; -import io.greitan.avion.paper.utils.Language; -import io.greitan.avion.common.network.Payloads.PacketType; -import io.greitan.avion.common.network.Payloads.LocationData; -import io.greitan.avion.common.network.Payloads.PlayerData; -import io.greitan.avion.common.network.Payloads.MCCommPacket; -import io.greitan.avion.common.network.Payloads.UpdatePacket; -import io.greitan.avion.common.network.Payloads.DenyPacket; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; - -import org.bukkit.Bukkit; -import org.bukkit.block.Block; -import org.bukkit.Location; -import org.bukkit.entity.Player; -import org.bukkit.World; -import org.bukkit.util.BlockIterator; -import org.bukkit.util.Vector; - -import java.util.concurrent.TimeUnit; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class PositionsTask extends BukkitRunnable { - private final GeyserVoice plugin; - private final String lang; - private boolean isConnected = false; - private Integer ReconnectRetries = 0; - - public PositionsTask(GeyserVoice plugin, String lang) { - this.plugin = plugin; - this.lang = lang; - } - - @Override - public void run() { - if (plugin.usesProxy) { - isConnected = true; // Only local variable... Needed for CaveEchoFactor - // plugin.getMessageHandler().sendPlayerDataList(plugin.getServer(), - // getPlayerDataList()); - for (Player player : plugin.getServer().getOnlinePlayers()) { - plugin.getMessageHandler().sendPlayerData(player, getPlayerData(player)); - } - return; - } - - isConnected = plugin.isConnected(); - String host = plugin.getHost(); - int port = plugin.getPort(); - String token = plugin.getToken(); - String link = "http://" + host + ":" + port; - - if (isConnected) { - if (host != null && token != null) { - UpdatePacket updatePacket = new UpdatePacket(); - updatePacket.Token = token; - updatePacket.Players = getPlayerDataList(); - - MCCommPacket response = plugin.network.sendPostRequest(link, updatePacket); - if (response != null) { - if (response.PacketId == PacketType.AckUpdate.ordinal()) { - // AckUpdatePacket packetData = plugin.objectMapper.convertValue(response, - // AckUpdatePacket.class); - // You can do stuff with the AckUpdate packet data here... - return; - } else if (response.PacketId == PacketType.Deny.ordinal()) { - DenyPacket packetData = GeyserVoice.objectMapper.convertValue(response, DenyPacket.class); - plugin.Logger.error(packetData.Reason); - if (!packetData.Reason.equals("Invalid Token!")) { - plugin.setNotConnected(); - // http.cancelAll(packetData.Reason); - cancel(); - return; - } - } else { - return; - } - } - if (!isConnected) - return; // do nothing. - - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-lost")); - plugin.setNotConnected(); - - if (plugin.getConfig().getBoolean("config.auto-reconnect")) { - if (plugin.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - Bukkit.broadcast(Component.text(Language.getMessage(lang, "plugin-connection-lost-reconnect")) - .color(NamedTextColor.RED)); - } - ReconnectRetries = 0; - Reconnect(); - return; - } - if (plugin.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - Bukkit.broadcast(Component.text(Language.getMessage(lang, "plugin-connection-lost")) - .color(NamedTextColor.RED)); - } - cancel(); - } - } - } - - public List getPlayerDataList() { - List playerDataList = new ArrayList<>(); - - for (Player player : Bukkit.getServer().getOnlinePlayers()) { - PlayerData playerData = getPlayerData(player); - playerDataList.add(playerData); - } - - return playerDataList; - } - - public PlayerData getPlayerData(Player player) { - Location headLocation = player.getEyeLocation(); - - LocationData locationData = new LocationData(); - locationData.x = headLocation.getX(); - locationData.y = headLocation.getY(); - locationData.z = headLocation.getZ(); - - PlayerData playerData = new PlayerData(); - playerData.PlayerId = player.getUniqueId().toString(); - playerData.DimensionId = getDimensionId(player); - playerData.Location = locationData; - playerData.Rotation = player.getLocation().getYaw(); - - if (player.getWorld().getEnvironment() == World.Environment.NORMAL) { - playerData.EchoFactor = getCaveDensity(player); - } else { - playerData.EchoFactor = 0.0; - } - playerData.Muffled = player.isInWater(); - playerData.IsDead = player.isDead(); - return playerData; - } - - public double getCaveDensity(Player player) { - if (!isConnected) { - return 0.0; - } - - String[] caveBlocks = { - "STONE", - "DIORITE", - "GRANITE", - "DEEPSLATE", - "TUFF" - }; - - int blockCount = 0; - for (int x = -1; x <= 1; x++) { - for (int y = -1; y <= 1; y++) { - for (int z = -1; z <= 1; z++) { - if (x == 0 && y == 0 && z == 0) - continue; // a vector of 0,0,0 won't go anywhere, so skip it... - Vector direction = new Vector(x, y, z); - blockCount += castRayUntilBlock( - new BlockIterator(player.getWorld(), player.getLocation().toVector(), direction, 0, 50), - caveBlocks); - } - } - } - - // (3 * 3 * 3) - 1 = 26.0 - return blockCount / 26.0; // Total blocks checked - } - - private int castRayUntilBlock(BlockIterator blockIterator, String[] caveBlocks) { - while (blockIterator.hasNext()) { - Block block = blockIterator.next(); - if (block.getType().isSolid()) { - if (Arrays.asList(caveBlocks).contains(getBlockType(block))) { - return 1; - } - break; - } - } - return 0; - } - - private String getBlockType(Block block) { - return block.getType().toString(); - } - - private String getDimensionId(Player player) { - String worldName = player.getWorld().getName(); - return switch (worldName) { - case "world" -> "minecraft:overworld"; - case "world_nether" -> "minecraft:nether"; - case "world_the_end" -> "minecraft:the_end"; - default -> "minecraft:unknown"; - }; - } - - private Boolean Reconnect() { - if (ReconnectRetries < 5) { - ReconnectRetries++; - - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-attempt").replace("$attempt", - ReconnectRetries.toString())); - - if (plugin.reconnect(true)) { - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-success")); - - if (plugin.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - Bukkit.broadcast(Component.text(Language.getMessage(lang, "plugin-connection-reconnecting-success")) - .color(NamedTextColor.GREEN)); - } - return true; - } else { - if (ReconnectRetries < 5) { - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-failed-retry")); - try { - TimeUnit.SECONDS.sleep(1); - } catch (Exception e) { - } - return Reconnect(); - } - plugin.Logger.error(Language.getMessage(lang, "plugin-connection-reconnecting-failed")); - - if (plugin.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - Bukkit.broadcast(Component.text(Language.getMessage(lang, "plugin-connection-reconnecting-failed")) - .color(NamedTextColor.RED)); - } - cancel(); - } - } - return false; - } -} diff --git a/src/main/java/io/greitan/avion/velocity/GeyserVoice.java b/src/main/java/io/greitan/avion/velocity/GeyserVoice.java deleted file mode 100644 index 6ad7408..0000000 --- a/src/main/java/io/greitan/avion/velocity/GeyserVoice.java +++ /dev/null @@ -1,457 +0,0 @@ -package io.greitan.avion.velocity; - -import lombok.Getter; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextDecoration; -import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.chat.ComponentBuilder; - -import com.google.inject.Inject; -import com.velocitypowered.api.command.CommandMeta; -import com.velocitypowered.api.command.CommandManager; -import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.plugin.annotation.DataDirectory; -import com.velocitypowered.api.plugin.Plugin; -import com.velocitypowered.api.proxy.ProxyServer; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.scheduler.ScheduledTask; -import com.velocitypowered.api.scheduler.TaskStatus; - -import de.leonhard.storage.Yaml; -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.greitan.avion.common.utils.Constants; -import io.greitan.avion.common.BaseGeyserVoice; -import io.greitan.avion.velocity.commands.VoiceCommand; -import io.greitan.avion.velocity.listeners.*; -import io.greitan.avion.common.network.Network; -import io.greitan.avion.common.network.Payloads.PlayerData; -import io.greitan.avion.velocity.tasks.PositionsTask; -import io.greitan.avion.velocity.utils.*; - -import java.nio.file.Path; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.FileOutputStream; -import java.util.concurrent.TimeUnit; - -import java.util.Objects; -import java.util.Map; -import java.util.HashMap; - -/** - * Main plugin class for GeyserVoice. - */ -@Plugin(id = "geyservoice", name = Constants.NAME, version = Constants.VERSION, description = Constants.DESCRIPTION, authors = { - "Alpha" }, url = Constants.URL) -public class GeyserVoice implements BaseGeyserVoice { - private final @Getter ProxyServer proxy; - private final @Getter File dataFolder; - private static @Getter Yaml config; - - private static @Getter GeyserVoice instance; - private @Getter boolean isConnected = false; - private @Getter String host = ""; - private @Getter int port = 0; - private @Getter String serverKey = ""; - private @Getter Map playerBinds = new HashMap<>(); - private @Getter String token = ""; - private String lang; - private @Getter PluginMessageHandler messageHandler = new PluginMessageHandler(this); - public Map playerDataList = new HashMap<>(); - - private ScheduledTask taskRunner; - private PositionsTask positionsTask = new PositionsTask(this, lang); - - public VelocityLogger Logger = new VelocityLogger(); - public Network network = new Network(Logger); - - public static final ObjectMapper objectMapper = new ObjectMapper(); - - @Inject - public GeyserVoice(final ProxyServer proxy, @DataDirectory Path dataDirectory) { - instance = this; - this.proxy = proxy; - this.dataFolder = dataDirectory.toFile(); - - saveResource("/config.yml"); - config = new Yaml("config", this.dataFolder.toString()); - } - - /** - * Executes upon enabling the plugin. - */ - @Subscribe - public void onProxyInitialization(final ProxyInitializeEvent event) { - Logger.info("Enabling GeyserVoice"); - - lang = getConfig().getString("config.lang"); - int positionTaskInterval = getConfig().getOrDefault("config.voice.position-task-interval", 1); - Language.init(this); - - proxy.getEventManager().register(this, messageHandler); - proxy.getChannelRegistrar().register(PluginMessageHandler.channelName); - - CommandManager commandManager = proxy.getCommandManager(); - CommandMeta commandMeta = commandManager.metaBuilder("voice").aliases("voicecraft").plugin(this).build(); - VoiceCommand voiceCommand = new VoiceCommand(this, lang); - commandManager.register(commandMeta, voiceCommand); - - taskRunner = proxy.getScheduler() - .buildTask(this, () -> { - if (!positionsTask.run()) { - taskRunner.cancel(); - } - }) - .repeat(positionTaskInterval * 50, TimeUnit.MILLISECONDS) // positionTaskInterval is in ticks, 1 tick = - // 0.050 seconds = 50 ms - .schedule(); - - proxy.getEventManager().register(this, new PlayerJoinHandler(this, lang)); - proxy.getEventManager().register(this, new PlayerQuitHandler(this, lang)); - - this.reload(); - } - - /** - * Reloads the plugin configuration and initializes connections. - */ - public void reload() { - saveDefaultConfig(); - reloadConfig(); - Logger.info(Language.getMessage(lang, "plugin-config-loaded")); - Logger.info(Language.getMessage(lang, "plugin-command-executor")); - - host = getConfig().getString("config.host"); - port = getConfig().getInt("config.port"); - serverKey = getConfig().getString("config.server-key"); - - if (getConfig().getBoolean("config.auto-reconnect")) - isConnected = reconnect(true); - - int positionTaskInterval = getConfig().getOrDefault("config.voice.position-task-interval", 1); - if (taskRunner.status() != TaskStatus.CANCELLED) - taskRunner.cancel(); - taskRunner = proxy.getScheduler() - .buildTask(this, () -> { - if (!positionsTask.run()) { - taskRunner.cancel(); - } - }) - .repeat(positionTaskInterval * 50, TimeUnit.MILLISECONDS) // positionTaskInterval is in ticks, 1 tick = - // 0.050 seconds = 50 ms - .schedule(); - - int proximityDistance = getConfig().getInt("config.voice.proximity-distance"); - Boolean proximityToggle = getConfig().getBoolean("config.voice.proximity-toggle"); - Boolean voiceEffects = getConfig().getBoolean("config.voice.voice-effects"); - - updateSettings(proximityDistance, proximityToggle, voiceEffects); - } - - /** - * Connects to a new server. - * - * @param host The host to connect to. - * @param port The port to connect to. - * @param serverKey The server key. - * @return True if connected successfully, otherwise false. - */ - public Boolean connect(String host, int port, String serverKey) { - if (Objects.nonNull(host) && Objects.nonNull(serverKey)) { - getConfig().set("config.host", host); - getConfig().set("config.port", port); - getConfig().set("config.server-key", serverKey); - saveConfig(); - reloadConfig(); - reload(); - - return isConnected; - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); - return false; - } - } - - /** - * Connects to the server. - * - * @param force Indicates whether to force a connection. - * @return True if connected successfully, otherwise false. - */ - public Boolean reconnect(Boolean force) { - if (isConnected && !force) - return true; - if (isConnected) { - disconnect("Reconnecting to another server."); - } - - if (Objects.nonNull(host) && Objects.nonNull(serverKey)) { - String link = "http://" + host + ":" + port; - String Token = network.sendLoginRequest(link, serverKey); - if (Objects.nonNull(Token)) { - Logger.info(Language.getMessage(lang, "plugin-connect-connected")); - isConnected = true; - token = Token; - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-failed")); - } - return isConnected; - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); - return false; - } - } - - /** - * Disconnects from the server. - * - * @param reason The reason why we disconnected - */ - public void disconnect(String reason) { - if (!isConnected) - return; - - if (Objects.nonNull(host) && Objects.nonNull(serverKey)) { - String link = "http://" + host + ":" + port; - network.sendLogoutRequest(link, token); - isConnected = false; - - String disconnectMessage = Language.getMessage(lang, "plugin-connection-disconnect").replace("$reason", reason); - Logger.info(disconnectMessage); - - boolean sendVoipDisconnectMessage = getConfig().getBoolean("config.voice.send-voip-disconnect-message"); - if (sendVoipDisconnectMessage) { - getProxy().sendMessage(Component.text(disconnectMessage).color(NamedTextColor.YELLOW)); - } - } else { - Logger.warn(Language.getMessage(lang, "plugin-connect-invalid-data")); - } - } - - /** - * Disconnects from the server. - */ - public void disconnect() { - disconnect("N.A."); - } - - /** - * Binds a player to the voice chat server. - * - * @param playerKey The key associated with the player. - * @param player The player to bind. - * @return True if the binding was successful, otherwise false. - */ - public Boolean bind(int playerKey, Player player, int tries) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return false; - - if (playerBinds.containsKey(player.getUsername()) && playerBinds.get(player.getUsername())) { - return true; - } - - String link = "http://" + host + ":" + port; - - getConfig().set("config.players." + player.getUsername(), playerKey); - saveConfig(); - - String result = network.sendBindRequest(link, token, playerKey, player.getUniqueId().toString(), - player.getUsername()); - playerBinds.put(player.getUsername(), false); - if (result != null) { - if (result == "SUCCESS") { - playerBinds.put(player.getUsername(), true); - messageHandler.sendPlayerBindSync(player); - - Logger.info(Language.getMessage(lang, "player-binded").replace("$player",player.getUsername())); - - boolean sendBindedMessage = getConfig().getBoolean("config.voice.send-binded-message"); - if (sendBindedMessage) { - getProxy().sendMessage( - Component.text(player.getUsername()).decorate(TextDecoration.BOLD) - .append( - Component.text( - Language.getMessage(lang, "player-binded") - .replace("$player", "") - ) - .color(NamedTextColor.DARK_GREEN) - ) - ); - } - return true; - } else if (result == "Invalid Token!" && tries == 0) { - Logger.info("Invalid Token detected, reconnecting..."); - isConnected = reconnect(true); - return bind(playerKey, player, 1); - } - } - messageHandler.sendPlayerBindSync(player); - return false; - } - - public Boolean bind(int playerKey, Player player) { - return bind(playerKey, player, 0); - } - - /** - * Bind a fake player - * @param bindKey - * @param name - * @return - */ - public Boolean bindFake(int playerKey, String name, int tries) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return false; - - if (playerBinds.containsKey(name) && playerBinds.get(name)) { - return true; - } - - String link = "http://" + host + ":" + port; - - String result = network.sendBindRequest(link, token, playerKey, String.format("%0", playerKey), name); - playerBinds.put(name, false); - if (result != null) { - if (result == "SUCCESS") { - playerBinds.put(name, true); - // messageHandler.sendPlayerBindSync(player); - - Logger.info(Language.getMessage(lang, "player-binded").replace("$player", name)); - - boolean sendBindedMessage = getConfig().getBoolean("config.voice.send-binded-message"); - if (sendBindedMessage) { - getProxy().sendMessage( - Component.text(name).decorate(TextDecoration.BOLD) - .append( - Component.text( - Language.getMessage(lang, "player-binded") - .replace("$player", "") - ) - .color(NamedTextColor.DARK_GREEN) - ) - ); - } - return true; - } else if (result == "Invalid Token!" && tries == 0) { - Logger.info("Invalid Token detected, reconnecting..."); - isConnected = reconnect(true); - return bindFake(playerKey, name, 1); - } - } - // messageHandler.sendPlayerBindSync(player); - return false; - } - - public Boolean bindFake(int playerKey, String name) { - return bindFake(playerKey, name, 0); - } - - /** - * Disconnects a player from the voice chat server. - * - * @param player The player to disconnect. - * @return True if the disconnection was successful, otherwise false. - */ - public Boolean disconnectPlayer(Player player, int tries) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return false; - String link = "http://" + host + ":" + port; - - String result = network.sendDisconnectRequest(link, token, player.getUniqueId().toString(), - player.getUsername()); - if (result != null) { - if (result == "SUCCESS") { - playerBinds.remove(player.getUsername()); - messageHandler.sendPlayerBindSync(player); - return true; - } else if (result == "Invalid Token!" && tries == 0) { - Logger.info("Invalid Token detected, reconnecting..."); - isConnected = reconnect(true); - return disconnectPlayer(player, 1); - } - } - return false; - } - - public Boolean disconnectPlayer(Player player) { - return disconnectPlayer(player, 0); - } - - /** - * Updates the voice chat settings. - * - * @param proximityDistance Proximity distance setting. - * @param proximityToggle Proximity toggle setting. - * @param voiceEffects Voice effects setting. - * @return True if settings were updated successfully, otherwise false. - */ - public Boolean updateSettings(int proximityDistance, Boolean proximityToggle, Boolean voiceEffects) { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return false; - String link = "http://" + host + ":" + port; - - return network.sendUpdateSettingsRequest(link, token, proximityDistance, proximityToggle, voiceEffects); - } - - public void setNotConnected() { - if (!isConnected || Objects.isNull(host) || Objects.isNull(serverKey)) - return; - isConnected = false; - } - - public void reloadConfig() { - config.forceReload(); - } - - public void saveConfig() { - config.write(); - } - - public void saveDefaultConfig() { - config.addDefaultsFromInputStream(getClass().getResourceAsStream("/config.yaml")); - } - - public void saveResource(String resourcePath, boolean replace) { - if (resourcePath == null || resourcePath.equals("")) { - throw new IllegalArgumentException("ResourcePath cannot be null or empty"); - } - - resourcePath = resourcePath.replace("\\", "/"); - InputStream in = getClass().getResourceAsStream(resourcePath); - if (in == null) { - throw new IllegalArgumentException("The embedded resource '" + resourcePath + "' cannot be found"); - } - - File outFile = new File(getDataFolder(), resourcePath); - int lastIndex = resourcePath.lastIndexOf("/"); - File outDir = new File(dataFolder, resourcePath.substring(0, lastIndex >= 0 ? lastIndex : 0)); - - if (!outDir.exists()) { - outDir.mkdirs(); - } - - try { - if (!outFile.exists() || replace) { - OutputStream out = new FileOutputStream(outFile); - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - out.close(); - in.close(); - } - } catch (IOException ex) { - Logger.error("Could not save " + outFile.getName() + " to " + outFile); - } - } - - public void saveResource(String resourcePath) { - saveResource(resourcePath, false); - } -} diff --git a/src/main/java/io/greitan/avion/velocity/commands/VoiceCommand.java b/src/main/java/io/greitan/avion/velocity/commands/VoiceCommand.java deleted file mode 100644 index d207679..0000000 --- a/src/main/java/io/greitan/avion/velocity/commands/VoiceCommand.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.greitan.avion.velocity.commands; - -import com.velocitypowered.api.command.CommandSource; -import com.velocitypowered.api.command.SimpleCommand; -import com.velocitypowered.api.proxy.Player; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; - -import io.greitan.avion.velocity.GeyserVoice; -import io.greitan.avion.velocity.utils.Language; -import io.greitan.avion.common.commands.BaseVoiceCommand; -import io.greitan.avion.common.utils.IntegerOperation; -import io.greitan.avion.common.utils.StringOperation; -import io.greitan.avion.common.utils.DoubleStringOperation; -import io.greitan.avion.common.utils.EmptyOperation; - -import java.util.List; - -public final class VoiceCommand implements SimpleCommand { - - private final BaseVoiceCommand voiceCommand; - private final GeyserVoice plugin; - private final String lang; - - // Get the plugin and lang interfaces. - public VoiceCommand(GeyserVoice plugin, String lang) { - this.voiceCommand = new BaseVoiceCommand(plugin); - this.plugin = plugin; - this.lang = lang; - } - - @Override - public void execute(final Invocation invocation) { - CommandSource sender = invocation.source(); - // Get the arguments after the command alias - String[] args = invocation.arguments(); - - this.voiceCommand.onCommand( - args, - plugin.isConnected(), - sender instanceof Player, - new StringOperation() { - @Override - public boolean execute(String permission) { - if (sender instanceof Player) - return sender.hasPermission(permission); - else - return true; - } - }, - new DoubleStringOperation() { - @Override - public void execute(String text, String rawColor) { - NamedTextColor color = NamedTextColor.RED; - if (rawColor == "red") color = NamedTextColor.RED; - else if (rawColor == "aqua") color = NamedTextColor.AQUA; - else if (rawColor == "green") color = NamedTextColor.GREEN; - else if (rawColor == "yellow") color = NamedTextColor.YELLOW; - - var message = Component.text(Language.getMessage(lang, text)).color(color); - if (sender instanceof Player) - sender.sendMessage(message); - else - plugin.Logger.log(message); - } - }, - new IntegerOperation() { - @Override - public boolean execute(int key) { - if (sender instanceof Player) { - Player player = (Player) sender; - return plugin.bind(key, player); - } - return false; - } - }, - new EmptyOperation() { - @Override - public boolean execute() { - if (sender instanceof Player) { - Player player = (Player) sender; - GeyserVoice.getConfig().set("config.players." + player.getUsername(), null); - return true; - } - return false; - } - } - ); - } - - @Override - public boolean hasPermission(final Invocation invocation) { - return invocation.source().hasPermission("voice.cmd"); - } - - @Override - public List suggest(final Invocation invocation) { - String[] args = invocation.arguments(); - return voiceCommand.onTabComplete(args, new StringOperation() { - @Override - public boolean execute(String permission) { - return invocation.source().hasPermission(permission); - } - }); - } -} diff --git a/src/main/java/io/greitan/avion/velocity/listeners/PlayerJoinHandler.java b/src/main/java/io/greitan/avion/velocity/listeners/PlayerJoinHandler.java deleted file mode 100644 index 9bb0108..0000000 --- a/src/main/java/io/greitan/avion/velocity/listeners/PlayerJoinHandler.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.greitan.avion.velocity.listeners; - -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.connection.PostLoginEvent; -import com.velocitypowered.api.event.player.ServerPostConnectEvent; -import io.greitan.avion.velocity.GeyserVoice; -import io.greitan.avion.velocity.utils.Language; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; - -import java.util.Objects; - -public class PlayerJoinHandler { - - private final GeyserVoice plugin; - private final String lang; - - public PlayerJoinHandler(GeyserVoice plugin, String lang) { - this.plugin = plugin; - this.lang = lang; - } - - @Subscribe - public void onPlayerJoin(PostLoginEvent event) { - boolean isConnected = plugin.isConnected(); - Player player = event.getPlayer(); - int playerBindKey = GeyserVoice.getConfig().getOrDefault("config.players." + player.getUsername(), -1); - - if (isConnected && Objects.nonNull(playerBindKey) && playerBindKey != -1) { - handleAutoBind(playerBindKey, player); - } - } - - @Subscribe - public void onPlayerConnect(ServerPostConnectEvent event) { - Player player = event.getPlayer(); - // Just send the message again, in case it didn't got send before... - plugin.getMessageHandler().sendPlayerBindSync(player); - } - - private void handleAutoBind(int playerBindKey, Player player) { - player.sendMessage( - Component.text(Language.getMessage(lang, "plugin-autobind-enabled")).color(NamedTextColor.GREEN) - .append(Component.text(" ")) - .append( - Component.text(Language.getMessage(lang, "plugin-autobind-binding")).color(NamedTextColor.YELLOW) - )); - - boolean isBound = plugin.bind(playerBindKey, player); - - if (!isBound) { - player.sendMessage( - Component.text(Language.getMessage(lang, "plugin-autobind-failed")).color(NamedTextColor.RED)); - } - } -} diff --git a/src/main/java/io/greitan/avion/velocity/listeners/PluginMessageHandler.java b/src/main/java/io/greitan/avion/velocity/listeners/PluginMessageHandler.java deleted file mode 100644 index 463a235..0000000 --- a/src/main/java/io/greitan/avion/velocity/listeners/PluginMessageHandler.java +++ /dev/null @@ -1,130 +0,0 @@ -package io.greitan.avion.velocity.listeners; - -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteStreams; -import com.fasterxml.jackson.core.JsonProcessingException; - -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.server.RegisteredServer; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.event.connection.PluginMessageEvent; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; -import io.greitan.avion.velocity.GeyserVoice; -import io.greitan.avion.common.network.Payloads.PlayerData; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -public class PluginMessageHandler { - - private final GeyserVoice plugin; - public static final MinecraftChannelIdentifier channelName = MinecraftChannelIdentifier.from("geyservoice:main"); - - public PluginMessageHandler(GeyserVoice plugin) { - this.plugin = plugin; - } - - public Boolean sendPlayerBindSync(Player player) { - boolean isBound = plugin.isConnected() && plugin.getPlayerBinds().getOrDefault(player.getUsername(), false); - - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("PlayerBindSync"); - out.writeUTF(player.getUsername()); - out.writeBoolean(isBound); - - // plugin.getProxy().sendMessage(Component.text("####PlayerBindSync####" + - // player.getUsername() + "####" + isBound + "####")); - return trySendMessage(player, out); - } - - public Boolean trySendMessage(Player player, ByteArrayDataOutput out) { - Optional connection = player.getCurrentServer(); - if (connection.isPresent()) { - // First we try using our player - try { - if (connection.get().sendPluginMessage(channelName, out.toByteArray())) { - return true; - } - } catch (Exception e) { - } - try { - // Else we try using any connected player of this server - if (connection.get().getServer().sendPluginMessage(channelName, out.toByteArray())) { - return true; - } - } catch (Exception e) { - } - } - // Else we try any other random player... which we shouldn't do btw... - for (Player otherPlayer : plugin.getProxy().getAllPlayers()) { - connection = otherPlayer.getCurrentServer(); - if (connection.isPresent()) { - try { - if (connection.get().sendPluginMessage(channelName, out.toByteArray())) { - return true; - } - } catch (Exception e) { - } - } - } - // At last we just use any server... - for (RegisteredServer server : plugin.getProxy().getAllServers()) { - server.sendPluginMessage(channelName, out.toByteArray()); - } - return true; - // return - // plugin.getProxy().getAllServers().iterator().next().sendPluginMessage(channelName, - // out.toByteArray()); - } - - @Subscribe() - public void onMessageReceived(PluginMessageEvent event) { - // Ensure the identifier is what you expect before trying to handle the data - if (event.getIdentifier() != channelName) { - return; - } - - if (!(event.getSource() instanceof ServerConnection)) { - return; - } - ServerConnection backend = (ServerConnection) event.getSource(); - - ByteArrayDataInput in = ByteStreams.newDataInput(event.getData()); - String subchannel = in.readUTF(); - String serverName = backend.getServerInfo().getName(); - if (subchannel.equals("PlayerDataList")) { - String rawPlayerDataList = in.readUTF(); - plugin.Logger.debug("Received playerdatalist: " + rawPlayerDataList); - try { - List playerDataList = Arrays - .asList(GeyserVoice.objectMapper.readValue(rawPlayerDataList, PlayerData[].class)); - for (PlayerData playerData : playerDataList) { - playerData.DimensionId = serverName + "_" + playerData.DimensionId; - plugin.playerDataList.put(playerData.PlayerId, playerData); - } - } catch (JsonProcessingException e) { - } - } else if (subchannel.equals("PlayerData")) { - PlayerData playerData = new PlayerData(); - playerData.PlayerId = in.readUTF(); - playerData.DimensionId = in.readUTF(); - playerData.Location.x = in.readDouble(); - playerData.Location.y = in.readDouble(); - playerData.Location.z = in.readDouble(); - playerData.Rotation = in.readDouble(); - playerData.EchoFactor = in.readDouble(); - playerData.Muffled = in.readBoolean(); - playerData.IsDead = in.readBoolean(); - - playerData.DimensionId = serverName + "_" + playerData.DimensionId; - plugin.playerDataList.put(playerData.PlayerId, playerData); - } - - // Make sure to set the result to Handled, else the player will also receive our - // messages... - event.setResult(PluginMessageEvent.ForwardResult.handled()); - } -} diff --git a/src/main/java/io/greitan/avion/velocity/tasks/PositionsTask.java b/src/main/java/io/greitan/avion/velocity/tasks/PositionsTask.java deleted file mode 100644 index ae7bed1..0000000 --- a/src/main/java/io/greitan/avion/velocity/tasks/PositionsTask.java +++ /dev/null @@ -1,134 +0,0 @@ -package io.greitan.avion.velocity.tasks; - -import io.greitan.avion.velocity.GeyserVoice; -import io.greitan.avion.velocity.utils.Language; -import io.greitan.avion.common.network.Payloads.PacketType; -import io.greitan.avion.common.network.Payloads.PlayerData; -import io.greitan.avion.common.network.Payloads.MCCommPacket; -import io.greitan.avion.common.network.Payloads.UpdatePacket; -import io.greitan.avion.common.network.Payloads.DenyPacket; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; - -import java.util.concurrent.TimeUnit; -import java.util.Map; -import java.util.ArrayList; -import java.util.List; - -public class PositionsTask { - private final GeyserVoice plugin; - private final String lang; - private boolean isConnected = false; - private Integer ReconnectRetries = 0; - - public PositionsTask(GeyserVoice plugin, String lang) { - this.plugin = plugin; - this.lang = lang; - } - - public Boolean run() { - isConnected = plugin.isConnected(); - String host = plugin.getHost(); - int port = plugin.getPort(); - String token = plugin.getToken(); - String link = "http://" + host + ":" + port; - - if (isConnected) { - if (host != null && token != null) { - UpdatePacket updatePacket = new UpdatePacket(); - updatePacket.Token = token; - updatePacket.Players = getPlayerDataList(plugin.playerDataList); - - MCCommPacket response = plugin.network.sendPostRequest(link, updatePacket); - if (response != null) { - if (response.PacketId == PacketType.AckUpdate.ordinal()) { - // AckUpdatePacket packetData = plugin.objectMapper.convertValue(response, - // AckUpdatePacket.class); - // You can do stuff with the AckUpdate packet data here... - return true; - } else if (response.PacketId == PacketType.Deny.ordinal()) { - DenyPacket packetData = GeyserVoice.objectMapper.convertValue(response, DenyPacket.class); - plugin.Logger.error(packetData.Reason); - if (!packetData.Reason.equals("Invalid Token!")) { - plugin.setNotConnected(); - // http.cancelAll(packetData.Reason); - // cancel(); - return false; - } - } else { - return false; - } - } - if (!isConnected) - return true; // do nothing. - - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-lost")); - plugin.setNotConnected(); - - if (GeyserVoice.getConfig().getBoolean("config.auto-reconnect")) { - if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - plugin.getProxy().sendMessage( - Component.text(Language.getMessage(lang, "plugin-connection-lost-reconnect")) - .color(NamedTextColor.RED)); - } - ReconnectRetries = 0; - return Reconnect(); - } - if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - plugin.getProxy().sendMessage(Component.text(Language.getMessage(lang, "plugin-connection-lost")) - .color(NamedTextColor.RED)); - } - return false; - } - } - return true; - } - - public List getPlayerDataList(Map allPlayerDataList) { - List playerDataList = new ArrayList<>(); - - for (String playerId : allPlayerDataList.keySet()) { - playerDataList.add(allPlayerDataList.get(playerId)); - } - - return playerDataList; - } - - private Boolean Reconnect() { - if (ReconnectRetries < 5) { - ReconnectRetries++; - - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-attempt").replace("$attempt", - ReconnectRetries.toString())); - - if (plugin.reconnect(true)) { - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-success")); - - if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - plugin.getProxy().sendMessage( - Component.text(Language.getMessage(lang, "plugin-connection-reconnecting-success")) - .color(NamedTextColor.GREEN)); - } - return true; - } else { - if (ReconnectRetries < 5) { - plugin.Logger.warn(Language.getMessage(lang, "plugin-connection-reconnecting-failed-retry")); - try { - TimeUnit.SECONDS.sleep(1); - } catch (Exception e) { - } - return Reconnect(); - } - plugin.Logger.error(Language.getMessage(lang, "plugin-connection-reconnecting-failed")); - - if (GeyserVoice.getConfig().getBoolean("config.voice.send-connection-lost-message")) { - plugin.getProxy().sendMessage( - Component.text(Language.getMessage(lang, "plugin-connection-reconnecting-failed")) - .color(NamedTextColor.RED)); - } - } - } - return false; - } -} diff --git a/src/main/java/io/greitan/avion/velocity/utils/Language.java b/src/main/java/io/greitan/avion/velocity/utils/Language.java deleted file mode 100644 index 3f2d8df..0000000 --- a/src/main/java/io/greitan/avion/velocity/utils/Language.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.greitan.avion.velocity.utils; - -import de.leonhard.storage.Yaml; - -import io.greitan.avion.velocity.GeyserVoice; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -public class Language { - private static final Map languageConfigs = new HashMap<>(); - private static String defaultLanguage = "en"; - - public static void init(GeyserVoice plugin) { - File languageFolder = new File(plugin.getDataFolder(), "locale"); - - if (!languageFolder.exists()) { - languageFolder.mkdirs(); - plugin.saveResource("/locale/en.yml"); - plugin.saveResource("/locale/ru.yml"); - plugin.saveResource("/locale/nl.yml"); - plugin.saveResource("/locale/ja.yml"); - } - - loadLanguages(languageFolder.getAbsolutePath()); - } - - private static void loadLanguages(String pluginFolder) { - File languageFolder = new File(pluginFolder); - - if (languageFolder.exists() && languageFolder.isDirectory()) { - - for (File file : languageFolder.listFiles()) { - - if (file.getName().endsWith(".yml")) { - String language = file.getName().replace(".yml", ""); - Yaml config = new Yaml(language, pluginFolder); - languageConfigs.put(language, config); - } - } - } - } - - public static String getMessage(String language, String key) { - if (languageConfigs.containsKey(language)) { - Yaml config = languageConfigs.get(language); - if (config.contains("messages." + key)) { - return config.getString("messages." + key); - } - } - return languageConfigs.get(defaultLanguage).getString("messages." + key); - } -} diff --git a/src/main/java/io/greitan/avion/velocity/utils/Placeholder.java b/src/main/java/io/greitan/avion/velocity/utils/Placeholder.java deleted file mode 100644 index 40fa319..0000000 --- a/src/main/java/io/greitan/avion/velocity/utils/Placeholder.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.greitan.avion.velocity.utils; - -import org.bukkit.entity.Player; - -import io.greitan.avion.common.utils.BasePlaceholder; -import io.greitan.avion.velocity.GeyserVoice; - -public class Placeholder extends BasePlaceholder { - private final GeyserVoice plugin; - - // Get the plugin interface. - public Placeholder(GeyserVoice plugin) { - this.plugin = plugin; - } - - @Override - public String onPlaceholderRequest(Player player, String identifier) { - // Voice icon placeholder "%voice_status%" - if (identifier.equalsIgnoreCase("status")) { - if (plugin.getPlayerBinds().getOrDefault(player.getName(), false)) { - return GeyserVoice.getConfig().getString("config.voice.in-voice-symbol"); - } else { - return GeyserVoice.getConfig().getString("config.voice.not-in-voice-symbol"); - } - } - return null; - } -} diff --git a/src/main/java/io/greitan/avion/velocity/utils/VelocityLogger.java b/src/main/java/io/greitan/avion/velocity/utils/VelocityLogger.java deleted file mode 100644 index 44691da..0000000 --- a/src/main/java/io/greitan/avion/velocity/utils/VelocityLogger.java +++ /dev/null @@ -1,96 +0,0 @@ -package io.greitan.avion.velocity.utils; - -import com.velocitypowered.api.proxy.ConsoleCommandSource; - -import io.greitan.avion.common.utils.BaseLogger; -import io.greitan.avion.velocity.GeyserVoice; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextDecoration; - -public class VelocityLogger extends BaseLogger { - - public void log(Component msg) { - ConsoleCommandSource console = GeyserVoice.getInstance().getProxy().getConsoleCommandSource(); - Component coloredLogo = Component.text("[") - .color(NamedTextColor.WHITE) - .decorate(TextDecoration.BOLD) - .append(Component.text("GeyserVoice") - .color(NamedTextColor.LIGHT_PURPLE) - .decorate(TextDecoration.BOLD)) - .append(Component.text("] ") - .color(NamedTextColor.WHITE) - .decorate(TextDecoration.BOLD)) - .append(msg); - - console.sendMessage(coloredLogo); - } - - public void info(String msg) { - ConsoleCommandSource console = GeyserVoice.getInstance().getProxy().getConsoleCommandSource(); - Component coloredLogo = Component.text("[") - .color(NamedTextColor.WHITE) - .decorate(TextDecoration.BOLD) - .append(Component.text("GeyserVoice") - .color(NamedTextColor.LIGHT_PURPLE) - .decorate(TextDecoration.BOLD)) - .append(Component.text("] ") - .color(NamedTextColor.WHITE) - .decorate(TextDecoration.BOLD)) - .append(Component.text(msg).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD)); - - console.sendMessage(coloredLogo); - } - - public void warn(String msg) { - ConsoleCommandSource console = GeyserVoice.getInstance().getProxy().getConsoleCommandSource(); - Component coloredLogo = Component.text("[") - .color(NamedTextColor.WHITE) - .decorate(TextDecoration.BOLD) - .append(Component.text("GeyserVoice") - .color(NamedTextColor.LIGHT_PURPLE) - .decorate(TextDecoration.BOLD)) - .append(Component.text("] ") - .color(NamedTextColor.WHITE) - .decorate(TextDecoration.BOLD)) - .append(Component.text(msg).color(NamedTextColor.YELLOW).decorate(TextDecoration.BOLD)); - - console.sendMessage(coloredLogo); - } - - public void error(String msg) { - ConsoleCommandSource console = GeyserVoice.getInstance().getProxy().getConsoleCommandSource(); - Component coloredLogo = Component.text("[") - .color(NamedTextColor.WHITE) - .decorate(TextDecoration.BOLD) - .append(Component.text("GeyserVoice") - .color(NamedTextColor.LIGHT_PURPLE) - .decorate(TextDecoration.BOLD)) - .append(Component.text("] ") - .color(NamedTextColor.WHITE) - .decorate(TextDecoration.BOLD)) - .append(Component.text(msg).color(NamedTextColor.RED).decorate(TextDecoration.BOLD)); - - console.sendMessage(coloredLogo); - } - - public void debug(String msg) { - ConsoleCommandSource console = GeyserVoice.getInstance().getProxy().getConsoleCommandSource(); - Boolean isDebug = GeyserVoice.getConfig().getBoolean("config.debug"); - if (isDebug) { - Component coloredLogo = Component.text("[") - .color(NamedTextColor.WHITE) - .decorate(TextDecoration.BOLD) - .append(Component.text("GeyserVoice") - .color(NamedTextColor.LIGHT_PURPLE) - .decorate(TextDecoration.BOLD)) - .append(Component.text("] ") - .color(NamedTextColor.WHITE) - .decorate(TextDecoration.BOLD)) - .append(Component.text(msg).color(NamedTextColor.BLUE) - .decorate(TextDecoration.BOLD)); - - console.sendMessage(coloredLogo); - } - } -} diff --git a/src/main/legacy/velocity-plugin.json b/src/main/legacy/velocity-plugin.json deleted file mode 100644 index 639198a..0000000 --- a/src/main/legacy/velocity-plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "geyservoice", - "name": "${project.artifactId}", - "version": "${project.version}", - "description": "", - "authors": ["Alpha"], - "main": "io.greitan.avion.velocity.GeyserVoice" -} diff --git a/src/main/resource-templates/bungee.yml b/src/main/resource-templates/bungee.yml deleted file mode 100644 index 48ba029..0000000 --- a/src/main/resource-templates/bungee.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: {{ name }} - -version: {{ version }} - -main: io.greitan.avion.bungeecord.GeyserVoice -description: {{ description }} - -author: Alpha -website: {{ url }} - -prefix: {{ name }} - -commands: - voice: - description: Voice chat main command. - usage: /voice - permission: voice.cmd - -permissions: - voice.cmd: - default: true - voice.connect: - default: op - voice.reconnect: - default: op - voice.disconnect: - default: op - voice.settings: - default: op - voice.bind: - default: true - voice.bindfake: - default: op - voice.reload: - default: op diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml deleted file mode 100644 index a657ca5..0000000 --- a/src/main/resources/config.yml +++ /dev/null @@ -1,32 +0,0 @@ -config: - debug: false # Debug mode: enabled (true) or disabled (false) - - lang: "en" # Language used in the plugin - - host: localhost # Server host the plugin interacts with - port: 9050 # Port for server interaction - server-key: "key" # Server key for authentication - - auto-reconnect: true # Enable/disable auto reconnection when connection lost - - server-behind-proxy: false # Is this paper server behind a proxy (velocity/bungeecord) with this plugin installed: true/false - - voice: - proximity-distance: 30 # Maximum distance for voice interaction - proximity-toggle: true # Enable/disable proximity feature in voice chat - voice-effects: true # Enable/disable voice chat sound effects - - not-in-voice-symbol: "\u2727" # Symbol for a player not in voice chat (placeholder %voice_status%) - in-voice-symbol: "\u2726" # Symbol for a player in voice chat - - send-binded-message: true # Send message when a player connects to voice chat - send-disconnect-message: true # Send message when a player disconnects from voice chat - send-voip-disconnect-message: true # Send message when the server connection is closed - send-connection-lost-message: true # Send message when the connection is lost from the voice chat server - - # The speed and accuracy of player positioning in voice chat depends on this. - # If you don't get errors - don't change this. - position-task-interval: 1 # Interval for player position update task in ticks - - players: - AlphaBaqpla: 228 # Some plugin data or values related to players