2 Commits
Author SHA1 Message Date
asminme 8bde5778e2 pl language fix
Build AsminVoicechat / build-and-release (push) Successful in 2m25s
2026-10-24 15:58:28 +02:00
asminme 71396a86a0 asminvoicechat second commit
Build AsminVoicechat / build-and-release (push) Canceled after 12s
2026-10-24 15:42:08 +02:00
583 changed files with 9049 additions and 38642 deletions

No files matched your search

+23 -119
View File
@@ -13,25 +13,27 @@ concurrency:
cancel-in-progress: false
jobs:
prepare-release:
build-and-release:
runs-on: docker-builder
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
release_id: ${{ steps.release.outputs.release_id }}
steps:
- name: Checkout repository and tags
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Java 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- name: Calculate next version
id: version
shell: bash
run: |
latest="$(git tag --list 'v[0-9]*' --sort=-version:refname | head -n 1)"
if [[ -z "$latest" ]]; then
version="1.0.0"
version="1.0.1"
else
current="${latest#v}"
IFS='.' read -r major minor patch <<< "$current"
@@ -44,134 +46,36 @@ jobs:
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=v$version" >> "$GITHUB_OUTPUT"
- name: Create Gitea release
id: release
- name: Build plugin and run crypto test
shell: bash
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
chmod +x ./gradlew
./gradlew --no-daemon clean build -PbuildVersion="$VERSION"
- name: Create Gitea release and upload plugin
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
VERSION: ${{ steps.version.outputs.version }}
TAG: ${{ steps.version.outputs.tag }}
shell: bash
run: |
set -euo pipefail
if [[ -z "$GITEA_TOKEN" ]]; then
echo "Add a repository write token as the GITEA_TOKEN secret."
exit 1
fi
jar="build/libs/AsminVoiceChat-${VERSION}.jar"
api="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY"
release="$(curl --fail-with-body -sS -X POST "$api/releases" \
-H "Authorization: token $GITEA_TOKEN" \
-H 'Content-Type: application/json' \
-d "{\"tag_name\":\"$TAG\",\"name\":\"AsminVoiceChat $VERSION\",\"body\":\"Dual-protocol Simple Voice Chat server release.\",\"draft\":false,\"prerelease\":false}")"
-d "{\"tag_name\":\"$TAG\",\"name\":\"AsminVoiceChat $VERSION\",\"body\":\"AsminVoiceChat $VERSION\",\"draft\":false,\"prerelease\":false}")"
release_id="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<< "$release")"
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
build-and-test:
needs: prepare-release
runs-on: docker-builder
strategy:
fail-fast: false
matrix:
java:
- '16'
- '17'
- '18'
- '19'
- '20'
- '21'
- '22'
- '23'
- '24'
- '25'
- '26'
- '27-ea'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Java ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java }}
- name: Download 2.6.25 base and compile dependencies
shell: bash
run: |
set -euo pipefail
mkdir -p build/deps build/classes build/test-classes
hangar="https://hangarcdn.papermc.io/plugins/henkelmax/SimpleVoiceChat/versions/bukkit-2.6.25/PAPER/voicechat-bukkit-2.6.25.jar"
curl --fail --location --silent --show-error "$hangar" -o build/deps/voicechat-bukkit-2.6.25.jar
repo="https://repo.papermc.io/repository/maven-public"
curl --fail --location --silent --show-error "$repo/io/papermc/paper/paper-api/1.17.1-R0.1-SNAPSHOT/paper-api-1.17.1-R0.1-20220414.034903-311.jar" -o build/deps/paper-api.jar
curl --fail --location --silent --show-error "$repo/net/md-5/bungeecord-chat/1.16-R0.4/bungeecord-chat-1.16-R0.4.jar" -o build/deps/bungeecord-chat.jar
curl --fail --location --silent --show-error "$repo/net/kyori/adventure-api/4.9.3/adventure-api-4.9.3.jar" -o build/deps/adventure-api.jar
curl --fail --location --silent --show-error "$repo/net/kyori/adventure-key/4.9.3/adventure-key-4.9.3.jar" -o build/deps/adventure-key.jar
curl --fail --location --silent --show-error "$repo/net/kyori/examination-api/1.3.0/examination-api-1.3.0.jar" -o build/deps/examination-api.jar
curl --fail --location --silent --show-error "$repo/net/kyori/examination-string/1.3.0/examination-string-1.3.0.jar" -o build/deps/examination-string.jar
curl --fail --location --silent --show-error "$repo/org/jetbrains/annotations/22.0.0/annotations-22.0.0.jar" -o build/deps/annotations.jar
curl --fail --location --silent --show-error "$repo/io/netty/netty-buffer/4.1.63.Final/netty-buffer-4.1.63.Final.jar" -o build/deps/netty-buffer.jar
curl --fail --location --silent --show-error "$repo/io/netty/netty-common/4.1.63.Final/netty-common-4.1.63.Final.jar" -o build/deps/netty-common.jar
- name: Compile Java 8-compatible patch classes
env:
VERSION: ${{ needs.prepare-release.outputs.version }}
shell: bash
run: |
set -euo pipefail
cp="build/deps/voicechat-bukkit-2.6.25.jar:$(printf '%s:' build/deps/*.jar)"
javac --release 8 -cp "$cp" -d build/classes \
source/patch/de/maxhenkel/voicechat/voice/common/Secret.java \
source/patch/de/maxhenkel/voicechat/voice/server/ServerVoiceEvents.java
sed "s/^version:.*/version: '$VERSION'/" plugin.yml > build/plugin.yml
python3 - <<'PY'
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile
base = Path("build/deps/voicechat-bukkit-2.6.25.jar")
target = Path("build/AsminVoiceChat-${{ needs.prepare-release.outputs.version }}.jar")
replacements = {
"de/maxhenkel/voicechat/voice/common/Secret.class": Path("build/classes/de/maxhenkel/voicechat/voice/common/Secret.class"),
"de/maxhenkel/voicechat/voice/server/ServerVoiceEvents.class": Path("build/classes/de/maxhenkel/voicechat/voice/server/ServerVoiceEvents.class"),
"plugin.yml": Path("build/plugin.yml"),
}
with ZipFile(base) as source, ZipFile(target, "w", ZIP_DEFLATED) as output:
for item in source.infolist():
replacement = replacements.get(item.filename)
output.writestr(item, replacement.read_bytes() if replacement else source.read(item.filename))
print(f"Built {target}")
PY
- name: Run CBC/GCM interoperability test
shell: bash
run: |
set -euo pipefail
cp="build/AsminVoiceChat-${{ needs.prepare-release.outputs.version }}.jar:build/deps/netty-buffer.jar:build/deps/netty-common.jar"
javac --release 8 -cp "$cp" -d build/test-classes tests/ProtocolCryptoSelfTest.java
java -cp "build/test-classes:$cp" ProtocolCryptoSelfTest
- name: Upload JVM-specific build to release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
RELEASE_ID: ${{ needs.prepare-release.outputs.release_id }}
VERSION: ${{ needs.prepare-release.outputs.version }}
JAVA_VERSION: ${{ matrix.java }}
shell: bash
run: |
set -euo pipefail
api="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY"
file="build/AsminVoiceChat-${VERSION}.jar"
asset="AsminVoiceChat-${VERSION}-Java${JAVA_VERSION}.jar"
curl --fail-with-body -sS -X POST "$api/releases/$RELEASE_ID/assets?name=$asset" \
curl --fail-with-body -sS -X POST "$api/releases/$release_id/assets?name=AsminVoiceChat-${VERSION}.jar" \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$file;filename=$asset"
if [[ "$JAVA_VERSION" == "21" ]]; then
curl --fail-with-body -sS -X POST "$api/releases/$RELEASE_ID/assets?name=AsminVoiceChat-${VERSION}.jar" \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$file;filename=AsminVoiceChat-${VERSION}.jar"
fi
-F "attachment=@$jar;filename=AsminVoiceChat-${VERSION}.jar"
+5
View File
@@ -0,0 +1,5 @@
.gradle/
build/
*.class
*.log
/LOCAL_AI_NOTES.md
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
Copyright (c) Max Henkel - All Rights Reserved
+4 -2
View File
@@ -3,6 +3,7 @@
Simple Voice Chat server fork based on 2.6.25. It accepts protocol compatibility
18 (2.5.x clients) and 20 (2.6.x clients) at the same time. Clients remain
unmodified; each player still needs a mod release for their Minecraft version.
The plugin is compiled to Java 8 bytecode and runs on newer JVMs.
## Architecture
@@ -12,8 +13,9 @@ uses AES-CBC for 2.5 clients and AES-GCM for 2.6 clients. Shared packet layouts
are passed through the existing 2.6 voice routing. The fork changes only the
per-client cipher selection and compatibility-version acceptance.
The full source is in `source/decompiled/`. The two changed classes are also
available separately in `source/patch/`.
The Bukkit plugin source and API source are in `source/bukkit/` and
`source/api/`. The cipher and compatibility changes are in the regular source
files for those modules.
## Goxy
+160
View File
@@ -0,0 +1,160 @@
import groovy.json.JsonSlurper
plugins {
id 'java'
id 'com.gradleup.shadow' version '9.6.0'
}
group = 'de.maxhenkel.voicechat'
version = findProperty('buildVersion') ?: '1.0.1'
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
repositories {
mavenCentral()
maven { url = 'https://hub.spigotmc.org/nexus/content/groups/public/' }
maven { url = 'https://libraries.minecraft.net/' }
maven { url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/' }
maven { url = 'https://maven.maxhenkel.de/repository/public' }
maven { url = 'https://repo.viaversion.com/' }
}
configurations {
shadow
}
dependencies {
compileOnly 'org.bukkit:bukkit:1.8.8-R0.1-SNAPSHOT'
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
compileOnly 'me.clip:placeholderapi:2.12.2'
compileOnly 'com.viaversion:viaversion-bukkit:5.7.2'
compileOnly 'com.viaversion:viaversion-common:5.7.2'
implementation 'io.netty:netty-all:4.1.132.Final'
implementation 'org.json:json:20251224'
shadow 'org.json:json:20251224'
implementation 'me.lucko:commodore:2.2'
shadow 'me.lucko:commodore:2.2'
implementation 'org.concentus:Concentus:1.0.1'
shadow 'org.concentus:Concentus:1.0.1'
implementation 'de.maxhenkel.configbuilder:configbuilder:2.1.0'
shadow 'de.maxhenkel.configbuilder:configbuilder:2.1.0'
}
sourceSets {
main {
java {
srcDirs = [
'source/api/src/main/java',
'source/bukkit/src/main/java',
layout.buildDirectory.dir('generated/sources/java').get().asFile
]
}
resources {
srcDirs = ['source/bukkit/src/main/resources']
exclude 'plugin.yml'
}
}
test {
java {
srcDirs = ['tests']
}
}
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
options.release = 8
}
tasks.register('generateBuildConstants', Copy) {
from 'source/bukkit/src/template/java'
into layout.buildDirectory.dir('generated/sources/java')
expand compatibility_version: '20',
mod_compatible_version: '2.6.x',
target_bukkit_version: '1.8.8-R0.1-SNAPSHOT',
plugin_name: 'Simple Voice Chat'
}
compileJava.dependsOn tasks.named('generateBuildConstants')
tasks.register('generateFallbackTranslations') {
def inputFile = file('source/common/src/main/resources/assets/voicechat/lang/en_us.json')
def outputFile = layout.buildDirectory.file('generated/sources/java/de/maxhenkel/voicechat/FallbackTranslations.java')
inputs.file inputFile
outputs.file outputFile
doLast {
def translations = new JsonSlurper().parse(inputFile)
def output = new StringBuilder('''package de.maxhenkel.voicechat;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class FallbackTranslations {
public static final Map<String, String> FALLBACK_TRANSLATIONS;
static {
Map<String, String> translations = new HashMap<>();
''')
translations.each { key, value ->
def k = key.toString().replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
def v = value.toString().replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
output.append(" translations.put(\"${k}\", \"${v}\");\n")
}
output.append(''' FALLBACK_TRANSLATIONS = Collections.unmodifiableMap(translations);
}
}
''')
outputFile.get().asFile.parentFile.mkdirs()
outputFile.get().asFile.text = output.toString()
}
}
compileJava.dependsOn tasks.named('generateFallbackTranslations')
processResources {
from('plugin.yml') {
filter { String line ->
line.startsWith('version:') ? "version: '${project.version}'" : line
}
}
}
tasks.named('jar') {
archiveClassifier = 'dev'
}
shadowJar {
configurations = [project.configurations.shadow]
archiveClassifier = ''
archiveFileName = "AsminVoiceChat-${project.version}.jar"
from('LICENSE-upstream.txt') {
rename { 'LICENSE_SimpleVoiceChat' }
}
relocate 'org.json', 'de.maxhenkel.voicechat.org.json'
relocate 'me.lucko.commodore', 'de.maxhenkel.voicechat.commodore'
relocate 'org.concentus', 'de.maxhenkel.voicechat.concentus'
relocate 'de.maxhenkel.configbuilder', 'de.maxhenkel.voicechat.configbuilder'
dependencies {
exclude(dependency('com.mojang:brigadier:.*'))
exclude(dependency('com.google.code.findbugs:jsr305:.*'))
}
}
tasks.named('build') {
dependsOn 'shadowJar'
dependsOn 'cryptoTest'
}
tasks.register('cryptoTest', JavaExec) {
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath + files(shadowJar.archiveFile)
mainClass = 'ProtocolCryptoSelfTest'
}
tasks.withType(Test).configureEach {
failOnNoDiscoveredTests = false
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx2G
org.gradle.daemon=false
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# 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.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/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/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# 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\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * 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" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+82
View File
@@ -0,0 +1,82 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
+1 -1
View File
@@ -1,5 +1,5 @@
name: AsminVoicechat
version: 1.0.0
version: 1.0.1
main: de.maxhenkel.voicechat.Voicechat
api-version: '1.13'
prefix: AsminVoicechat
+8
View File
@@ -0,0 +1,8 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
}
}
rootProject.name = 'AsminVoicechat'
@@ -0,0 +1,12 @@
package de.maxhenkel.voicechat.api;
public interface BukkitVoicechatService {
/**
* Registers the voice chat plugin on bukkit based servers.
*
* @param plugin the voicechat plugin
*/
void registerPlugin(VoicechatPlugin plugin);
}
@@ -0,0 +1,21 @@
package de.maxhenkel.voicechat.api;
import java.net.SocketAddress;
/**
* A socket used for client side voice chat traffic.
* Can be set using {@link de.maxhenkel.voicechat.api.events.ClientVoicechatInitializationEvent#setSocketImplementation(ClientVoicechatSocket)}.
*/
public interface ClientVoicechatSocket {
void open() throws Exception;
RawUdpPacket read() throws Exception;
void send(byte[] data, SocketAddress address) throws Exception;
void close();
boolean isClosed();
}
@@ -0,0 +1,22 @@
package de.maxhenkel.voicechat.api;
import java.util.UUID;
public interface Entity {
/**
* @return the UUID of the entity
*/
UUID getUuid();
/**
* @return the actual entity object
*/
Object getEntity();
/**
* @return the current position of the entity
*/
Position getPosition();
}
@@ -1,12 +1,8 @@
/*
* Decompiled with CFR 0.152.
*/
package de.maxhenkel.voicechat.api;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(value=RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.RUNTIME)
public @interface ForgeVoicechatPlugin {
}
@@ -0,0 +1,115 @@
package de.maxhenkel.voicechat.api;
import javax.annotation.Nullable;
import java.util.UUID;
/**
* Groups can be created using {@link VoicechatServerApi#groupBuilder()}.
*/
public interface Group {
/**
* @return the visual name of the group
*/
String getName();
/**
* @return if the group has a password
*/
boolean hasPassword();
/**
* @return the ID of the group
*/
UUID getId();
/**
* @return if the group is persistent
*/
boolean isPersistent();
/**
* @return if the group is hidden in the group list UI
*/
boolean isHidden();
/**
* @return the group type
*/
Type getType();
public interface Type {
/**
* Players in a group can hear nearby players that are not in a group
*/
public static final Type NORMAL = new Type() {
};
/**
* Players in a group can hear nearby players and nearby players can hear players in the group
*/
public static final Type OPEN = new Type() {
};
/**
* Players in a group can only hear other players in the group
*/
public static final Type ISOLATED = new Type() {
};
}
public interface Builder {
/**
* Sets the ID of the group.
* If this is not set, the ID will be randomly generated.
* <br/>
* <b>NOTE</b>: If there is already a group with the same ID, the group will be overwritten.
*
* @param id the group ID or <code>null</code> if the ID should be randomly generated
* @return the builder
*/
Builder setId(@Nullable UUID id);
/**
* <b>NOTE</b>: The name might be stripped of special characters and whitespace.
*
* @param name the name of the group
* @return the builder
*/
Builder setName(String name);
/**
* @param password the group password
* @return the builder
*/
Builder setPassword(@Nullable String password);
/**
* @param persistent if the group should be persistent
* @return the builder
*/
Builder setPersistent(boolean persistent);
/**
* @param hidden if the group should be hidden in the group list UI
* @return the builder
*/
Builder setHidden(boolean hidden);
/**
* @param type the group type
* @return the builder
*/
Builder setType(Type type);
/**
* @return the built group
* @throws IllegalStateException if the name is not set or invalid
*/
Group build();
}
}
@@ -0,0 +1,10 @@
package de.maxhenkel.voicechat.api;
public interface Player extends Entity {
/**
* @return the actual player object
*/
Object getPlayer();
}
@@ -0,0 +1,20 @@
package de.maxhenkel.voicechat.api;
public interface Position {
/**
* @return the X position
*/
double getX();
/**
* @return the Y position
*/
double getY();
/**
* @return the Z position
*/
double getZ();
}
@@ -0,0 +1,13 @@
package de.maxhenkel.voicechat.api;
import java.net.SocketAddress;
public interface RawUdpPacket {
byte[] getData();
long getTimestamp();
SocketAddress getSocketAddress();
}
@@ -0,0 +1,10 @@
package de.maxhenkel.voicechat.api;
public interface ServerLevel {
/**
* @return the actual level object
*/
Object getServerLevel();
}
@@ -0,0 +1,10 @@
package de.maxhenkel.voicechat.api;
public interface ServerPlayer extends Player {
/**
* @return the level of the player
*/
ServerLevel getServerLevel();
}
@@ -0,0 +1,116 @@
package de.maxhenkel.voicechat.api;
import de.maxhenkel.voicechat.api.audio.AudioConverter;
import de.maxhenkel.voicechat.api.mp3.Mp3Decoder;
import de.maxhenkel.voicechat.api.mp3.Mp3Encoder;
import de.maxhenkel.voicechat.api.opus.OpusDecoder;
import de.maxhenkel.voicechat.api.opus.OpusEncoder;
import de.maxhenkel.voicechat.api.opus.OpusEncoderMode;
import javax.annotation.Nullable;
import javax.sound.sampled.AudioFormat;
import java.io.InputStream;
import java.io.OutputStream;
public interface VoicechatApi {
/**
* Creates a new opus encoder.
* Note that the encoder needs to be closed after you are finished using it.
*/
OpusEncoder createEncoder();
/**
* Creates a new opus encoder.
* Note that the encoder needs to be closed after you are finished using it.
*
* @param mode the opus encoder mode
* @return the opus encoder
*/
OpusEncoder createEncoder(OpusEncoderMode mode);
/**
* Creates a new opus decoder.
* Note that the decoder needs to be closed after you are finished using it.
*
* @return the opus decoder
*/
OpusDecoder createDecoder();
/**
* <b>NOTE</b>: This is not available for Bukkit! It will always return null.
*
* @param audioFormat the audio format
* @param bitrate the bitrate in kbps
* @param quality the quality from 0 (highest) to 9 (lowest)
* @param outputStream the output stream to write the mp3 to
* @return the mp3 encoder or null if the encoder could not be initialized or is not available
*/
@Nullable
Mp3Encoder createMp3Encoder(AudioFormat audioFormat, int bitrate, int quality, OutputStream outputStream);
/**
* <b>NOTE</b>: This is not available for Bukkit! It will always return null.
*
* @param inputStream the input stream to read the mp3 from
* @return the mp3 decoder or null if the decoder could not be initialized or is not available
*/
@Nullable
Mp3Decoder createMp3Decoder(InputStream inputStream);
/**
* @return the audio converter
*/
AudioConverter getAudioConverter();
/**
* Creates an entity object from an actual entity.
*
* @param entity the entity implementation of your mod/plugin loader
* @return the entity object
*/
Entity fromEntity(Object entity);
/**
* Creates a level object from an actual level.
*
* @param serverLevel the level implementation of your mod/plugin loader
* @return the level
*/
ServerLevel fromServerLevel(Object serverLevel);
/**
* Creates a player object from an actual player.
*
* @param serverPlayer the player implementation of your mod/plugin loader
* @return the player
*/
ServerPlayer fromServerPlayer(Object serverPlayer);
/**
* Creates a new position object.
*
* @param x the X position
* @param y the Y position
* @param z the Z position
* @return the position with the provided coordinates
*/
Position createPosition(double x, double y, double z);
/**
* Don't forget to register your category with {@link VoicechatServerApi#registerVolumeCategory(VolumeCategory)} or {@link VoicechatClientApi#registerClientVolumeCategory(VolumeCategory)}
*
* @return a builder to build a category
*/
VolumeCategory.Builder volumeCategoryBuilder();
/**
* <b>NOTE</b>: Voice chat plugins can change this.
* This is only the default value.
* This value might not be correct if you are not connected to a server.
*
* @return the default distance, audio from voice chat is heard from
*/
double getVoiceChatDistance();
}
@@ -0,0 +1,166 @@
package de.maxhenkel.voicechat.api;
import de.maxhenkel.voicechat.api.audiochannel.ClientEntityAudioChannel;
import de.maxhenkel.voicechat.api.audiochannel.ClientLocationalAudioChannel;
import de.maxhenkel.voicechat.api.audiochannel.ClientStaticAudioChannel;
import de.maxhenkel.voicechat.api.config.ConfigAccessor;
import de.maxhenkel.voicechat.api.config.VolumeConfigAccessor;
import javax.annotation.Nullable;
import java.util.UUID;
public interface VoicechatClientApi extends VoicechatApi {
/**
* @return if the local voice chat is muted
*/
boolean isMuted();
/**
* Does the same as {@link #isDisabled(UUID)} with <code>null</code> as the player UUID.
*
* @return if the local voice chat is disabled
*/
default boolean isDisabled() {
return isDisabled(null);
}
/**
* @param playerId the UUID of the player or <code>null</code> for the local player
* @return if the player with the provided UUID has the voice chat disabled
*/
boolean isDisabled(@Nullable UUID playerId);
/**
* Does the same as {@link #isDisconnected(UUID)} with <code>null</code> as the player UUID.
*
* @return if the voice chat is disconnected from the server
*/
default boolean isDisconnected() {
return isDisconnected(null);
}
/**
* @param playerId the UUID of the player or <code>null</code> for the local player
* @return if the player with the provided UUID is disconnected from voice chat
*/
boolean isDisconnected(@Nullable UUID playerId);
/**
* Does the same as {@link #isTalking(UUID)} with <code>null</code> as the player UUID.
*
* @return if the local player is talking
*/
default boolean isTalking() {
return isTalking(null);
}
/**
* @param playerId the UUID of the player or <code>null</code> for the local player
* @return if the player with the provided UUID is currently talking
*/
boolean isTalking(@Nullable UUID playerId);
/**
* Does the same as {@link #isWhispering(UUID)} with <code>null</code> as the player UUID.
*
* @return if the local player is whispering
*/
default boolean isWhispering() {
return isWhispering(null);
}
/**
* @param playerId the UUID of the player or <code>null</code> for the local player
* @return if the player with the provided UUID is currently whispering
*/
boolean isWhispering(@Nullable UUID playerId);
/**
* This method returns if the push to talk key is pressed, even when using voice activation.
*
* @return if the push to talk key is pressed
*/
boolean isPushToTalkKeyPressed();
/**
* @return if the whisper key is pressed
*/
boolean isWhisperKeyPressed();
@Nullable
Group getGroup();
/**
* Creates a client side entity audio channel.
*
* @param uuid the UUID of the entity
* @return the audio channel
* @deprecated use {@link #createEntityAudioChannel(UUID, Entity)}
*/
@Deprecated
ClientEntityAudioChannel createEntityAudioChannel(UUID uuid);
/**
* Creates a client side entity audio channel.
*
* @param uuid the ID of the channel
* @param entity the entity to attach the channel to
* @return the audio channel
*/
ClientEntityAudioChannel createEntityAudioChannel(UUID uuid, Entity entity);
/**
* Creates a client side locational audio channel.
*
* @param uuid the ID of the channel
* @return the audio channel
*/
ClientLocationalAudioChannel createLocationalAudioChannel(UUID uuid, Position position);
/**
* Creates a client side static audio channel.
*
* @param uuid the ID of the channel
* @return the audio channel
*/
ClientStaticAudioChannel createStaticAudioChannel(UUID uuid);
/**
* Registers a volume category just for this client.
* A category can be created with {@link VoicechatApi#volumeCategoryBuilder()}.
* The category can be unregistered with {@link #unregisterClientVolumeCategory}.
*
* @param category the category to register
*/
void registerClientVolumeCategory(VolumeCategory category);
/**
* Unregisters a category on this client.
* This will release the texture ID for the icon if one exists.
*
* @param category the category to remove
*/
default void unregisterClientVolumeCategory(VolumeCategory category) {
unregisterClientVolumeCategory(category.getId());
}
/**
* Unregisters a category on this client.
* This will release the texture ID for the icon if one exists.
*
* @param categoryId the category ID to remove
*/
void unregisterClientVolumeCategory(String categoryId);
/**
* @return a read-only config accessor for the mods client config
*/
ConfigAccessor getClientConfig();
/**
* @return a read-only accessor for player and category volumes
*/
VolumeConfigAccessor getVolumeConfig();
}
@@ -0,0 +1,80 @@
package de.maxhenkel.voicechat.api;
import javax.annotation.Nullable;
/**
* <b>Note</b>: It is not guaranteed that the state of this connection object is up to date.
* Please re-fetch the connection object to get the latest state.
*/
public interface VoicechatConnection {
/**
* <b>Note</b>: This only returns the group the player was in when fetching this connection object.
* Calling {@link #setGroup(Group)} won't update the return value of this method.
*
* @return the group of the player - <code>null</code> if the player is not in a group
*/
@Nullable
Group getGroup();
/**
* <b>Note</b>: This only returns if the player was in a group when fetching this connection object.
* Calling {@link #setGroup(Group)} won't update the return value of this method.
*
* @return if the player is in a group
*/
boolean isInGroup();
/**
* Joins this player to the provided group.
*
* @param group the group to join or <code>null</code> to leave the current group
*/
void setGroup(@Nullable Group group);
/**
* This might not represent the actual state of the voice chat connection, since other voice chat plugins can fake having a player connected.
* <br/>
* <b>Note</b>: This only returns if the player was connected fetching this connection object.
* Calling {@link #setConnected(boolean)} won't update the return value of this method.
*
* @return if the player is connected to voice chat
*/
boolean isConnected();
/**
* The players disconnected state will reset if its actual disconnected state changes or if the player reconnects.
* <br/>
* <b>NOTE</b>: This method will only work for players that don't have the mod installed. See {@link #isInstalled()}.
*
* @param connected if the player should be shown as connected to voice chat
*/
void setConnected(boolean connected);
/**
* <b>Note</b>: This only returns if the player has the voice chat disabled when fetching this connection object.
* Calling {@link #setDisabled(boolean)} won't update the return value of this method.
*
* @return If the player muted the sound
*/
boolean isDisabled();
/**
* Sets the players disabled state.
* <b>NOTE</b>: This method will only work for players that don't have the mod installed. See {@link #isInstalled()}.
*
* @param disabled if the player should have the voice chat disabled icon
*/
void setDisabled(boolean disabled);
/**
* @return if the player has a version of voice chat installed that is compatible with the server
*/
boolean isInstalled();
/**
* @return the player
*/
ServerPlayer getPlayer();
}
@@ -0,0 +1,26 @@
package de.maxhenkel.voicechat.api;
import de.maxhenkel.voicechat.api.events.EventRegistration;
public interface VoicechatPlugin {
/**
* @return the ID of this plugin - Has to be unique
*/
String getPluginId();
/**
* Called after loading the plugin.
*/
default void initialize(VoicechatApi api) {
}
/**
* Register your events here - Only here!
*
* @param registration the event registration object, used to register events
*/
default void registerEvents(EventRegistration registration) {
}
}
@@ -0,0 +1,343 @@
package de.maxhenkel.voicechat.api;
import de.maxhenkel.voicechat.api.audiochannel.*;
import de.maxhenkel.voicechat.api.audiolistener.AudioListener;
import de.maxhenkel.voicechat.api.audiolistener.PlayerAudioListener;
import de.maxhenkel.voicechat.api.audiosender.AudioSender;
import de.maxhenkel.voicechat.api.config.ConfigAccessor;
import de.maxhenkel.voicechat.api.opus.OpusEncoder;
import de.maxhenkel.voicechat.api.packets.ConvertablePacket;
import de.maxhenkel.voicechat.api.packets.EntitySoundPacket;
import de.maxhenkel.voicechat.api.packets.LocationalSoundPacket;
import de.maxhenkel.voicechat.api.packets.StaticSoundPacket;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.function.Supplier;
public interface VoicechatServerApi extends VoicechatApi {
/**
* Sends the sound packet to the provided connection.
*
* @param connection the connection to send the packet to
* @param packet the packet to send
*/
void sendEntitySoundPacketTo(VoicechatConnection connection, EntitySoundPacket packet);
/**
* Sends the sound packet to the provided connection.
*
* @param connection the connection to send the packet to
* @param packet the packet to send
*/
void sendLocationalSoundPacketTo(VoicechatConnection connection, LocationalSoundPacket packet);
/**
* Sends the sound packet to the provided connection.
*
* @param connection the connection to send the packet to
* @param packet the packet to send
*/
void sendStaticSoundPacketTo(VoicechatConnection connection, StaticSoundPacket packet);
/**
* Creates a sound packet that can be converted to any type.
* <b>NOTE</b>: When creating a packet using this method, the sequence number must be set manually using {@link de.maxhenkel.voicechat.api.packets.SoundPacket.Builder#sequenceNumber(long)}.
*
* @return a convertable packet without any data
*/
ConvertablePacket createPacket();
/**
* Creates a sound channel for the specified entity.
*
* @param channelId the ID of the channel - Has to be unique
* @param entity the entity to attach the channel to
* @return the channel
*/
@Nullable
EntityAudioChannel createEntityAudioChannel(UUID channelId, Entity entity);
/**
* Creates a sound channel at the provided location.
*
* @param channelId the ID of the channel - Has to be unique
* @param level the world
* @param initialPosition the position where the sound should be played
* @return the channel
*/
@Nullable
LocationalAudioChannel createLocationalAudioChannel(UUID channelId, ServerLevel level, Position initialPosition);
/**
* Creates a static audio channel.
*
* @param channelId the ID of the channel - Has to be unique
* @param level the level
* @param connection the connection that should hear the audio
* @return the channel
* @deprecated use {@link #createStaticAudioChannel(UUID)} and add a target using {@link StaticAudioChannel#addTarget(VoicechatConnection)}
*/
@Deprecated
@Nullable
StaticAudioChannel createStaticAudioChannel(UUID channelId, ServerLevel level, VoicechatConnection connection);
/**
* Creates a static audio channel.
*
* @param channelId the ID of the channel - Has to be unique
* @return the channel
*/
@Nullable
StaticAudioChannel createStaticAudioChannel(UUID channelId);
/**
* Creates a new audio player.
* <br/>
* <br/>
* <b>NOTE</b>: Never use more than one audio player for every audio channel.
*
* @param audioChannel the channel where the audio player should send the audio to
* @param encoder the opus encoder used to encode the audio data
* @param audioSupplier this gets called whenever a new audio frame needs to be sent. The size of the array always needs to be 960. To end the playback, return <code>null</code>
* @return the audio player
*/
AudioPlayer createAudioPlayer(AudioChannel audioChannel, OpusEncoder encoder, Supplier<short[]> audioSupplier);
/**
* Creates a new audio player.
* <br/>
* <br/>
* <b>NOTE</b>: Never use more than one audio player for every audio channel.
*
* @param audioChannel the channel where the audio player should send the audio to
* @param encoder the opus encoder used to encode the audio data
* @param audio the audio data
* @return the audio player
*/
AudioPlayer createAudioPlayer(AudioChannel audioChannel, OpusEncoder encoder, short[] audio);
/**
* Creates a new audio sender.
* <br/>
* This can be used to simulate a player sending microphone packets.
* This needs to be registered using {@link #registerAudioSender(AudioSender)} and unregistered using {@link #unregisterAudioSender(AudioSender)}.
*
* @param connection the connection of the player
* @return the audio sender
*/
AudioSender createAudioSender(VoicechatConnection connection);
/**
* <b>NOTE</b>: Only one instance of this can exist per player. This will return <code>false</code> if an audio sender for this player already exists.
* <br/>
* <b>NOTE</b>: The audio sender will only work for players that are connected to the server and don't have the mod installed. Otherwise, it will return <code>false</code>.
*
* @param sender the sender to register
* @return if the sender was registered
*/
boolean registerAudioSender(AudioSender sender);
/**
* Unregisters an audio sender.
*
* @param sender the sender to unregister
* @return <code>false</code> if the audio sender was not registered.
*/
boolean unregisterAudioSender(AudioSender sender);
/**
* @return a {@link PlayerAudioListener} builder
*/
PlayerAudioListener.Builder playerAudioListenerBuilder();
/**
* Registers a new {@link AudioListener}.
* <br/>
* Returns false if the listener is already registered.
*
* @param listener the listener to register
* @return if the listener was registered
*/
boolean registerAudioListener(AudioListener listener);
/**
* Unregisters an {@link AudioListener}.
* <br/>
* Returns false if the listener is already unregistered.
*
* @param listener the listener to unregister
* @return if the listener was unregistered
*/
boolean unregisterAudioListener(AudioListener listener);
/**
* Unregisters an {@link AudioListener}.
* <br/>
* Returns false if the listener is already unregistered.
*
* @param listenerId the {@link AudioListener#getListenerId()} of the listener to unregister
* @return if the listener was unregistered
*/
boolean unregisterAudioListener(UUID listenerId);
/**
* Gets the connection of the player with this UUID.
*
* @param playerUuid the players UUID
* @return the connection or <code>null</code> if the player is not connected
*/
@Nullable
VoicechatConnection getConnectionOf(UUID playerUuid);
/**
* Gets the connection of the player.
*
* @param player the player
* @return the connection or <code>null</code> if the player is not connected
*/
@Nullable
default VoicechatConnection getConnectionOf(ServerPlayer player) {
return getConnectionOf(player.getUuid());
}
/**
* Creates a new group.
*
* @param name the name of the group
* @param password the password of the group - <code>null</code> for no password
* @return the group
* @deprecated use {@link #groupBuilder()} instead
*/
@Deprecated
Group createGroup(String name, @Nullable String password);
/**
* Creates a new group.
*
* @param name the name of the group
* @param password the password of the group - <code>null</code> for no password
* @param persistent if the group should be persistent
* @return the group
* @deprecated use {@link #groupBuilder()} instead
*/
@Deprecated
Group createGroup(String name, @Nullable String password, boolean persistent);
/**
* @return a new group builder
*/
Group.Builder groupBuilder();
/**
* Removes a persistent group.
*
* <b>NOTE</b>: You can't remove a group that is not persistent or has players in it.
*
* @param groupId the ID of the group
* @return if the group was removed
*/
boolean removeGroup(UUID groupId);
/**
* Gets a group by its ID.
*
* @param groupId the ID of the group
* @return the group or <code>null</code> if the group doesn't exist
*/
@Nullable
Group getGroup(UUID groupId);
/**
* @return all groups
*/
Collection<Group> getGroups();
/**
* Gets the secret for the user with this <code>userId</code>.
* Calling this function with a players UUID, that is already connected, this will return the secret of that player.
* Calling it with a new random UUID this will return a new secret.
* Use this with caution - Keep this secret confidential, as this allows connecting to the voice chat as this person.
* Note that if this <code>userId</code> is a players UUID and this player disconnects, the secret will get removed and thus is no longer valid to connect with.
*
* @param userId the user ID
* @return the secret or <code>null</code> if no server is running
* @deprecated this will just return <code>null</code>
*/
@Nullable
@Deprecated
UUID getSecret(UUID userId);
/**
* A convenience method to get all players in the range of a specific location.
*
* @param level the world
* @param pos the location
* @param range the range
* @param filter the filter to exclude specific players
* @return all players in the provided location
*/
Collection<ServerPlayer> getPlayersInRange(ServerLevel level, Position pos, double range, Predicate<ServerPlayer> filter);
/**
* The distance at which audio packets are broadcast.
* <b>NOTE</b>: This is not the distance it can be heard.
*
* @return the maximum distance, voice chat audio packets can be received
*/
double getBroadcastRange();
/**
* A convenience method to get all players in the range of a specific location.
*
* @param level the world
* @param pos the location
* @param range the range
* @return all players in the provided location
*/
default Collection<ServerPlayer> getPlayersInRange(ServerLevel level, Position pos, double range) {
return getPlayersInRange(level, pos, range, null);
}
/**
* Registers a volume category.
* This will get synchronized to all connected clients and all future clients that will connect.
* A category can be created with {@link VoicechatApi#volumeCategoryBuilder()}.
* The category can be unregistered with {@link #unregisterVolumeCategory}.
*
* @param category the category to register
*/
void registerVolumeCategory(VolumeCategory category);
/**
* Unregisters a category for all connected players.
* This will release the texture ID for the icon if one exists on all clients.
*
* @param category the category to remove
*/
default void unregisterVolumeCategory(VolumeCategory category) {
unregisterVolumeCategory(category.getId());
}
/**
* Unregisters a category for all connected players.
* This will release the texture ID for the icon if one exists on all clients.
*
* @param categoryId the category ID to remove
*/
void unregisterVolumeCategory(String categoryId);
/**
* @return all registered volume categories
*/
Collection<VolumeCategory> getVolumeCategories();
/**
* @return a read-only config accessor for the mods server config
*/
ConfigAccessor getServerConfig();
}
@@ -0,0 +1,23 @@
package de.maxhenkel.voicechat.api;
import java.net.SocketAddress;
/**
* A socket used for server side voice chat traffic.
* Can be set using {@link de.maxhenkel.voicechat.api.events.VoicechatServerStartingEvent#setSocketImplementation(VoicechatSocket)}.
*/
public interface VoicechatSocket {
void open(int port, String bindAddress) throws Exception;
RawUdpPacket read() throws Exception;
void send(byte[] data, SocketAddress address) throws Exception;
int getLocalPort();
void close();
boolean isClosed();
}
@@ -0,0 +1,97 @@
package de.maxhenkel.voicechat.api;
import javax.annotation.Nullable;
/**
* A custom volume category for the "adjust volumes screen".
* Create it with {@link VoicechatApi#volumeCategoryBuilder()}.
* Register it with {@link VoicechatServerApi#registerVolumeCategory(VolumeCategory)} or {@link VoicechatClientApi#registerClientVolumeCategory(VolumeCategory)}.
* Unregister it with {@link VoicechatServerApi#unregisterVolumeCategory} or {@link VoicechatClientApi#unregisterClientVolumeCategory}.
*/
public interface VolumeCategory {
/**
* @return the unique ID of the category
*/
String getId();
/**
* @return the full name of the category, that is displayed for the end user
*/
String getName();
/**
* @return the translation key for the full name of the category
*/
@Nullable
String getNameTranslationKey();
/**
* @return the hover tooltip that is shown for this category
*/
@Nullable
String getDescription();
/**
* @return the translation key for the hover tooltip of the category
*/
@Nullable
String getDescriptionTranslationKey();
/**
* @return the icon that is shown for this category
*/
@Nullable
int[][] getIcon();
public interface Builder {
/**
* This ID has to be between 1 and 16 characters and can only contain lowercase <code>a-z</code> and <code>_</code>.
*
* @param id the unique ID of the category
* @return the builder
*/
Builder setId(String id);
/**
* @param name the full name of the category, that is displayed for the end user if no translation is present
* @return the builder
*/
Builder setName(String name);
/**
* @param translationKey the translation key for the full name of the category
* @return the builder
*/
Builder setNameTranslationKey(@Nullable String translationKey);
/**
* @param description the hover tooltip that is shown for this category if no translation is present
* @return the builder
*/
Builder setDescription(@Nullable String description);
/**
* @param translationKey the translation key for the hover tooltip
* @return the builder
*/
Builder setDescriptionTranslationKey(@Nullable String translationKey);
/**
* The array has to be 16x16.
* Each integer represents a pixel in the RGBA format.
*
* @param icon the icon that is shown for this category
* @return the builder
*/
Builder setIcon(@Nullable int[][] icon);
/**
* @return the built category
*/
VolumeCategory build();
}
}
@@ -0,0 +1,27 @@
package de.maxhenkel.voicechat.api.audio;
import de.maxhenkel.voicechat.api.VoicechatApi;
/**
* A utility class to convert audio between different representations.
* <br/>
* Can be obtained by calling {@link VoicechatApi#getAudioConverter()}.
* <br/>
* <br/>
* <b>NOTE</b>: Everything is assumed to be 16 bit PCM audio.
*/
public interface AudioConverter {
short[] bytesToShorts(byte[] bytes);
byte[] shortsToBytes(short[] shorts);
short[] floatsToShorts(float[] floats);
float[] shortsToFloats(short[] shorts);
byte[] floatsToBytes(float[] floats);
float[] bytesToFloats(byte[] bytes);
}
@@ -0,0 +1,4 @@
/**
* Everything related to audio conversion and manipulation.
*/
package de.maxhenkel.voicechat.api.audio;
@@ -0,0 +1,65 @@
package de.maxhenkel.voicechat.api.audiochannel;
import de.maxhenkel.voicechat.api.ServerPlayer;
import de.maxhenkel.voicechat.api.VolumeCategory;
import de.maxhenkel.voicechat.api.packets.MicrophonePacket;
import javax.annotation.Nullable;
import java.util.UUID;
import java.util.function.Predicate;
public interface AudioChannel {
/**
* Sends the audio data to this audio channel.
*
* @param opusData Opus encoded audio data
*/
void send(byte[] opusData);
/**
* Forwards the provided microphone packet to this audio channel.
*
* @param packet the microphone packet containing the audio data
*/
void send(MicrophonePacket packet);
/**
* Applies a filter to the audio channel.
*
* @param filter only sends the data to players where the filter applies
*/
void setFilter(Predicate<ServerPlayer> filter);
/**
* Call this if you are finished sending data.
* You can still use this audio channel after calling this function,
* just don't call it after every packet, only after you are finished sending data.
*/
void flush();
/**
* @return if the audio channel is closed
*/
boolean isClosed();
/**
* @return the ID of the audio channel
*/
UUID getId();
/**
* @return the category ID of the audio channel
*/
@Nullable
String getCategory();
/**
* Make sure you registered your category before using it.
* See {@link de.maxhenkel.voicechat.api.VoicechatServerApi#registerVolumeCategory(VolumeCategory)}.
*
* @param category the category ID of the audio channel
*/
void setCategory(@Nullable String category);
}
@@ -0,0 +1,39 @@
package de.maxhenkel.voicechat.api.audiochannel;
/**
* Streams audio data from the server to clients.
* A player instance can be obtained by calling {@link de.maxhenkel.voicechat.api.VoicechatServerApi#createAudioPlayer}.
*/
public interface AudioPlayer {
/**
* Starts playing/streaming the audio.
*/
void startPlaying();
/**
* Stops playing/streaming the audio.
*/
void stopPlaying();
/**
* @return if the player has been started
*/
boolean isStarted();
/**
* @return if the audio is still playing
*/
boolean isPlaying();
/**
* @return if the player stopped playing
*/
boolean isStopped();
/**
* @param onStopped a runnable that's called when the player is finished playing
*/
void setOnStopped(Runnable onStopped);
}
@@ -0,0 +1,38 @@
package de.maxhenkel.voicechat.api.audiochannel;
import de.maxhenkel.voicechat.api.VolumeCategory;
import javax.annotation.Nullable;
import java.util.UUID;
public interface ClientAudioChannel {
/**
* @return the ID of the channel
*/
UUID getId();
/**
* Plays this audio data on this audio channel.
* <br>
* Providing audio that's not 960 samples will be ignored.
*
* @param rawAudio the raw 16 bit PCM audio frame
*/
void play(short[] rawAudio);
/**
* @return the category ID of the audio channel
*/
@Nullable
String getCategory();
/**
* Make sure you registered your category before using it.
* See {@link de.maxhenkel.voicechat.api.VoicechatServerApi#registerVolumeCategory(VolumeCategory)}.
*
* @param category the category of the audio channel
*/
void setCategory(@Nullable String category);
}
@@ -0,0 +1,32 @@
package de.maxhenkel.voicechat.api.audiochannel;
import java.util.UUID;
public interface ClientEntityAudioChannel extends ClientAudioChannel {
/**
* @return the UUID of the entity
*/
UUID getEntityId();
/**
* @param whispering if the entity should whisper
*/
void setWhispering(boolean whispering);
/**
* @return if the entity is whispering
*/
boolean isWhispering();
/**
* @return the distance, the audio can be heard
*/
float getDistance();
/**
* @param distance the distance, the audio can be heard
*/
void setDistance(float distance);
}
@@ -0,0 +1,29 @@
package de.maxhenkel.voicechat.api.audiochannel;
import de.maxhenkel.voicechat.api.Position;
public interface ClientLocationalAudioChannel extends ClientAudioChannel {
/**
* @return the current location of the audio channel
*/
Position getLocation();
/**
* Updates the location of the audio channel.
*
* @param position the location of the audio channel
*/
void setLocation(Position position);
/**
* @return the distance, the audio can be heard
*/
float getDistance();
/**
* @param distance the distance, the audio can be heard
*/
void setDistance(float distance);
}
@@ -0,0 +1,5 @@
package de.maxhenkel.voicechat.api.audiochannel;
public interface ClientStaticAudioChannel extends ClientAudioChannel {
}
@@ -0,0 +1,43 @@
package de.maxhenkel.voicechat.api.audiochannel;
import de.maxhenkel.voicechat.api.Entity;
/**
* An audio channel that is bound to an entity.
* <b>NOTE</b>: If you are using this for a player, you need to use {@link #updateEntity(Entity)} to update the player after it died.
*/
public interface EntityAudioChannel extends AudioChannel {
/**
* @return if the entity is whispering
*/
boolean isWhispering();
/**
* @param whispering if the entity should whisper
*/
void setWhispering(boolean whispering);
/**
* Sets a new entity where this channel is attached to.
*
* @param entity the entity to attach the channel to
*/
void updateEntity(Entity entity);
/**
* @return the entity where the channel is attached to
*/
Entity getEntity();
/**
* @return the distance, the audio can be heard
*/
float getDistance();
/**
* @param distance the distance, the audio can be heard
*/
void setDistance(float distance);
}
@@ -0,0 +1,29 @@
package de.maxhenkel.voicechat.api.audiochannel;
import de.maxhenkel.voicechat.api.Position;
public interface LocationalAudioChannel extends AudioChannel {
/**
* Updates the location of the audio.
*
* @param position the audio location
*/
void updateLocation(Position position);
/**
* @return the current location of this channel
*/
Position getLocation();
/**
* @return the distance, the audio can be heard
*/
float getDistance();
/**
* @param distance the distance, the audio can be heard
*/
void setDistance(float distance);
}
@@ -0,0 +1,38 @@
package de.maxhenkel.voicechat.api.audiochannel;
import de.maxhenkel.voicechat.api.VoicechatConnection;
public interface StaticAudioChannel extends AudioChannel {
/**
* Sets whether this audio channel should be received by targets in isolated groups.
*
* @param bypassGroupIsolation if group isolation should be bypassed
*/
void setBypassGroupIsolation(boolean bypassGroupIsolation);
/**
* @return if this audio channel is received by targets in isolated groups
*/
boolean bypassesGroupIsolation();
/**
* Adds a target to the audio channel.
*
* @param target the target to add
*/
void addTarget(VoicechatConnection target);
/**
* Removes a target from the audio channel.
*
* @param target the target to remove
*/
void removeTarget(VoicechatConnection target);
/**
* Removes all targets from the audio channel.
*/
void clearTargets();
}
@@ -0,0 +1,4 @@
/**
* Everything related to audio channels and serverside audio.
*/
package de.maxhenkel.voicechat.api.audiochannel;
@@ -1,11 +1,12 @@
/*
* Decompiled with CFR 0.152.
*/
package de.maxhenkel.voicechat.api.audiolistener;
import java.util.UUID;
public interface AudioListener {
public UUID getListenerId();
}
/**
* @return the ID of the listener
*/
UUID getListenerId();
}
@@ -0,0 +1,47 @@
package de.maxhenkel.voicechat.api.audiolistener;
import de.maxhenkel.voicechat.api.ServerPlayer;
import de.maxhenkel.voicechat.api.packets.SoundPacket;
import java.util.UUID;
import java.util.function.Consumer;
public interface PlayerAudioListener extends AudioListener {
/**
* @return the {@link UUID} of the player that is listened to
*/
UUID getPlayerUuid();
public interface Builder {
/**
* <b>Note</b>: It is required to either set a player with this method or with {@link #setPlayer(UUID)}
*
* @param player the player that should be listened to
*/
Builder setPlayer(ServerPlayer player);
/**
* <b>Note</b>: It is required to either set a player with this method or with {@link #setPlayer(ServerPlayer)}
*
* @param playerUuid the player UUID that should be listened to
*/
Builder setPlayer(UUID playerUuid);
/**
* <b>Note</b>: It is required to set a listener
*
* @param listener the listener
*/
Builder setPacketListener(Consumer<SoundPacket> listener);
/**
* @return the built listener
* @throws IllegalStateException if the player or the listener is not set
*/
PlayerAudioListener build();
}
}
@@ -0,0 +1,6 @@
/**
* Utilities for listening to all audio a player would theoretically receive.
* <p>
* Can be obtained by calling {@link de.maxhenkel.voicechat.api.VoicechatServerApi#playerAudioListenerBuilder()}.
*/
package de.maxhenkel.voicechat.api.audiolistener;
@@ -0,0 +1,52 @@
package de.maxhenkel.voicechat.api.audiosender;
public interface AudioSender {
/**
* @param whispering if the player should whisper
* @return the audio sender itself
*/
AudioSender whispering(boolean whispering);
/**
* @return if the player is whispering
*/
boolean isWhispering();
/**
* Sets the sequence number of the packet.
* <br/>
* Setting this will override the automatic sequence numbering.
* <br/>
* If you set this value, consequent calls to {@link AudioSender#send(byte[])} will increase this number by 1.
* If you don't intend this, you need to set the sequence number every time before calling {@link AudioSender#send(byte[])}.
* <br/>
* Calling {@link AudioSender#reset()} will also reset the sequence number to start with <code>0</code> again.
*
* @param sequenceNumber the sequence number (Must be >= 0)
* @return the audio sender itself
*/
AudioSender sequenceNumber(long sequenceNumber);
/**
* @return if a microphone packet can be sent. This will return false if the player has the mod installed or the {@link AudioSender} is not registered.
*/
boolean canSend();
/**
* Acts as if the player has sent a microphone packet.
*
* @param opusEncodedAudioData the opus encoded audio data
* @return <code>true</code> if the packet was sent, <code>false</code> if the sender does have the mod installed or the {@link AudioSender} is not registered
*/
boolean send(byte[] opusEncodedAudioData);
/**
* Resets the sequence number and indicates to clients that the current audio stream is paused/stopped.
* <br/>
* This method should be called when the sending of audio is paused or stopped.
* You can still send audio after this method is called.
*/
boolean reset();
}
@@ -0,0 +1,6 @@
/**
* Utilities for simulating players sending audio without the mod.
* <p>
* Can be obtained by calling {@link de.maxhenkel.voicechat.api.VoicechatServerApi#createAudioSender(de.maxhenkel.voicechat.api.VoicechatConnection)}.
*/
package de.maxhenkel.voicechat.api.audiosender;
@@ -0,0 +1,100 @@
package de.maxhenkel.voicechat.api.config;
import de.maxhenkel.voicechat.api.VoicechatClientApi;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
import javax.annotation.Nullable;
/**
* An interface to access a config.
* This is kept very generic, so it can be used for any config type.
* <br/>
* This contains a few default methods to get primitive values from the config.
* <br/>
* <b>NOTE</b>: You need to implement more complex values yourself (e.g. enums).
* <br/>
* Can be obtained by calling {@link VoicechatServerApi#getServerConfig()} or {@link VoicechatClientApi#getClientConfig()}.
*/
public interface ConfigAccessor {
/**
* Checks if the config has the given key.
*
* @param key the key
* @return <code>true</code> if the config has the key, <code>false</code> otherwise
*/
boolean hasKey(String key);
/**
* Gets the raw value of the given key.
* <b>NOTE</b>: This gets the <i>raw</i> value from the config, so it will be a {@link String} even if the value is a number, a boolean or an enum.
*
* @param key the key
* @return the value or <code>null</code> if the key doesn't exist
*/
@Nullable
String getValue(String key);
/**
* Gets the string value of the given key.
*
* @param key the key
* @param def the default value
* @return the value or the default value if the key doesn't exist
*/
default String getString(String key, String def) {
String value = getValue(key);
if (value == null) {
return def;
}
return value;
}
/**
* Gets the boolean value of the given key.
*
* @param key the key
* @param def the default value
* @return the boolean value, <code>false</code> if the value is not <code>"true"</code> or the default value if the key doesn't exist
*/
default boolean getBoolean(String key, boolean def) {
String value = getValue(key);
if (value == null) {
return def;
}
return Boolean.parseBoolean(value);
}
/**
* Gets the integer value of the given key.
*
* @param key the key
* @param def the default value
* @return the value or the default value if the key doesn't exist
* @throws NumberFormatException if the value is not an integer
*/
default int getInt(String key, int def) {
String value = getValue(key);
if (value == null) {
return def;
}
return Integer.parseInt(value);
}
/**
* Gets the double value of the given key.
*
* @param key the key
* @param def the default value
* @return the value or the default value if the key doesn't exist
* @throws NumberFormatException if the value is not a double
*/
default double getDouble(String key, double def) {
String value = getValue(key);
if (value == null) {
return def;
}
return Double.parseDouble(value);
}
}
@@ -0,0 +1,51 @@
package de.maxhenkel.voicechat.api.config;
import de.maxhenkel.voicechat.api.VoicechatClientApi;
import java.util.UUID;
/**
* An interface to access client side adjusted volumes.
* <br/>
* Can be obtained by calling {@link VoicechatClientApi#getVolumeConfig()}.
*/
public interface VolumeConfigAccessor {
/**
* Gets the volume of a player by its UUID.
* <br/>
* <br/>
* This will always return a value, even if the player is not in the volume config.
* <br/>
* By default, the volume will be <code>1.0</code>.
* A volume of <code>0.0</code> means this player is muted.
* Can be larger than <code>1.0</code>.
* Also works for players that are not online.
* <br/>
* <br/>
* If you want the <code>Other</code> volume (All volumes that are not covered by categories or players), use the UUID <code>00000000-0000-0000-0000-000000000000</code>.
*
* @param uuid the UUID of the player
* @return the volume of the player
* @throws NullPointerException if the UUID is null
*/
double getVolume(UUID uuid);
/**
* Gets the volume of a category by its UUID.
* <br/>
* <br/>
* This will always return a value, even if the category doesn't exist.
* <br/>
* By default, the volume will be <code>1.0</code>.
* A volume of <code>0.0</code> means this category is muted.
* Can be larger than <code>1.0</code>.
* Also works for categories that are currently not registered.
*
* @param category the category
* @return the volume of the category
* @throws NullPointerException if the category is null
*/
double getCategoryVolume(String category);
}
@@ -0,0 +1,4 @@
/**
* Everything configuration related.
*/
package de.maxhenkel.voicechat.api.config;
@@ -0,0 +1,12 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.VoicechatClientApi;
public interface ClientEvent extends Event {
/**
* @return the voice chat client API
*/
VoicechatClientApi getVoicechat();
}
@@ -0,0 +1,73 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.Position;
import javax.annotation.Nullable;
import java.util.UUID;
/**
* This event is emitted before the sound is played on the client.
*/
public interface ClientReceiveSoundEvent extends ClientEvent {
/**
* @return the ID of the channel
*/
UUID getId();
/**
* The unencoded audio data.
* <br/>
* Returns an empty array in case of the end of transmission
*
* @return the raw 16 bit PCM audio frame
*/
short[] getRawAudio();
/**
* Overrides the actual audio data that is played.
* <p>
* This is ignored, if the {@link #getRawAudio} array is empty
* <br>
* Providing audio that's not 960 samples will be ignored.
* An empty array skips this frame, which adds latency. To mute the audio, use an array filled with zeros instead.
*
* @param rawAudio the raw 16 bit PCM audio frame
*/
void setRawAudio(@Nullable short[] rawAudio);
public static interface EntitySound extends ClientReceiveSoundEvent {
/**
* @return the UUID of the entity
*/
UUID getEntityId();
/**
* @return if the player is whispering
*/
boolean isWhispering();
/**
* @return the distance, the audio can be heard
*/
float getDistance();
}
public static interface LocationalSound extends ClientReceiveSoundEvent {
/**
* @return the position of the sound
*/
Position getPosition();
/**
* @return the distance, the audio can be heard
*/
float getDistance();
}
public static interface StaticSound extends ClientReceiveSoundEvent {
}
}
@@ -0,0 +1,29 @@
package de.maxhenkel.voicechat.api.events;
/**
* This event is emitted before the client encodes the audio and sends it to the server.
*/
public interface ClientSoundEvent extends ClientEvent {
/**
* The unencoded audio data.
* <br/>
* <s>Returns an empty array in case of the end of transmission</s>
*
* @return the raw 16 bit PCM audio frame
*/
short[] getRawAudio();
/**
* Overrides the actual audio data that's sent to the server.
*
* @param rawAudio the raw 16 bit PCM audio frame
*/
void setRawAudio(short[] rawAudio);
/**
* @return if the player is whispering
*/
boolean isWhispering();
}
@@ -0,0 +1,13 @@
package de.maxhenkel.voicechat.api.events;
/**
* This event is emitted on the client when the voice chat connects/disconnects.
*/
public interface ClientVoicechatConnectionEvent extends ClientEvent {
/**
* @return if the voice chat is connected to the server
*/
boolean isConnected();
}
@@ -0,0 +1,22 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.ClientVoicechatSocket;
import javax.annotation.Nullable;
public interface ClientVoicechatInitializationEvent extends ClientEvent {
/**
* Sets a custom implementation of the socket used for client side voice chat traffic.
*
* @param socket the custom socket implementation
*/
void setSocketImplementation(ClientVoicechatSocket socket);
/**
* @return the custom socket implementation or <code>null</code> to use voice chats default one
*/
@Nullable
ClientVoicechatSocket getSocketImplementation();
}
@@ -0,0 +1,21 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.Group;
import de.maxhenkel.voicechat.api.VoicechatConnection;
import javax.annotation.Nullable;
public interface CreateGroupEvent extends GroupEvent {
/**
* @return the group that was created
*/
Group getGroup();
/**
* @return the connection of the player that created the group or <code>null</code> if the group was not created by a player
*/
@Nullable
VoicechatConnection getConnection();
}
@@ -0,0 +1,18 @@
package de.maxhenkel.voicechat.api.events;
/**
* This event is emitted when the voice chats OpenAL context is created.
*/
public interface CreateOpenALContextEvent extends ClientEvent {
/**
* @return the OpenAL context
*/
long getContext();
/**
* @return the OpenAL device
*/
long getDevice();
}
@@ -0,0 +1,18 @@
package de.maxhenkel.voicechat.api.events;
/**
* This event is emitted when the voice chats OpenAL context is destroyed.
*/
public interface DestroyOpenALContextEvent extends ClientEvent {
/**
* @return the OpenAL context - Might be <code>0L</code> in some cases
*/
long getContext();
/**
* @return the OpenAL device - Might be <code>0L</code> in some cases
*/
long getDevice();
}
@@ -0,0 +1,10 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.packets.EntitySoundPacket;
/**
* This event is emitted when an entity sound packet is about to get sent to a client.
*/
public interface EntitySoundPacketEvent extends SoundPacketEvent<EntitySoundPacket> {
}
@@ -0,0 +1,23 @@
package de.maxhenkel.voicechat.api.events;
public interface Event {
/**
* @return if this event can be cancelled
*/
boolean isCancellable();
/**
* Cancels this event.
* Does nothing if the event isn't cancellable.
*
* @return if the event was actually cancelled
*/
boolean cancel();
/**
* @return if the event was cancelled
*/
boolean isCancelled();
}
@@ -0,0 +1,28 @@
package de.maxhenkel.voicechat.api.events;
import java.util.function.Consumer;
public interface EventRegistration {
/**
* Registers an event.
*
* @param eventClass the class of the event you want to receive
* @param onPacket the consumer that is called when the event was dispatched
* @param priority the event priority - Higher values mean a higher priority
* @param <T> the event type
*/
<T extends Event> void registerEvent(Class<T> eventClass, Consumer<T> onPacket, int priority);
/**
* Registers an event.
*
* @param eventClass the class of the event you want to receive
* @param onPacket the consumer that is called when the event was dispatched
* @param <T> the event type
*/
default <T extends Event> void registerEvent(Class<T> eventClass, Consumer<T> onPacket) {
registerEvent(eventClass, onPacket, 0);
}
}
@@ -0,0 +1,22 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.Group;
import de.maxhenkel.voicechat.api.VoicechatConnection;
import javax.annotation.Nullable;
public interface GroupEvent extends ServerEvent {
/**
* @return the group - <code>null</code> if there is no group
*/
@Nullable
Group getGroup();
/**
* @return the connection of the player
*/
@Nullable
VoicechatConnection getConnection();
}
@@ -0,0 +1,18 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.Group;
import de.maxhenkel.voicechat.api.VoicechatConnection;
public interface JoinGroupEvent extends GroupEvent {
/**
* @return the group that was joined
*/
Group getGroup();
/**
* @return the connection of the player
*/
VoicechatConnection getConnection();
}
@@ -0,0 +1,21 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.Group;
import de.maxhenkel.voicechat.api.VoicechatConnection;
import javax.annotation.Nullable;
public interface LeaveGroupEvent extends GroupEvent {
/**
* @return the group that was left or <code>null</code> if the player was not in a group
*/
@Nullable
Group getGroup();
/**
* @return the connection of the player
*/
VoicechatConnection getConnection();
}
@@ -0,0 +1,10 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.packets.LocationalSoundPacket;
/**
* This event is emitted when a locational sound packet is about to get sent to a client.
*/
public interface LocationalSoundPacketEvent extends SoundPacketEvent<LocationalSoundPacket> {
}
@@ -0,0 +1,22 @@
package de.maxhenkel.voicechat.api.events;
/**
* This event is emitted before the {@link ClientSoundEvent} is getting called.
* It allows you to merge audio into the audio that is captured from the microphone.
* <br/>
* This is called even when the player is muted or not talking.
* <br/>
* When the voice chat it disabled, this event is not called.
*/
public interface MergeClientSoundEvent extends ClientEvent {
/**
* Merges the audio into the audio that is captured from the microphone.
* <br/>
* Calling this multiple times will merge the audio multiple times into the same audio frame.
*
* @param audio the audio that should get merged
*/
void mergeAudio(short[] audio);
}
@@ -0,0 +1,13 @@
package de.maxhenkel.voicechat.api.events;
/**
* This event is emitted on the client when the state of the microphone changes.
*/
public interface MicrophoneMuteEvent extends ClientEvent {
/**
* @return if the microphone is muted
*/
boolean isDisabled();
}
@@ -0,0 +1,10 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.packets.MicrophonePacket;
/**
* This event is emitted when a microphone packet arrives at the server.
*/
public interface MicrophonePacketEvent extends PacketEvent<MicrophonePacket> {
}
@@ -0,0 +1,30 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.VoicechatClientApi;
import java.util.UUID;
/**
* This event is emitted when an icon is getting rendered.
* <br/>
* This event should not be used to do any actual rendering.
* Just use this event to cancel the rendering of the icon.
* <br/>
* <b>Warning: </b> Don't do heavy operations in this event, as it will block the render thread!
* <br/>
* <br/>
* <b>Note: </b> Don't keep references to this event. It will be reused for every icon render to avoid allocations.
* <br/>
* <br/>
* Use {@link VoicechatClientApi#isTalking(UUID)}, {@link VoicechatClientApi#isWhispering(UUID)},
* {@link VoicechatClientApi#isDisabled(UUID)} and {@link VoicechatClientApi#isDisconnected(UUID)}
* to get the status of the player and render it using your mod loaders render event system.
*/
public interface NameTagIconRenderEvent extends ClientEvent {
/**
* @return the UUID of the entity the icon is being rendered for
*/
UUID getEntityId();
}
@@ -0,0 +1,59 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.Position;
import javax.annotation.Nullable;
import java.util.UUID;
/**
* This event is emitted for every audio chunk for every audio channel.
* These events are meant to be used to modify OpenAL effects.
* <br/>
* This specific event is emitted after the position of the sound is set, but before the any other OpenAL calls.
* Use {@link Pre} or {@link Post} get earlier or later events respectively.
*/
public interface OpenALSoundEvent extends ClientEvent {
/**
* Returns the position of the sound.
* This returns <code>null</code> for static sounds.
*
* @return the position of the sound
*/
@Nullable
Position getPosition();
/**
* This returns <code>null</code> for non audio channel sounds, like microphone testing.
*
* @return the unique ID of the audio channel
*/
@Nullable
UUID getChannelId();
/**
* @return the OpenAL source
*/
int getSource();
/**
* @return the category of the sound
*/
@Nullable
String getCategory();
/**
* This event is emitted before any OpenAL calls were made by the voice chat.
*/
public interface Pre extends OpenALSoundEvent {
}
/**
* This event is emitted after all OpenAL calls by the voice chat and the audio is added to the buffer queue.
*/
public interface Post extends OpenALSoundEvent {
}
}
@@ -0,0 +1,27 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.VoicechatConnection;
import de.maxhenkel.voicechat.api.packets.Packet;
import javax.annotation.Nullable;
public interface PacketEvent<T extends Packet> extends ServerEvent {
/**
* @return the packet
*/
T getPacket();
/**
* @return the connection of the player that should receive this packet
*/
@Nullable
VoicechatConnection getReceiverConnection();
/**
* @return the connection of the player that sent this packet or <code>null</code> if it wasn't sent by a player
*/
@Nullable
VoicechatConnection getSenderConnection();
}
@@ -0,0 +1,12 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.VoicechatConnection;
public interface PlayerConnectedEvent extends ServerEvent {
/**
* @return the connection of the player
*/
VoicechatConnection getConnection();
}
@@ -0,0 +1,12 @@
package de.maxhenkel.voicechat.api.events;
import java.util.UUID;
public interface PlayerDisconnectedEvent extends ServerEvent {
/**
* @return the UUID of the player
*/
UUID getPlayerUuid();
}
@@ -0,0 +1,35 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.VoicechatConnection;
import javax.annotation.Nullable;
import java.util.UUID;
/**
* Called when a player changes its state.
* For example if voice chat connects/disconnects, the player joins/leaves a group or disables voice chat.
*/
public interface PlayerStateChangedEvent extends ServerEvent {
/**
* @return if the player disabled voice chat
*/
boolean isDisabled();
/**
* @return if the player is disconnected from voice chat
*/
boolean isDisconnected();
/**
* @return the uuid of the player
*/
UUID getPlayerUuid();
/**
* @return the connection of the player
*/
@Nullable
VoicechatConnection getConnection();
}
@@ -0,0 +1,5 @@
package de.maxhenkel.voicechat.api.events;
public interface RegisterVolumeCategoryEvent extends VolumeCategoryEvent {
}
@@ -0,0 +1,25 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.Group;
import de.maxhenkel.voicechat.api.VoicechatConnection;
import javax.annotation.Nullable;
/**
* This event is only cancellable if the group is persistent
*/
public interface RemoveGroupEvent extends GroupEvent {
/**
* @return the group that was removed
*/
Group getGroup();
/**
* @return <code>null</code>
*/
@Nullable
@Deprecated
VoicechatConnection getConnection();
}
@@ -0,0 +1,12 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
public interface ServerEvent extends Event {
/**
* @return the voice chat server API
*/
VoicechatServerApi getVoicechat();
}
@@ -0,0 +1,23 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.packets.Packet;
public interface SoundPacketEvent<T extends Packet> extends PacketEvent<T> {
String SOURCE_GROUP = "group";
String SOURCE_PROXIMITY = "proximity";
String SOURCE_SPECTATOR = "spectator";
String SOURCE_PLUGIN = "plugin";
/**
* Where the packet originated from.
* It can be either {@value SOURCE_GROUP} for group chats,
* {@value SOURCE_PROXIMITY} for proximity sound,
* {@value SOURCE_SPECTATOR} for spectator sound
* or {@value SOURCE_PLUGIN} if the packet was sent from a plugin.
*
* @return the source of the packet
*/
String getSource();
}
@@ -0,0 +1,10 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.packets.StaticSoundPacket;
/**
* This event is emitted when a static sound packet is about to get sent to a client.
*/
public interface StaticSoundPacketEvent extends SoundPacketEvent<StaticSoundPacket> {
}
@@ -0,0 +1,5 @@
package de.maxhenkel.voicechat.api.events;
public interface UnregisterVolumeCategoryEvent extends VolumeCategoryEvent {
}
@@ -0,0 +1,39 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.VoicechatConnection;
import de.maxhenkel.voicechat.api.packets.MicrophonePacket;
/**
* This event is emitted after a microphone packet arrives at the server and the distance is processed.
* This can be used to modify the distance at which other players can hear this packet.
*/
public interface VoiceDistanceEvent extends ServerEvent {
/**
* @return the microphone packet
*/
MicrophonePacket getPacket();
/**
* @return the connection of the player
*/
VoicechatConnection getSenderConnection();
/**
* This returns either the default distance that's set on the server or the modified distance if another subscriber changed it already.
* In case a player is whispering, this will return the whisper distance.
* The whisper state can be obtained by calling {@link #getPacket()} and getting {@link MicrophonePacket#isWhispering()}.
*
* @return the distance in blocks
*/
float getDistance();
/**
* Sets the distance this player sends audio at.
* Note that this also includes the whisper distance, if the player is whispering.
*
* @param distance the distance in blocks
*/
void setDistance(float distance);
}
@@ -0,0 +1,24 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.ServerPlayer;
public interface VoiceHostEvent extends ServerEvent {
/**
* @return the voice host string
*/
String getVoiceHost();
/**
* Overwrites voice chats voice host - This is sent to the client and used by it to connect to the server.
*
* @param voiceHost the voice host string
*/
void setVoiceHost(String voiceHost);
/**
* @return the player that is connecting
*/
ServerPlayer getPlayer();
}
@@ -0,0 +1,13 @@
package de.maxhenkel.voicechat.api.events;
/**
* This event is emitted on the client when the voice chat is getting enabled/disabled.
*/
public interface VoicechatDisableEvent extends ClientEvent {
/**
* @return if the voice chat is disabled
*/
boolean isDisabled();
}
@@ -0,0 +1,5 @@
package de.maxhenkel.voicechat.api.events;
public interface VoicechatServerStartedEvent extends ServerEvent {
}
@@ -0,0 +1,22 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.VoicechatSocket;
import javax.annotation.Nullable;
public interface VoicechatServerStartingEvent extends ServerEvent {
/**
* Sets a custom implementation of the socket used for voice chat traffic.
*
* @param socket the custom socket implementation
*/
void setSocketImplementation(VoicechatSocket socket);
/**
* @return the custom socket implementation or <code>null</code> to use voice chats default one
*/
@Nullable
VoicechatSocket getSocketImplementation();
}
@@ -0,0 +1,5 @@
package de.maxhenkel.voicechat.api.events;
public interface VoicechatServerStoppedEvent extends ServerEvent {
}
@@ -0,0 +1,12 @@
package de.maxhenkel.voicechat.api.events;
import de.maxhenkel.voicechat.api.VolumeCategory;
public interface VolumeCategoryEvent extends ServerEvent {
/**
* @return the volume category
*/
VolumeCategory getVolumeCategory();
}
@@ -0,0 +1,6 @@
/**
* All registerable events.
* <p>
* Events can be registered by calling {@link de.maxhenkel.voicechat.api.events.EventRegistration#registerEvent}.
*/
package de.maxhenkel.voicechat.api.events;
@@ -0,0 +1,37 @@
package de.maxhenkel.voicechat.api.mp3;
import de.maxhenkel.voicechat.api.VoicechatApi;
import javax.sound.sampled.AudioFormat;
import java.io.IOException;
/**
* You can obtain an instance of this class by calling {@link VoicechatApi#createMp3Decoder}.
*/
public interface Mp3Decoder {
/**
* Decodes the MP3 file and returns the decoded audio data as PCM samples.
*
* @return the decoded audio data as PCM samples
* @throws IOException if an I/O error occurs
*/
short[] decode() throws IOException;
/**
* Decodes the MP3 file if {@link #decode()} has not been called before.
*
* @return the audio format of the decoded audio data
* @throws IOException if an I/O error occurs
*/
AudioFormat getAudioFormat() throws IOException;
/**
* Decodes the MP3 file if {@link #decode()} has not been called before.
*
* @return the bitrate of the mp3 file
* @throws IOException if an I/O error occurs
*/
int getBitrate() throws IOException;
}
@@ -0,0 +1,30 @@
package de.maxhenkel.voicechat.api.mp3;
import de.maxhenkel.voicechat.api.VoicechatApi;
import java.io.IOException;
/**
* You can obtain an instance of this class by calling {@link VoicechatApi#createMp3Encoder}.
*/
public interface Mp3Encoder {
/**
* Encodes the given PCM samples and writes it to the provided output stream.
*
* @param samples the PCM samples to encode
* @throws IOException if an I/O error occurs
*/
void encode(short[] samples) throws IOException;
/**
* Closes the encoder and flushes the output stream.
* Also writes leftover mp3 data to the output stream.
*
* <b>NOTE</b>: Not closing encoders will cause a memory leak in voice chat versions <code>&lt;2.6.3</code>!
*
* @throws IOException if an I/O error occurs
*/
void close() throws IOException;
}
@@ -0,0 +1,4 @@
/**
* Wrappers for mp3 encoding and decoding.
*/
package de.maxhenkel.voicechat.api.mp3;
@@ -0,0 +1,45 @@
package de.maxhenkel.voicechat.api.opus;
import de.maxhenkel.voicechat.api.VoicechatApi;
import javax.annotation.Nullable;
/**
* Instances can be obtained by calling {@link VoicechatApi#createDecoder()}.
*/
public interface OpusDecoder {
/**
* Decodes opus encoded audio data to 16 bit PCM audio.
*
* @param data the opus encoded data or <code>null</code> to do PLC
* @return the 16 bit PCM audio data
*/
short[] decode(@Nullable byte[] data);
/**
* Decodes the provided packet and recovers previous lost frames using FEC.
*
* @param input the input packet
* @param frames the number of frames to return (min 1 for just the current frame)
* @return an array containing the decoded frames - the length of the array is equal to {@param frames}
*/
short[][] decode(byte[] input, int frames);
/**
* Resets the decoders state.
*/
void resetState();
/**
* @return if the decoder is closed
*/
boolean isClosed();
/**
* Closes the decoder.
* <b>NOTE</b>: Not closing encoders will cause a memory leak in voice chat versions <code>&lt;2.6.3</code>!
*/
void close();
}
@@ -0,0 +1,34 @@
package de.maxhenkel.voicechat.api.opus;
import de.maxhenkel.voicechat.api.VoicechatApi;
/**
* Instances can be obtained by calling {@link VoicechatApi#createEncoder()}.
*/
public interface OpusEncoder {
/**
* Encodes 16 bit PCM audio with opus.
*
* @param rawAudio the raw 16 bit PCM audio
* @return the opus encoded data
*/
byte[] encode(short[] rawAudio);
/**
* Resets the encoders state.
*/
void resetState();
/**
* @return if the encoder is closed
*/
boolean isClosed();
/**
* Closes the encoder.
* <b>NOTE</b>: Not closing encoders will cause a memory leak in voice chat versions <code>&lt;2.6.3</code>!
*/
void close();
}
@@ -0,0 +1,8 @@
package de.maxhenkel.voicechat.api.opus;
/**
* The different Opus encoder modes
*/
public enum OpusEncoderMode {
VOIP, AUDIO, RESTRICTED_LOWDELAY;
}
@@ -0,0 +1,4 @@
/**
* Wrappers for the Opus codec.
*/
package de.maxhenkel.voicechat.api.opus;
@@ -0,0 +1,4 @@
/**
* The main Simple Voice Chat API package.
*/
package de.maxhenkel.voicechat.api;
@@ -0,0 +1,64 @@
package de.maxhenkel.voicechat.api.packets;
import de.maxhenkel.voicechat.api.Position;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
import java.util.UUID;
public interface ConvertablePacket {
/**
* <b>NOTE</b>: Make sure to set {@link SoundPacket.Builder#channelId(UUID)} to avoid conflicts with other channels.
*
* @return a builder to build an entity sound packet based on this packet
*/
EntitySoundPacket.Builder<?> entitySoundPacketBuilder();
/**
* <b>NOTE</b>: Make sure to set {@link SoundPacket.Builder#channelId(UUID)} to avoid conflicts with other channels.
*
* @return a builder to build a locational sound packet based on this packet
*/
LocationalSoundPacket.Builder<?> locationalSoundPacketBuilder();
/**
* <b>NOTE</b>: Make sure to set {@link SoundPacket.Builder#channelId(UUID)} to avoid conflicts with other channels.
*
* @return a builder to build a static sound packet based on this packet
*/
StaticSoundPacket.Builder<?> staticSoundPacketBuilder();
/**
* Converts this packet to an entity sound packet.
*
* @param entityUuid the UUID of the entity
* @param whispering if the entity is whispering
* @return the entity sound packet
* @throws UnsupportedOperationException if this is called on a packet created by {@link VoicechatServerApi#createPacket()}
* @deprecated use {@link #entitySoundPacketBuilder()}
*/
@Deprecated
EntitySoundPacket toEntitySoundPacket(UUID entityUuid, boolean whispering);
/**
* Converts this packet to a locational sound packet.
*
* @param position the position of the audio
* @return the locational sound packet
* @throws UnsupportedOperationException if this is called on a packet created by {@link VoicechatServerApi#createPacket()}
* @deprecated use {@link #locationalSoundPacketBuilder()}
*/
@Deprecated
LocationalSoundPacket toLocationalSoundPacket(Position position);
/**
* Converts this packet to a static sound packet.
*
* @return the static sound packet
* @throws UnsupportedOperationException if this is called on a packet created by {@link VoicechatServerApi#createPacket()}
* @deprecated use {@link #staticSoundPacketBuilder()}
*/
@Deprecated
StaticSoundPacket toStaticSoundPacket();
}
@@ -0,0 +1,60 @@
package de.maxhenkel.voicechat.api.packets;
import java.util.UUID;
/**
* The receiver of this event will hear the sound from the specified entity.
* This can be done for all entities, but only players will show the speaker icon.
* If the entity is further away than the maximum voice distance, the receiving player won't actually hear the sound.
*/
public interface EntitySoundPacket extends SoundPacket {
/**
* @return the UUID of the entity
*/
UUID getEntityUuid();
/**
* @return if the entity is whispering
*/
boolean isWhispering();
/**
* @return the distance, the audio can be heard
*/
float getDistance();
/**
* A builder to build an entity sound packet.
*
* <b>NOTE</b>: Some values are required to be set.
*
* @param <T> the builder itself
*/
public interface Builder<T extends Builder<T>> extends SoundPacket.Builder<T, EntitySoundPacket> {
/**
* This is required to be set!
* <br>
* The entity UUID is also used as the sender of the packet.
*
* @param entityUuid the UUID of the entity
* @return the builder
*/
T entityUuid(UUID entityUuid);
/**
* @param whispering if the entity should be whispering
* @return the builder
*/
T whispering(boolean whispering);
/**
* @param distance the distance, this packet can be heard
* @return the builder
*/
T distance(float distance);
}
}
@@ -0,0 +1,46 @@
package de.maxhenkel.voicechat.api.packets;
import de.maxhenkel.voicechat.api.Position;
/**
* The receiver of this event will hear the sound from the specified location.
* If the location is further away than the maximum voice distance, the receiving player won't actually hear the sound.
*/
public interface LocationalSoundPacket extends SoundPacket {
/**
* @return the audio location
*/
Position getPosition();
/**
* @return the distance, the audio can be heard
*/
float getDistance();
/**
* A builder to build a locational sound packet.
*
* <b>NOTE</b>: Some values are required to be set.
*
* @param <T> the builder itself
*/
public interface Builder<T extends Builder<T>> extends SoundPacket.Builder<T, LocationalSoundPacket> {
/**
* This is required to be set!
*
* @param position the position of the sound
* @return the builder
*/
T position(Position position);
/**
* @param distance the distance, this packet can be heard
* @return the builder
*/
T distance(float distance);
}
}
Loaded 100 of 583 files, more files were not shown because too many files have changed in this diff. Show more