asminvoicechat second commit
Build AsminVoicechat / build-and-release (push) Canceled after 12s

This commit is contained in:
asminme committed 2026-10-24 15:42:08 +02:00
1 parent bbccd4a815
commit 71396a86a0
581 files changed
+9084 -38713

No files matched your search

@@ -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);
}
}
@@ -0,0 +1,22 @@
package de.maxhenkel.voicechat.api.packets;
public interface MicrophonePacket extends Packet, ConvertablePacket {
/**
* @return if the player is whispering
*/
boolean isWhispering();
/**
* @return the opus encoded audio data from the player
*/
byte[] getOpusEncodedData();
/**
* Allows you to modify or replace the opus encoded audio data.
*
* @param data the opus encoded audio data to replace
*/
void setOpusEncodedData(byte[] data);
}
@@ -1,8 +1,4 @@
/*
* Decompiled with CFR 0.152.
*/
package de.maxhenkel.voicechat.api.packets;
public interface Packet {
}
@@ -0,0 +1,100 @@
package de.maxhenkel.voicechat.api.packets;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
import de.maxhenkel.voicechat.api.VolumeCategory;
import javax.annotation.Nullable;
import java.util.UUID;
public interface SoundPacket extends Packet, ConvertablePacket {
/**
* @return the channel ID of the sound packet
*/
UUID getChannelId();
/**
* @return the sender of this packet - doesn't necessarily need to be a player's UUID
*/
UUID getSender();
/**
* @return the opus encoded audio data
*/
byte[] getOpusEncodedData();
/**
* @return the sequence number of the packet
*/
long getSequenceNumber();
/**
* @return the category ID of the sound packet
*/
@Nullable
String getCategory();
/**
* A builder to build a sound packet.
*
* @param <T> the builder itself
* @param <P> the packet, the builder builds
*/
public interface Builder<T extends Builder<T, P>, P extends SoundPacket> {
/**
* <b>NOTE</b>: Make sure to set this to a unique value to avoid conflicts with other channels.
* This value needs to be the same when sending multiple packets of the same channel.
*
* @param channelId the channel ID of the sound packet
* @return the builder
*/
T channelId(UUID channelId);
/**
* Sets the sender of the sound packet.
* This must be set if this is created using {@link VoicechatServerApi#createPacket()}.
*
* @param sender the sender of the sound packet
* @return the builder
*/
T sender(UUID sender);
/**
* @param data the opus encoded audio data
* @return the builder
*/
T opusEncodedData(byte[] data);
/**
* Sets the sequence number of the sound packet.
* The sequence number is set automatically for everything except when using {@link VoicechatServerApi#createPacket()}.
* <br/>
* <br/>
* <b>NOTE</b>: You should only change this when using {@link VoicechatServerApi#createPacket()}.
*
* @param sequenceNumber the sequence number of the sound packet
* @return the builder
*/
T sequenceNumber(long sequenceNumber);
/**
* 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 sound packet
* @return the builder
*/
T category(@Nullable String category);
/**
* Builds the packet.
* <b>NOTE</b>: If you are missing required values, this will throw an {@link IllegalStateException}.
*
* @return the packet
*/
P build();
}
}
@@ -0,0 +1,18 @@
package de.maxhenkel.voicechat.api.packets;
/**
* The receiver of this event will hear the sound non-directional.
* This is what is basically used for group chats.
*/
public interface StaticSoundPacket extends SoundPacket {
/**
* A builder to build a static sound packet.
*
* @param <T> the builder itself
*/
public interface Builder<T extends StaticSoundPacket.Builder<T>> extends SoundPacket.Builder<T, StaticSoundPacket> {
}
}
@@ -0,0 +1,4 @@
/**
* All voice chat UDP packets.
*/
package de.maxhenkel.voicechat.api.packets;
@@ -1,19 +1,13 @@
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* javax.annotation.Nullable
*/
package de.maxhenkel.voicechat;
import de.maxhenkel.voicechat.Version;
import javax.annotation.Nullable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
public class BukkitVersion
extends Version {
public class BukkitVersion extends Version {
public static final Pattern BUKKIT_VERSION_REGEX = Pattern.compile("^(?<major>\\d+)\\.(?<minor>\\d+)(?:\\.(?<patch>\\d+))?-R(?<revision>[\\d.-]+)(?:-SNAPSHOT)?$");
protected final String revision;
public BukkitVersion(int major, int minor, int patch, String revision) {
@@ -35,15 +29,17 @@ extends Version {
@Nullable
public static BukkitVersion parseBukkitVersion(String bukkitVersion) {
Matcher targetMatcher = BUKKIT_VERSION_REGEX.matcher(bukkitVersion);
if (!targetMatcher.matches()) {
return null;
}
return BukkitVersion.fromRegex(targetMatcher);
}
@Nullable
public static BukkitVersion getTargetVersion() {
return BukkitVersion.parseBukkitVersion("1.8.8-R0.1-SNAPSHOT");
return parseBukkitVersion(BuildConstants.TARGET_BUKKIT_VERSION);
}
@Override
@@ -54,24 +50,26 @@ extends Version {
if (!(o instanceof BukkitVersion)) {
return false;
}
BukkitVersion that = (BukkitVersion)o;
if (this.major != that.major) {
BukkitVersion that = (BukkitVersion) o;
if (major != that.major) {
return false;
}
if (this.minor != that.minor) {
if (minor != that.minor) {
return false;
}
if (this.patch != that.patch) {
if (patch != that.patch) {
return false;
}
return this.revision.equals(that.revision);
return revision.equals(that.revision);
}
@Override
public String toString() {
if (this.patch <= 0) {
return this.major + "." + this.minor + "-R" + this.revision;
if (patch <= 0) {
return major + "." + minor + "-R" + revision;
}
return this.major + "." + this.minor + "." + this.patch + "-R" + this.revision;
return major + "." + minor + "." + patch + "-R" + revision;
}
}
@@ -1,20 +1,15 @@
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* javax.annotation.Nullable
*/
package de.maxhenkel.voicechat;
import de.maxhenkel.voicechat.Version;
import de.maxhenkel.voicechat.compatibility.ReflectionUtils;
import javax.annotation.Nullable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
public class PaperVersion
extends Version {
public class PaperVersion extends Version {
public static final Pattern PAPER_VERSION_REGEX = Pattern.compile("^(?<major>\\d+)\\.(?<minor>\\d+)(?:\\.(?<patch>\\d+))?\\.(?<build>.*)$");
private final String build;
public PaperVersion(int major, int minor, int patch, String build) {
@@ -39,21 +34,24 @@ extends Version {
@Nullable
public static PaperVersion parsePaperVersion(String version) {
if (!PaperVersion.isPaper()) {
if (!isPaper()) {
return null;
}
Matcher targetMatcher = PAPER_VERSION_REGEX.matcher(version);
if (!targetMatcher.matches()) {
return null;
}
return PaperVersion.fromRegex(targetMatcher);
}
@Override
public String toString() {
if (this.patch <= 0) {
return this.major + "." + this.minor + "." + this.build;
if (patch <= 0) {
return major + "." + minor + "." + build;
}
return this.major + "." + this.minor + "." + this.patch + "." + this.build;
return major + "." + minor + "." + patch + "." + build;
}
}
@@ -1,18 +1,11 @@
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* javax.annotation.Nullable
* org.bukkit.Bukkit
*/
package de.maxhenkel.voicechat;
import de.maxhenkel.voicechat.BukkitVersion;
import de.maxhenkel.voicechat.PaperVersion;
import javax.annotation.Nullable;
import org.bukkit.Bukkit;
import javax.annotation.Nullable;
public class Version {
protected final int major;
protected final int minor;
protected final int patch;
@@ -34,43 +27,46 @@ public class Version {
@Nullable
public static Version getVersion() {
return Version.parse(Bukkit.getBukkitVersion());
return parse(Bukkit.getBukkitVersion());
}
public int getMajor() {
return this.major;
return major;
}
public int getMinor() {
return this.minor;
return minor;
}
public int getPatch() {
return this.patch;
return patch;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || this.getClass() != o.getClass()) {
if (o == null || getClass() != o.getClass()) {
return false;
}
Version that = (Version)o;
if (this.major != that.major) {
Version that = (Version) o;
if (major != that.major) {
return false;
}
if (this.minor != that.minor) {
if (minor != that.minor) {
return false;
}
return this.patch == that.patch;
return patch == that.patch;
}
@Override
public int hashCode() {
int result = this.major;
result = 31 * result + this.minor;
result = 31 * result + this.patch;
int result = major;
result = 31 * result + minor;
result = 31 * result + patch;
return result;
}
}
@@ -0,0 +1,157 @@
package de.maxhenkel.voicechat;
import de.maxhenkel.configbuilder.ConfigBuilder;
import de.maxhenkel.voicechat.api.BukkitVoicechatService;
import de.maxhenkel.voicechat.command.VoiceChatCommands;
import de.maxhenkel.voicechat.compatibility.BukkitCompatibilityManager;
import de.maxhenkel.voicechat.compatibility.Compatibility;
import de.maxhenkel.voicechat.config.ServerConfig;
import de.maxhenkel.voicechat.config.Translations;
import de.maxhenkel.voicechat.integration.commodore.CommodoreCommands;
import de.maxhenkel.voicechat.integration.commodore.MissingArgumentTypeException;
import de.maxhenkel.voicechat.integration.placeholderapi.VoicechatExpansion;
import de.maxhenkel.voicechat.integration.viaversion.ViaVersionCompatibility;
import de.maxhenkel.voicechat.logging.JavaLoggingLogger;
import de.maxhenkel.voicechat.logging.VoicechatLogger;
import de.maxhenkel.voicechat.net.NetManager;
import de.maxhenkel.voicechat.plugins.PluginManager;
import de.maxhenkel.voicechat.plugins.impl.BukkitVoicechatServiceImpl;
import de.maxhenkel.voicechat.voice.server.ServerVoiceEvents;
import de.maxhenkel.voicechat.voice.server.ServerPlayerManager;
import me.lucko.commodore.Commodore;
import me.lucko.commodore.CommodoreProvider;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.ServicePriority;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.regex.Pattern;
public final class Voicechat extends JavaPlugin {
public static Voicechat INSTANCE;
public static final String MODID = "voicechat";
public static VoicechatLogger LOGGER;
public static int COMPATIBILITY_VERSION = BuildConstants.COMPATIBILITY_VERSION;
public static ServerConfig SERVER_CONFIG;
public static Translations TRANSLATIONS;
public static ServerVoiceEvents SERVER;
public static BukkitVoicechatServiceImpl apiService;
public static NetManager netManager;
public static Compatibility compatibility;
public static final int MAX_GROUP_NAME_LENGTH = 24;
public static final Pattern GROUP_REGEX = Pattern.compile("^[^\\p{C}\\s][^\\p{C}]{0,23}$");
@Override
public void onEnable() {
INSTANCE = this;
LOGGER = new JavaLoggingLogger(getLogger());
if (debugMode()) {
LOGGER.warn("Running in debug mode - Don't leave this enabled in production!");
}
try {
compatibility = BukkitCompatibilityManager.loadCompatibility();
if (compatibility == null) {
disablePlugin();
return;
}
} catch (Throwable t) {
LOGGER.fatal("An unexpected error occurred while loading compatibility", t);
disablePlugin();
return;
}
LOGGER.info("Compatibility version {}", COMPATIBILITY_VERSION);
SERVER_CONFIG = ConfigBuilder.builder(ServerConfig::new).path(getDataFolder().toPath().resolve("voicechat-server.properties")).build();
TRANSLATIONS = ConfigBuilder.builder(Translations::new).path(getDataFolder().toPath().resolve("translations.properties")).build();
netManager = new NetManager();
netManager.onEnable();
apiService = new BukkitVoicechatServiceImpl();
getServer().getServicesManager().register(BukkitVoicechatService.class, apiService, this, ServicePriority.Normal);
PluginCommand voicechatCommand = getCommand(VoiceChatCommands.VOICECHAT_COMMAND);
if (voicechatCommand != null) {
VoiceChatCommands voiceChatCommands = new VoiceChatCommands();
voicechatCommand.setExecutor(voiceChatCommands);
voicechatCommand.setTabCompleter(voiceChatCommands);
try {
if (CommodoreProvider.isSupported()) {
Commodore commodore = CommodoreProvider.getCommodore(this);
CommodoreCommands.registerCompletions(commodore);
LOGGER.info("Successfully initialized commodore command completion");
} else {
LOGGER.warn("Commodore command completion is not supported");
}
} catch (MissingArgumentTypeException t) {
LOGGER.warn("Failed to initialize commodore command completion: {}", t.getMessage());
} catch (Throwable t) {
LOGGER.warn("Failed to initialize commodore command completion", t);
}
} else {
LOGGER.error("Failed to register commands");
}
try {
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
new VoicechatExpansion().register();
LOGGER.info("Successfully registered PlaceholderAPI expansion");
}
} catch (Throwable t) {
LOGGER.error("Failed to register PlaceholderAPI expansion", t);
}
try {
if (Bukkit.getPluginManager().getPlugin("ViaVersion") != null) {
ViaVersionCompatibility.register();
LOGGER.info("Successfully added ViaVersion mappings");
}
} catch (Throwable t) {
LOGGER.error("Failed to add ViaVersion mappings", t);
}
if (System.getProperty("VOICECHAT_RELOADED") != null) {
LOGGER.error("{} does not support reloads! Expect that things will break!", BuildConstants.PLUGIN_NAME);
}
System.setProperty("VOICECHAT_RELOADED", "true");
compatibility.runTask(() -> {
SERVER = new ServerVoiceEvents();
PluginManager.instance().init();
SERVER.init();
Bukkit.getPluginManager().registerEvents(SERVER, this);
ServerPlayerManager.init(this);
});
}
private void disablePlugin() {
LOGGER.fatal("Disabling {}", BuildConstants.PLUGIN_NAME);
Bukkit.getPluginManager().disablePlugin(this);
}
@Override
public void onDisable() {
if (netManager != null) {
netManager.onDisable();
}
getServer().getServicesManager().unregister(apiService);
if (SERVER != null) {
SERVER.getServer().close();
}
}
public static boolean debugMode() {
return System.getProperty("voicechat.debug") != null;
}
}
@@ -1,16 +1,6 @@
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* javax.annotation.Nullable
* org.bukkit.command.Command
* org.bukkit.command.CommandExecutor
* org.bukkit.command.CommandSender
* org.bukkit.command.TabCompleter
* org.bukkit.entity.Player
*/
package de.maxhenkel.voicechat.command;
import de.maxhenkel.voicechat.BuildConstants;
import de.maxhenkel.voicechat.Voicechat;
import de.maxhenkel.voicechat.net.NetManager;
import de.maxhenkel.voicechat.permission.PermissionManager;
@@ -19,72 +9,76 @@ import de.maxhenkel.voicechat.voice.server.ClientConnection;
import de.maxhenkel.voicechat.voice.server.Group;
import de.maxhenkel.voicechat.voice.server.PingManager;
import de.maxhenkel.voicechat.voice.server.Server;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
public class VoiceChatCommands
implements CommandExecutor,
TabCompleter {
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class VoiceChatCommands implements CommandExecutor, TabCompleter {
public static final String VOICECHAT_COMMAND = "voicechat";
@Override
public boolean onCommand(CommandSender commandSender, Command command, String label, String[] args) {
if (VoiceChatCommands.checkNoVoicechat(commandSender)) {
if (checkNoVoicechat(commandSender)) {
return true;
}
if (!(commandSender instanceof Player)) {
return true;
}
Player sender = (Player)commandSender;
Player sender = (Player) commandSender;
if (args.length >= 1) {
if (args[0].equalsIgnoreCase("help")) {
return this.helpCommand(sender, command, label, args);
}
if (args[0].equalsIgnoreCase("test")) {
return helpCommand(sender, command, label, args);
} else if (args[0].equalsIgnoreCase("test")) {
if (commandSender.hasPermission(PermissionManager.ADMIN_PERMISSION)) {
return this.testCommand(sender, command, label, args);
return testCommand(sender, command, label, args);
} else {
return true;
}
return true;
}
if (args[0].equalsIgnoreCase("invite")) {
return this.inviteCommand(sender, command, label, args);
}
if (args[0].equalsIgnoreCase("join")) {
return this.joinCommand(sender, command, label, args);
}
if (args[0].equalsIgnoreCase("leave")) {
return this.leaveCommand(sender, command, label, args);
} else if (args[0].equalsIgnoreCase("invite")) {
return inviteCommand(sender, command, label, args);
} else if (args[0].equalsIgnoreCase("join")) {
return joinCommand(sender, command, label, args);
} else if (args[0].equalsIgnoreCase("leave")) {
return leaveCommand(sender, command, label, args);
}
}
return this.helpCommand(sender, command, label, args);
return helpCommand(sender, command, label, args);
}
@Nullable
@Override
public List<String> onTabComplete(CommandSender commandSender, Command command, String label, String[] args) {
String arg;
if (args.length == 1) {
return this.tabCompleteList(args[0], Arrays.asList("help", "test", "invite", "join", "leave"));
return tabCompleteList(args[0], Arrays.asList("help", "test", "invite", "join", "leave"));
}
if (args.length == 2) {
arg = args[0];
String arg = args[0];
if (arg.equals("test") || arg.equals("invite")) {
return null;
} else if (arg.startsWith("join")) {
return Collections.emptyList();
}
}
if (args.length == 3) {
String arg = args[0];
if (arg.startsWith("join")) {
return Collections.emptyList();
}
}
if (args.length == 3 && (arg = args[0]).startsWith("join")) {
return Collections.emptyList();
}
return Collections.emptyList();
}
@@ -101,31 +95,33 @@ TabCompleter {
return true;
}
private boolean testCommand(final Player commandSender, Command command, String label, String[] args) {
private boolean testCommand(Player commandSender, Command command, String label, String[] args) {
if (args.length < 2) {
return false;
}
Player player = VoiceChatCommands.parsePlayer((CommandSender)commandSender, args[1]);
Player player = parsePlayer(commandSender, args[1]);
if (player == null) {
NetManager.sendMessage(commandSender, "argument.entity.notfound.player", new String[0]);
NetManager.sendMessage(commandSender, "argument.entity.notfound.player");
return true;
}
if (!Voicechat.SERVER.isCompatible(player)) {
NetManager.sendMessage(commandSender, "message.voicechat.player_no_voicechat", player.getDisplayName(), "Simple Voice Chat");
NetManager.sendMessage(commandSender, "message.voicechat.player_no_voicechat", player.getDisplayName(), BuildConstants.PLUGIN_NAME);
return true;
}
ClientConnection clientConnection = Voicechat.SERVER.getServer().getConnections().get(player.getUniqueId());
if (clientConnection == null) {
NetManager.sendMessage(commandSender, "message.voicechat.client_not_connected", new String[0]);
NetManager.sendMessage(commandSender, "message.voicechat.client_not_connected");
return true;
}
try {
NetManager.sendMessage(commandSender, "message.voicechat.sending_ping", new String[0]);
Voicechat.SERVER.getServer().getPingManager().sendPing(clientConnection, 500L, 10, new PingManager.PingListener(){
final /* synthetic */ VoiceChatCommands this$0;
{
this.this$0 = this$0;
}
NetManager.sendMessage(commandSender, "message.voicechat.sending_ping");
Voicechat.SERVER.getServer().getPingManager().sendPing(clientConnection, 500, 10, new PingManager.PingListener() {
@Override
public void onPong(int attempts, long pingMilliseconds) {
@@ -138,7 +134,7 @@ TabCompleter {
@Override
public void onFailedAttempt(int attempts) {
NetManager.sendMessage(commandSender, "message.voicechat.ping_retry", new String[0]);
NetManager.sendMessage(commandSender, "message.voicechat.ping_retry");
}
@Override
@@ -146,9 +142,8 @@ TabCompleter {
NetManager.sendMessage(commandSender, "message.voicechat.ping_timed_out", String.valueOf(attempts));
}
});
NetManager.sendMessage(commandSender, "message.voicechat.ping_sent_waiting", new String[0]);
}
catch (Exception e) {
NetManager.sendMessage(commandSender, "message.voicechat.ping_sent_waiting");
} catch (Exception e) {
NetManager.sendMessage(commandSender, "message.voicechat.failed_to_send_ping", e.getMessage());
Voicechat.LOGGER.warn("Failed to send ping", e);
}
@@ -159,128 +154,156 @@ TabCompleter {
if (args.length < 2) {
return false;
}
Player player = VoiceChatCommands.parsePlayer((CommandSender)commandSender, args[1]);
Player player = parsePlayer(commandSender, args[1]);
if (player == null || !Voicechat.compatibility.canSee(commandSender, player)) {
NetManager.sendMessage(commandSender, "argument.entity.notfound.player", new String[0]);
NetManager.sendMessage(commandSender, "argument.entity.notfound.player");
return true;
}
PlayerState state = Voicechat.SERVER.getServer().getPlayerStateManager().getState(commandSender.getUniqueId());
if (state == null || !state.hasGroup()) {
NetManager.sendMessage(commandSender, "message.voicechat.not_in_group", new String[0]);
NetManager.sendMessage(commandSender, "message.voicechat.not_in_group");
return true;
}
Group group = Voicechat.SERVER.getServer().getGroupManager().getGroup(state.getGroup());
if (group == null) {
return true;
}
if (!Voicechat.SERVER.isCompatible(player)) {
NetManager.sendMessage(commandSender, "message.voicechat.player_no_voicechat", player.getDisplayName(), "Simple Voice Chat");
NetManager.sendMessage(commandSender, "message.voicechat.player_no_voicechat", player.getDisplayName(), BuildConstants.PLUGIN_NAME);
return true;
}
String passwordSuffix = group.getPassword() == null ? "" : " \"" + group.getPassword() + "\"";
String cmd = "/voicechat join " + group.getId().toString() + passwordSuffix;
Voicechat.compatibility.sendInviteMessage(player, commandSender, group.getName(), cmd);
NetManager.sendMessage(commandSender, "message.voicechat.invite_successful", player.getName());
return true;
}
private boolean joinCommand(Player commandSender, Command command, String label, String[] args) {
UUID groupUUID;
int argIndex;
if (args.length < 2) {
return false;
}
int argIndex = 1;
UUID groupUUID;
try {
groupUUID = UUID.fromString(args[argIndex]);
}
catch (Exception e) {
} catch (Exception e) {
String groupName;
if (args[argIndex].startsWith("\"")) {
String[] split;
StringBuilder sb = new StringBuilder();
for (argIndex = 1; argIndex < args.length; ++argIndex) {
for (; argIndex < args.length; argIndex++) {
sb.append(args[argIndex]).append(" ");
if (args[argIndex].endsWith("\"") && !args[argIndex].endsWith("\\\"")) break;
if (args[argIndex].endsWith("\"") && !args[argIndex].endsWith("\\\"")) {
break;
}
}
if ((split = (groupName = sb.toString().trim()).split("\"")).length > 1) {
groupName = sb.toString().trim();
String[] split = groupName.split("\"");
if (split.length > 1) {
groupName = split[1];
}
} else {
groupName = args[argIndex];
}
groupUUID = this.getGroupUUID(commandSender, Voicechat.SERVER.getServer(), groupName);
groupUUID = getGroupUUID(commandSender, Voicechat.SERVER.getServer(), groupName);
}
if (groupUUID == null) {
return true;
}
argIndex++;
String password = null;
if (args.length >= ++argIndex + 1) {
String[] split;
if (args.length >= argIndex + 1) {
StringBuilder sb = new StringBuilder();
while (argIndex < args.length) {
for (; argIndex < args.length; argIndex++) {
sb.append(args[argIndex]).append(" ");
++argIndex;
}
password = sb.toString().trim();
if (password.startsWith("\"") && (split = password.split("\"")).length > 1) {
password = split[1];
if (password.startsWith("\"")) {
String[] split = password.split("\"");
if (split.length > 1) {
password = split[1];
}
}
}
VoiceChatCommands.joinGroup(commandSender, groupUUID, password);
joinGroup(commandSender, groupUUID, password);
return true;
}
private UUID getGroupUUID(Player commandSender, Server server, String groupName) {
List groups = server.getGroupManager().getGroups().values().stream().filter(group -> group.getName().equals(groupName)).collect(Collectors.toList());
List<Group> groups = server.getGroupManager().getGroups().values().stream().filter(group -> group.getName().equals(groupName)).collect(Collectors.toList());
if (groups.isEmpty()) {
NetManager.sendMessage(commandSender, "message.voicechat.group_does_not_exist", new String[0]);
NetManager.sendMessage(commandSender, "message.voicechat.group_does_not_exist");
return null;
}
if (groups.size() > 1) {
NetManager.sendMessage(commandSender, "message.voicechat.group_name_not_unique", new String[0]);
NetManager.sendMessage(commandSender, "message.voicechat.group_name_not_unique");
return null;
}
return ((Group)groups.get(0)).getId();
return groups.get(0).getId();
}
private static void joinGroup(Player commandSender, UUID groupID, @Nullable String password) {
if (!Voicechat.SERVER_CONFIG.groupsEnabled.get().booleanValue()) {
NetManager.sendMessage(commandSender, "message.voicechat.groups_disabled", new String[0]);
if (!Voicechat.SERVER_CONFIG.groupsEnabled.get()) {
NetManager.sendMessage(commandSender, "message.voicechat.groups_disabled");
return;
}
Server server = Voicechat.SERVER.getServer();
if (!commandSender.hasPermission(PermissionManager.GROUPS_PERMISSION)) {
NetManager.sendMessage(commandSender, "message.voicechat.no_group_permission", new String[0]);
NetManager.sendMessage(commandSender, "message.voicechat.no_group_permission");
return;
}
Group group = server.getGroupManager().getGroup(groupID);
if (group == null) {
NetManager.sendMessage(commandSender, "message.voicechat.group_does_not_exist", new String[0]);
NetManager.sendMessage(commandSender, "message.voicechat.group_does_not_exist");
return;
}
server.getGroupManager().joinGroup(group, commandSender, password);
NetManager.sendMessage(commandSender, "message.voicechat.join_successful", group.getName());
NetManager.sendMessage(commandSender, "message.voicechat.join_successful", group.getName()); //TODO Make group name green
}
private boolean leaveCommand(Player commandSender, Command command, String label, String[] args) {
if (!Voicechat.SERVER_CONFIG.groupsEnabled.get().booleanValue()) {
NetManager.sendMessage(commandSender, "message.voicechat.groups_disabled", new String[0]);
if (!Voicechat.SERVER_CONFIG.groupsEnabled.get()) {
NetManager.sendMessage(commandSender, "message.voicechat.groups_disabled");
return true;
}
Server server = Voicechat.SERVER.getServer();
PlayerState state = server.getPlayerStateManager().getState(commandSender.getUniqueId());
if (state == null || !state.hasGroup()) {
NetManager.sendMessage(commandSender, "message.voicechat.not_in_group", new String[0]);
NetManager.sendMessage(commandSender, "message.voicechat.not_in_group");
return true;
}
server.getGroupManager().leaveGroup(commandSender);
NetManager.sendMessage(commandSender, "message.voicechat.leave_successful", new String[0]);
NetManager.sendMessage(commandSender, "message.voicechat.leave_successful");
return true;
}
private static boolean checkNoVoicechat(CommandSender commandSender) {
if (commandSender instanceof Player) {
if (Voicechat.SERVER.isCompatible((Player)commandSender)) {
if (Voicechat.SERVER.isCompatible((Player) commandSender)) {
return false;
}
commandSender.sendMessage(String.format(Voicechat.TRANSLATIONS.voicechatNeededForCommandMessage.get(), "Simple Voice Chat"));
@@ -299,10 +322,8 @@ TabCompleter {
try {
UUID uuid = UUID.fromString(playerArg);
return commandSender.getServer().getPlayer(uuid);
}
catch (IllegalArgumentException e) {
} catch (IllegalArgumentException e) {
return null;
}
}
}
}
@@ -0,0 +1,217 @@
package de.maxhenkel.voicechat.compatibility;
import de.maxhenkel.voicechat.Voicechat;
import de.maxhenkel.voicechat.util.Key;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerEvent;
import org.bukkit.plugin.Plugin;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.function.Consumer;
import static de.maxhenkel.voicechat.compatibility.ReflectionUtils.*;
public abstract class BaseCompatibility implements Compatibility {
private String baseBukkitPackage;
private String baseServerPackage;
private Method addChannel;
private Method removeChannel;
@Nullable
private Class<? extends PlayerEvent> playerHideEntityEvent;
@Nullable
private Method playerHideEntityEventGetEntity;
@Nullable
private Class<? extends PlayerEvent> playerShowEntityEvent;
@Nullable
private Method playerShowEntityEventGetEntity;
private Consumer<Runnable> runTask;
private TaskScheduler taskScheduler;
@Override
public void init() throws Exception {
Compatibility.super.init();
baseBukkitPackage = Bukkit.getServer().getClass().getPackage().getName();
baseServerPackage = callMethod(Bukkit.getServer(), "getServer").getClass().getPackage().getName();
Class<?> craftPlayer = getBukkitClass("entity.CraftPlayer");
addChannel = getMethod(craftPlayer, new String[]{"addChannel"}, new Class[]{String.class});
removeChannel = getMethod(craftPlayer, new String[]{"removeChannel"}, new Class[]{String.class});
try {
if (doesClassExist("org.bukkit.event.player.PlayerHideEntityEvent") && doesClassExist("org.bukkit.event.player.PlayerShowEntityEvent")) {
Class<?> hide = ReflectionUtils.getClazz("org.bukkit.event.player.PlayerHideEntityEvent");
if (!PlayerEvent.class.isAssignableFrom(hide)) {
throw new CompatibilityReflectionException("PlayerHideEntityEvent is not a subclass of PlayerEvent");
}
Class<?> show = ReflectionUtils.getClazz("org.bukkit.event.player.PlayerShowEntityEvent");
if (!PlayerEvent.class.isAssignableFrom(show)) {
throw new CompatibilityReflectionException("PlayerShowEntityEvent is not a subclass of PlayerEvent");
}
playerShowEntityEventGetEntity = getMethod(show, "getEntity");
playerHideEntityEventGetEntity = getMethod(hide, "getEntity");
playerShowEntityEvent = (Class<? extends PlayerEvent>) show;
playerHideEntityEvent = (Class<? extends PlayerEvent>) hide;
} else {
Voicechat.LOGGER.warn("Vanish plugin integration is not supported");
}
} catch (CompatibilityReflectionException e) {
Voicechat.LOGGER.warn("Vanish plugin integration is disabled");
Voicechat.LOGGER.warn("Failed to load vanish compatibility", e);
playerHideEntityEvent = null;
playerShowEntityEvent = null;
}
runTask = runnable -> Bukkit.getScheduler().runTask(Voicechat.INSTANCE, runnable);
taskScheduler = new TaskScheduler() {
@Override
public void scheduleSyncRepeatingTask(Runnable runnable, long delay, long period) {
Bukkit.getScheduler().scheduleSyncRepeatingTask(Voicechat.INSTANCE, runnable, delay, period);
}
@Override
public void runTaskLater(Runnable runnable, long delay) {
Bukkit.getScheduler().runTaskLater(Voicechat.INSTANCE, runnable, delay);
}
};
if (doesMethodExist(Bukkit.class, "getGlobalRegionScheduler")) {
Object globalRegionScheduler = callMethod(Bukkit.class, "getGlobalRegionScheduler");
Method run = getMethod(globalRegionScheduler.getClass(), new String[]{"run"}, new Class[]{Plugin.class, Consumer.class});
runTask = runnable -> call(run, globalRegionScheduler, Voicechat.INSTANCE, (Consumer<?>) (task) -> runnable.run());
if (
doesMethodExist(globalRegionScheduler.getClass(), "runAtFixedRate", Plugin.class, Consumer.class, long.class, long.class)
&& doesMethodExist(globalRegionScheduler.getClass(), "runDelayed", Plugin.class, Consumer.class, long.class)
) {
Method runAtFixedRate = getMethod(globalRegionScheduler.getClass(), new String[]{"runAtFixedRate"}, new Class[]{Plugin.class, Consumer.class, long.class, long.class});
Method runDelayed = getMethod(globalRegionScheduler.getClass(), new String[]{"runDelayed"}, new Class[]{Plugin.class, Consumer.class, long.class});
taskScheduler = new TaskScheduler() {
@Override
public void scheduleSyncRepeatingTask(Runnable runnable, long delay, long period) {
call(runAtFixedRate, globalRegionScheduler, Voicechat.INSTANCE, (Consumer<?>) (task) -> runnable.run(), delay <= 0 ? 1 : delay, period);
}
@Override
public void runTaskLater(Runnable runnable, long delay) {
call(runDelayed, globalRegionScheduler, Voicechat.INSTANCE, (Consumer<?>) (task) -> runnable.run(), delay <= 0 ? 1 : delay);
}
};
}
}
}
@Override
public void addChannel(Player player, String channel) {
call(addChannel, player, channel);
}
@Override
public void removeChannel(Player player, String channel) {
call(removeChannel, player, channel);
}
@Override
public Key createNamespacedKey(String key) {
return Key.of(key);
}
@Override
public void runTask(Runnable runnable) {
runTask.accept(runnable);
}
@Override
public void scheduleSyncRepeatingTask(Runnable runnable, long delay, long period) {
taskScheduler.scheduleSyncRepeatingTask(runnable, delay, period);
}
@Override
public void runTaskLater(Runnable runnable, long delay) {
taskScheduler.runTaskLater(runnable, delay);
}
@Override
public boolean canSee(Player player, Player other) {
if (playerHideEntityEvent == null || playerShowEntityEvent == null) {
// Don't use canSee if hide and show events are missing
// This avoids state desyncs on older versions that have canSee but not the events
return true;
}
return player.canSee(other);
}
@Override
public void registerPlayerHideEvent(Consumer<PlayerHideEvent> event) {
if (playerHideEntityEvent == null) {
return;
}
Bukkit.getPluginManager().registerEvent(
playerHideEntityEvent,
new Listener() {
},
EventPriority.NORMAL,
(listener, evt) -> {
if (!evt.getClass().isAssignableFrom(playerHideEntityEvent)) {
return;
}
PlayerEvent playerEvent = (PlayerEvent) evt;
Object entityObj = call(playerHideEntityEventGetEntity, playerEvent);
if (!(entityObj instanceof Player)) {
return;
}
event.accept(new PlayerHideEvent((Player) entityObj, playerEvent.getPlayer()));
},
Voicechat.INSTANCE
);
}
@Override
public void registerPlayerShowEvent(Consumer<PlayerShowEvent> event) {
if (playerShowEntityEvent == null) {
return;
}
Bukkit.getPluginManager().registerEvent(
playerShowEntityEvent,
new Listener() {
},
EventPriority.NORMAL,
(listener, evt) -> {
if (!evt.getClass().isAssignableFrom(playerShowEntityEvent)) {
return;
}
PlayerEvent playerEvent = (PlayerEvent) evt;
Object entityObj = call(playerShowEntityEventGetEntity, playerEvent);
if (!(entityObj instanceof Player)) {
return;
}
event.accept(new PlayerShowEvent((Player) entityObj, playerEvent.getPlayer()));
},
Voicechat.INSTANCE
);
}
@Override
public String getBaseBukkitPackage() {
return baseBukkitPackage;
}
@Override
public String getBaseServerPackage() {
return baseServerPackage;
}
private interface TaskScheduler {
void scheduleSyncRepeatingTask(Runnable runnable, long delay, long period);
void runTaskLater(Runnable runnable, long delay);
}
}
@@ -1,123 +1,16 @@
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* javax.annotation.Nullable
* org.bukkit.Bukkit
*/
package de.maxhenkel.voicechat.compatibility;
import de.maxhenkel.voicechat.Version;
import de.maxhenkel.voicechat.Voicechat;
import de.maxhenkel.voicechat.compatibility.Compatibility;
import de.maxhenkel.voicechat.compatibility.Compatibility1_12;
import de.maxhenkel.voicechat.compatibility.Compatibility1_16;
import de.maxhenkel.voicechat.compatibility.Compatibility1_17;
import de.maxhenkel.voicechat.compatibility.Compatibility1_18;
import de.maxhenkel.voicechat.compatibility.Compatibility1_19;
import de.maxhenkel.voicechat.compatibility.Compatibility1_19_1;
import de.maxhenkel.voicechat.compatibility.Compatibility1_20;
import de.maxhenkel.voicechat.compatibility.Compatibility1_20_2;
import de.maxhenkel.voicechat.compatibility.Compatibility1_20_3;
import de.maxhenkel.voicechat.compatibility.Compatibility1_21_5;
import de.maxhenkel.voicechat.compatibility.Compatibility1_8;
import de.maxhenkel.voicechat.compatibility.CompatibilityReflectionException;
import de.maxhenkel.voicechat.compatibility.FallbackCompatibility;
import de.maxhenkel.voicechat.compatibility.SpigotCompatibility;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
import org.bukkit.Bukkit;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
public class BukkitCompatibilityManager {
private static final Map<Version, Compatibility> COMPATIBILITIES = new HashMap<Version, Compatibility>();
@Nullable
public static Compatibility loadCompatibility() {
Version forcedVersion = BukkitCompatibilityManager.getForcedVersion();
Version version = forcedVersion != null ? forcedVersion : Version.getVersion();
if (version == null) {
Voicechat.LOGGER.info("Failed to parse version {}, trying to fall back to Spigot API compatibility mode", Bukkit.getBukkitVersion());
return BukkitCompatibilityManager.tryLoadSpigot();
}
Voicechat.LOGGER.info("Initializing compatibility for version {}", version);
if (version.getMajor() == 1 && version.getMinor() <= 7) {
Voicechat.LOGGER.fatal("You are running a server that's too old to be able to join with any voice chat client", new Object[0]);
return null;
}
Compatibility compatibility = COMPATIBILITIES.get(version);
if (compatibility != null) {
try {
compatibility.init();
}
catch (Throwable t) {
compatibility = null;
Voicechat.LOGGER.warn("Failed to load compatibility for version {}", version, t);
}
}
if (compatibility == null) {
Voicechat.LOGGER.warn("Incompatible version {}, trying to fall back to Spigot API compatibility mode", version);
compatibility = BukkitCompatibilityManager.tryLoadSpigot();
}
if (compatibility == null) {
compatibility = FallbackCompatibility.INSTANCE;
try {
compatibility.init();
Voicechat.LOGGER.warn("Falling back to Bukkit compatibility mode, expect issues and lack of features", new Object[0]);
Voicechat.LOGGER.warn("Chat messages won't be translatable", new Object[0]);
}
catch (CompatibilityReflectionException e) {
compatibility = null;
Voicechat.LOGGER.warn("Failed to load Bukkit compatibility mode: {}", e.getMessage());
}
catch (Throwable t) {
compatibility = null;
Voicechat.LOGGER.warn("Failed to load Bukkit compatibility mode", t);
}
}
if (compatibility == null) {
Voicechat.LOGGER.fatal("Could not load any compatibility for {}", version);
}
return compatibility;
}
@Nullable
private static Compatibility tryLoadSpigot() {
if (!SpigotCompatibility.isSpigotCompatible()) {
Voicechat.LOGGER.warn("Spigot API not found", new Object[0]);
return null;
}
SpigotCompatibility compatibility = SpigotCompatibility.INSTANCE;
try {
compatibility.init();
Voicechat.LOGGER.warn("Falling back to Spigot API compatibility mode, expect issues and lack of features", new Object[0]);
}
catch (CompatibilityReflectionException e) {
compatibility = null;
Voicechat.LOGGER.warn("Failed to load Spigot API compatibility mode: {}", e.getMessage());
}
catch (Throwable t) {
compatibility = null;
Voicechat.LOGGER.warn("Failed to load Spigot API compatibility mode", t);
}
return compatibility;
}
@Nullable
public static Version getForcedVersion() {
String property = System.getProperty("voicechat.compatibility");
if (property == null) {
return null;
}
Version forcedVersion = Version.parse(property);
if (forcedVersion == null) {
Voicechat.LOGGER.warn("Failed to parse forced compatibility version: {}", property);
return null;
}
Voicechat.LOGGER.info("Forcing compatibility for {}", forcedVersion);
Voicechat.LOGGER.warn("Forcing version compatibility could lead to issues - use with caution!", new Object[0]);
return forcedVersion;
}
private static final Map<Version, Compatibility> COMPATIBILITIES = new HashMap<>();
static {
COMPATIBILITIES.put(Compatibility1_21_5.VERSION_PAPER_26_3, Compatibility1_21_5.INSTANCE);
@@ -169,5 +62,102 @@ public class BukkitCompatibilityManager {
COMPATIBILITIES.put(Compatibility1_12.VERSION_1_12, Compatibility1_12.INSTANCE);
COMPATIBILITIES.put(Compatibility1_8.VERSION_1_8_8, Compatibility1_8.INSTANCE);
}
}
@Nullable
public static Compatibility loadCompatibility() {
Version version;
Version forcedVersion = getForcedVersion();
if (forcedVersion != null) {
version = forcedVersion;
} else {
version = Version.getVersion();
}
if (version == null) {
Voicechat.LOGGER.info("Failed to parse version {}, trying to fall back to Spigot API compatibility mode", Bukkit.getBukkitVersion());
return tryLoadSpigot();
}
Voicechat.LOGGER.info("Initializing compatibility for version {}", version);
if (version.getMajor() == 1 && version.getMinor() <= 7) {
Voicechat.LOGGER.fatal("You are running a server that's too old to be able to join with any voice chat client");
return null;
}
Compatibility compatibility = COMPATIBILITIES.get(version);
if (compatibility != null) {
try {
compatibility.init();
} catch (Throwable t) {
compatibility = null;
Voicechat.LOGGER.warn("Failed to load compatibility for version {}", version, t);
}
}
if (compatibility == null) {
Voicechat.LOGGER.warn("Incompatible version {}, trying to fall back to Spigot API compatibility mode", version);
compatibility = tryLoadSpigot();
}
if (compatibility == null) {
compatibility = FallbackCompatibility.INSTANCE;
try {
compatibility.init();
Voicechat.LOGGER.warn("Falling back to Bukkit compatibility mode, expect issues and lack of features");
Voicechat.LOGGER.warn("Chat messages won't be translatable");
} catch (CompatibilityReflectionException e) {
compatibility = null;
// Only log the message in case reflection fails
Voicechat.LOGGER.warn("Failed to load Bukkit compatibility mode: {}", e.getMessage());
} catch (Throwable t) {
compatibility = null;
Voicechat.LOGGER.warn("Failed to load Bukkit compatibility mode", t);
}
}
if (compatibility == null) {
Voicechat.LOGGER.fatal("Could not load any compatibility for {}", version);
}
return compatibility;
}
@Nullable
private static Compatibility tryLoadSpigot() {
if (!SpigotCompatibility.isSpigotCompatible()) {
Voicechat.LOGGER.warn("Spigot API not found");
return null;
}
Compatibility compatibility = SpigotCompatibility.INSTANCE;
try {
compatibility.init();
Voicechat.LOGGER.warn("Falling back to Spigot API compatibility mode, expect issues and lack of features");
} catch (CompatibilityReflectionException e) {
compatibility = null;
// Only log the message in case reflection fails
Voicechat.LOGGER.warn("Failed to load Spigot API compatibility mode: {}", e.getMessage());
} catch (Throwable t) {
compatibility = null;
Voicechat.LOGGER.warn("Failed to load Spigot API compatibility mode", t);
}
return compatibility;
}
@Nullable
public static Version getForcedVersion() {
String property = System.getProperty("voicechat.compatibility");
if (property == null) {
return null;
}
Version forcedVersion = Version.parse(property);
if (forcedVersion == null) {
Voicechat.LOGGER.warn("Failed to parse forced compatibility version: {}", property);
return null;
}
Voicechat.LOGGER.info("Forcing compatibility for {}", forcedVersion);
Voicechat.LOGGER.warn("Forcing version compatibility could lead to issues - use with caution!");
return forcedVersion;
}
}
@@ -0,0 +1,76 @@
package de.maxhenkel.voicechat.compatibility;
import com.mojang.brigadier.arguments.ArgumentType;
import de.maxhenkel.voicechat.util.Key;
import org.bukkit.entity.Player;
import javax.annotation.Nullable;
import java.util.function.Consumer;
public interface Compatibility {
default void init() throws Exception {
}
void addChannel(Player player, String channel);
void removeChannel(Player player, String channel);
Key createNamespacedKey(String key);
void sendTranslationMessage(Player player, String key, String... args);
void sendStatusMessage(Player player, String key, String... args);
void sendInviteMessage(Player player, Player commandSender, String groupName, String joinCommand);
void sendIncompatibleMessage(Player player, String pluginVersion, String pluginName);
void runTask(Runnable runnable);
void scheduleSyncRepeatingTask(Runnable runnable, long delay, long period);
void runTaskLater(Runnable runnable, long delay);
boolean canSee(Player player, Player other);
void registerPlayerHideEvent(Consumer<PlayerHideEvent> event);
void registerPlayerShowEvent(Consumer<PlayerShowEvent> event);
@Nullable
ArgumentType<?> playerArgument();
@Nullable
ArgumentType<?> uuidArgument();
String getBaseBukkitPackage();
String getBaseServerPackage();
/**
* @param classNames the class names including the package name, starting after the bukkit the base package e.g. <code>org.bukkit.craftbukkit.v1_20_R3.CraftServer</code> would be <code>CraftServer</code>
* @return the class
*/
default Class<?> getBukkitClass(String... classNames) {
for (String className : classNames) {
try {
return Class.forName(String.format("%s.%s", getBaseBukkitPackage(), className));
} catch (Throwable ignored) {
}
}
throw new CompatibilityReflectionException(String.format("Could not find any of the following classes: %s", String.join(", ", classNames)));
}
default Class<?> getServerClass(String... classNames) {
for (String className : classNames) {
try {
return Class.forName(String.format("%s.%s", getBaseServerPackage(), className));
} catch (Throwable ignored) {
}
}
throw new CompatibilityReflectionException(String.format("Could not find any of the following classes: %s", String.join(", ", classNames)));
}
}
@@ -0,0 +1,82 @@
package de.maxhenkel.voicechat.compatibility;
import com.mojang.brigadier.arguments.ArgumentType;
import de.maxhenkel.voicechat.BukkitVersion;
import de.maxhenkel.voicechat.util.Key;
import org.bukkit.entity.Player;
import static de.maxhenkel.voicechat.compatibility.ReflectionUtils.*;
public class Compatibility1_12 extends JsonMessageBaseCompatibility {
public static final String CHANNEL = "vc";
public static final BukkitVersion VERSION_1_12_2 = BukkitVersion.parseBukkitVersion("1.12.2-R0.1");
public static final BukkitVersion VERSION_1_12_1 = BukkitVersion.parseBukkitVersion("1.12.1-R0.1");
public static final BukkitVersion VERSION_1_12 = BukkitVersion.parseBukkitVersion("1.12-R0.1");
public static final Compatibility1_12 INSTANCE = new Compatibility1_12();
@Override
public Key createNamespacedKey(String key) {
return Key.of(CHANNEL, key);
}
@Override
public void sendJsonMessage(Player player, String json) {
Class<?> chatMessageTypeClass = getServerClass("ChatMessageType");
Object b = getField(chatMessageTypeClass, "CHAT");
send(player, json, b);
}
@Override
public void sendJsonStatusMessage(Player player, String json) {
Class<?> chatMessageTypeClass = getServerClass("ChatMessageType");
Object b = getField(chatMessageTypeClass, "GAME_INFO");
send(player, json, b);
}
@Override
public String createTranslationMessage(String key, String... args) {
return Compatibility1_8.constructTranslationMessage(key, args);
}
@Override
public void sendInviteMessage(Player player, Player commandSender, String groupName, String joinCommand) {
sendJsonMessage(player, Compatibility1_8.constructInviteMessage(commandSender, groupName, joinCommand));
}
@Override
public void sendIncompatibleMessage(Player player, String pluginVersion, String pluginName) {
sendJsonMessage(player, Compatibility1_8.constructIncompatibleMessage(pluginVersion, pluginName));
}
@Override
public ArgumentType<?> playerArgument() {
return null;
}
@Override
public ArgumentType<?> uuidArgument() {
return null;
}
private void send(Player player, String json, Object chatMessageType) {
Object entityPlayer = callMethod(player, "getHandle");
Object playerConnection = getField(entityPlayer, "playerConnection");
Class<?> packet = getServerClass("Packet");
Class<?> chatSerializer = getServerClass("IChatBaseComponent$ChatSerializer");
Class<?> iChatBaseComponentClass = getServerClass("IChatBaseComponent");
Object iChatBaseComponent = callMethod(chatSerializer, "a", new Class[]{String.class}, json);
Class<?> packetPlayOutChatClass = getServerClass("PacketPlayOutChat");
Class<?> chatMessageTypeClass = getServerClass("ChatMessageType");
Object clientboundSystemChatPacket = callConstructor(packetPlayOutChatClass, new Class[]{iChatBaseComponentClass, chatMessageTypeClass}, iChatBaseComponent, chatMessageType);
callMethod(playerConnection, "sendPacket", new Class[]{packet}, clientboundSystemChatPacket);
}
}
Loaded 100 of 581 files, more files were not shown because too many files have changed in this diff. Show more