Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add primitive support for sound api #1422

Open
wants to merge 1 commit into
base: dev/3.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions api/src/main/java/com/velocitypowered/api/proxy/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,14 @@ default void clearHeaderAndFooter() {
/**
* {@inheritDoc}
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
* <p>Note: This method is currently only implemented for players from version 1.19.3 and above.
* <br>A {@link ServerConnection} is required for this to function, so a {@link #getCurrentServer()}.isPresent() check should be made beforehand.
*
* @param sound the sound to play
* @throws IllegalArgumentException if the player is from a version lower than 1.19.3
* @throws IllegalStateException if no server is connected
* @since 3.3.0
* @sinceMinecraft 1.19.3
*/
@Override
default void playSound(@NotNull Sound sound) {
Expand Down Expand Up @@ -413,8 +419,12 @@ default void playSound(@NotNull Sound sound, Sound.Emitter emitter) {
/**
* {@inheritDoc}
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
* <p>Note: This method is currently only implemented for players from version 1.19.3 and above.
*
* @param stop the sound and/or a sound source, to stop
* @throws IllegalArgumentException if the player is from a version lower than 1.19.3
* @since 3.3.0
* @sinceMinecraft 1.19.3
*/
@Override
default void stopSound(@NotNull SoundStop stop) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
Expand Down Expand Up @@ -364,4 +366,12 @@ default boolean handle(ClientboundCustomReportDetailsPacket packet) {
default boolean handle(ClientboundServerLinksPacket packet) {
return false;
}

default boolean handle(ClientboundSoundEntityPacket packet) {
return false;
}

default boolean handle(ClientboundStopSoundPacket packet) {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull;

Expand All @@ -70,6 +71,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private boolean gracefulDisconnect = false;
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
private final Map<Long, Long> pendingPings = new HashMap<>();
private @MonotonicNonNull Integer entityId;

/**
* Initializes a new server connection.
Expand Down Expand Up @@ -324,6 +326,14 @@ public Map<Long, Long> getPendingPings() {
return pendingPings;
}

public Integer getEntityId() {
return entityId;
}

public void setEntityId(Integer entityId) {
this.entityId = entityId;
}

/**
* Ensures that this server connection remains "active": the connection is established and not
* closed, the player is still connected to the server, and the player still remains online.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,8 @@ public void handleBackendJoinGame(JoinGamePacket joinGame, VelocityServerConnect
}
}

destination.setEntityId(joinGame.getEntityId()); // used for sound api

// Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to
// track them.
for (UUID serverBossBar : serverBossBars) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket;
Expand Down Expand Up @@ -124,6 +126,8 @@
import net.kyori.adventure.resource.ResourcePackInfoLike;
import net.kyori.adventure.resource.ResourcePackRequest;
import net.kyori.adventure.resource.ResourcePackRequestLike;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.sound.SoundStop;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
Expand Down Expand Up @@ -1012,6 +1016,32 @@ void setClientBrand(final @Nullable String clientBrand) {
this.clientBrand = clientBrand;
}

@Override
public void playSound(@NotNull Sound sound) {
Preconditions.checkNotNull(sound, "sound");
Preconditions.checkArgument(
getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3),
"Player version must be 1.19.3 to be able to interact with sounds");
if (connection.getState() != StateRegistry.PLAY) {
throw new IllegalStateException("Can only interact with sounds in PLAY protocol");
}

connection.write(new ClientboundSoundEntityPacket(sound, null, ensureAndGetCurrentServer().getEntityId()));
}

@Override
public void stopSound(@NotNull SoundStop stop) {
Preconditions.checkNotNull(stop, "stop");
Preconditions.checkArgument(
getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3),
"Player version must be 1.19.3 to be able to interact with sounds");
if (connection.getState() != StateRegistry.PLAY) {
throw new IllegalStateException("Can only interact with sounds in PLAY protocol");
}

connection.write(new ClientboundStopSoundPacket(stop));
}

@Override
public void transferToHost(final InetSocketAddress address) {
Preconditions.checkNotNull(address);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
Expand Down Expand Up @@ -411,6 +413,22 @@ public enum StateRegistry {
clientbound.register(
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
map(0x16, MINECRAFT_1_20_5, false));
clientbound.register(
ClientboundSoundEntityPacket.class, ClientboundSoundEntityPacket::new,
map(0x5D, MINECRAFT_1_19_3, true),
map(0x61, MINECRAFT_1_19_4, true),
map(0x63, MINECRAFT_1_20_2, true),
map(0x65, MINECRAFT_1_20_3, true),
map(0x67, MINECRAFT_1_20_5, true),
map(0x6E, MINECRAFT_1_21_2, true));
clientbound.register(
ClientboundStopSoundPacket.class, ClientboundStopSoundPacket::new,
map(0x5F, MINECRAFT_1_19_3, true),
map(0x63, MINECRAFT_1_19_4, true),
map(0x66, MINECRAFT_1_20_2, true),
map(0x68, MINECRAFT_1_20_3, true),
map(0x6A, MINECRAFT_1_20_5, true),
map(0x71, MINECRAFT_1_21_2, true));
clientbound.register(
PluginMessagePacket.class,
PluginMessagePacket::new,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2024 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.velocitypowered.proxy.protocol.packet;

import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.sound.Sound;
import org.jetbrains.annotations.Nullable;

import java.util.Random;

public class ClientboundSoundEntityPacket implements MinecraftPacket {

private static final Random SEEDS_RANDOM = new Random();

private Sound sound;
private @Nullable Float fixedRange;
private int entityId;

public ClientboundSoundEntityPacket() {}

public ClientboundSoundEntityPacket(Sound sound, @Nullable Float fixedRange, int entityId) {
this.sound = sound;
this.fixedRange = fixedRange;
this.entityId = entityId;
}

@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException("Decode is not implemented");
}

@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
ProtocolUtils.writeVarInt(buf, 0); // version-dependent hardcoded sound id

ProtocolUtils.writeString(buf, sound.name().asMinimalString()); // not using writeKey, as the client already defaults to the vanilla namespace

buf.writeBoolean(fixedRange != null);
if (fixedRange != null)
buf.writeFloat(fixedRange);

ProtocolUtils.writeVarInt(buf, sound.source().ordinal());

ProtocolUtils.writeVarInt(buf, entityId);

buf.writeFloat(sound.volume());

buf.writeFloat(sound.pitch());

buf.writeLong(sound.seed().orElse(SEEDS_RANDOM.nextLong()));
}

@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (C) 2024 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.velocitypowered.proxy.protocol.packet;

import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.sound.SoundStop;

import javax.annotation.Nullable;

public class ClientboundStopSoundPacket implements MinecraftPacket {

private @Nullable Sound.Source source;
private @Nullable Key soundName;

public ClientboundStopSoundPacket() {}

public ClientboundStopSoundPacket(SoundStop soundStop) {
this(soundStop.source(), soundStop.sound());
}

public ClientboundStopSoundPacket(@Nullable Sound.Source source, @Nullable Key soundName) {
this.source = source;
this.soundName = soundName;
}

@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
int flagsBitmask = buf.readByte();

if ((flagsBitmask & 1) != 0) {
source = Sound.Source.values()[ProtocolUtils.readVarInt(buf)];
} else {
source = null;
}

if ((flagsBitmask & 2) != 0) {
soundName = ProtocolUtils.readKey(buf);
} else {
soundName = null;
}
}

@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
int flagsBitmask = 0;
if (source != null && soundName == null) {
flagsBitmask |= 1;
} else if (soundName != null && source == null) {
flagsBitmask |= 2;
} else if (source != null /*&& sound != null*/) {
flagsBitmask |= 3;
}

buf.writeByte(flagsBitmask);

if (source != null) {
ProtocolUtils.writeVarInt(buf, source.ordinal());
}

if (soundName != null) {
ProtocolUtils.writeString(buf, soundName.asMinimalString()); // not using writeKey, as the client already defaults to the vanilla namespace
}
}

@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}

@Nullable
public Sound.Source getSource() {
return source;
}

@Nullable
public Key getSoundName() {
return soundName;
}

}