Documentation Index
Fetch the complete documentation index at: https://mintlify.com/minekube/gate/llms.txt
Use this file to discover all available pages before exploring further.
Sound System Protocol
Gate’s sound system operates at the protocol level, sending and managing sound packets for Minecraft clients. This page documents the packet-level implementation details.
Sound Packets
Gate handles two main types of sound packets:
Sound Effect Packet
Plays a named sound effect at a specific location:
type SoundEffect struct {
Name string // Sound identifier (e.g., "entity.player.levelup")
Source SoundSource // Category/source of sound
X float64 // X coordinate (absolute)
Y float64 // Y coordinate (absolute)
Z float64 // Z coordinate (absolute)
Volume float32 // Volume (0.0 to infinity)
Pitch float32 // Pitch (0.5 to 2.0)
Seed int64 // Random seed (1.19+)
}
Entity Sound Effect Packet
Plays a sound at an entity’s position:
type EntitySoundEffect struct {
Name string // Sound identifier
Source SoundSource // Category/source
EntityID int // Entity to play sound from
Volume float32 // Volume (0.0 to infinity)
Pitch float32 // Pitch (0.5 to 2.0)
Seed int64 // Random seed (1.19+)
}
Sound Sources
Sound sources determine which client volume slider controls the sound:
type SoundSource int
const (
MasterSource SoundSource = 0 // Master volume
MusicSource SoundSource = 1 // Music
RecordSource SoundSource = 2 // Jukebox/Music discs
WeatherSource SoundSource = 3 // Weather
BlockSource SoundSource = 4 // Blocks
HostileSource SoundSource = 5 // Hostile creatures
NeutralSource SoundSource = 6 // Neutral creatures
PlayerSource SoundSource = 7 // Players
AmbientSource SoundSource = 8 // Ambient sounds
VoiceSource SoundSource = 9 // Voice/Speech
UISource SoundSource = 10 // UI sounds (1.21.5+)
)
Source String Mapping
var sourceNames = map[string]SoundSource{
"master": MasterSource,
"music": MusicSource,
"record": RecordSource,
"weather": WeatherSource,
"block": BlockSource,
"hostile": HostileSource,
"neutral": NeutralSource,
"player": PlayerSource,
"ambient": AmbientSource,
"voice": VoiceSource,
"ui": UISource,
}
func ParseSource(name string) (SoundSource, error) {
if source, ok := sourceNames[strings.ToLower(name)]; ok {
return source, nil
}
return 0, fmt.Errorf("unknown sound source: %s", name)
}
Stop Sound Packet
Stops playing sounds based on filters:
type StopSound struct {
Source *SoundSource // nil = all sources
Sound *string // nil = all sounds
}
Filter Combinations
| Source | Sound | Effect |
|---|
nil | nil | Stop all sounds |
&source | nil | Stop all sounds from source |
nil | &name | Stop specific sound from all sources |
&source | &name | Stop specific sound from specific source |
Protocol Version Handling
Version Requirements
const (
MinSoundProtocol = version.Minecraft_1_19_3 // Minimum version
MinUISourceVersion = version.Minecraft_1_21_5 // UI source support
)
func CheckSoundSupport(protocol proto.Protocol) error {
if protocol.Lower(MinSoundProtocol) {
return ErrUnsupportedClientProtocol
}
return nil
}
func CheckUISourceSupport(protocol proto.Protocol) error {
if protocol.Lower(MinUISourceVersion) {
return ErrUISourceUnsupported
}
return nil
}
Version-Specific Encoding
Sound packets encode differently based on protocol version:
func (s *SoundEffect) Encode(c *proto.PacketContext, wr io.Writer) error {
// Write sound name
if err := util.WriteString(wr, s.Name); err != nil {
return err
}
// Write source (added in 1.9)
if c.Protocol.GreaterEqual(version.Minecraft_1_9) {
if err := util.WriteVarInt(wr, int(s.Source)); err != nil {
return err
}
}
// Write position (multiplied by 8)
if err := util.WriteInt32(wr, int32(s.X*8)); err != nil {
return err
}
if err := util.WriteInt32(wr, int32(s.Y*8)); err != nil {
return err
}
if err := util.WriteInt32(wr, int32(s.Z*8)); err != nil {
return err
}
// Write volume and pitch
if err := util.WriteFloat32(wr, s.Volume); err != nil {
return err
}
if err := util.WriteFloat32(wr, s.Pitch); err != nil {
return err
}
// Write seed (added in 1.19)
if c.Protocol.GreaterEqual(version.Minecraft_1_19) {
if err := util.WriteInt64(wr, s.Seed); err != nil {
return err
}
}
return nil
}
Sound Package Implementation
The sound package (go.minekube.com/gate/pkg/edition/java/sound) provides high-level functions that construct and send sound packets:
Playing Sounds
import "go.minekube.com/gate/pkg/edition/java/sound"
// Play sound at entity position
func Play(target Player, snd *Sound, emitter Player) error {
// Validate protocol support
if err := CheckSoundSupport(target.Protocol()); err != nil {
return err
}
// Check if UI source is supported
if snd.Source == UISource {
if err := CheckUISourceSupport(target.Protocol()); err != nil {
return err
}
}
// Verify both players on same server
targetServer := target.CurrentServer()
emitterServer := emitter.CurrentServer()
if targetServer == nil || emitterServer == nil {
return ErrNotConnected
}
if !ServerInfoEqual(targetServer.ServerInfo(), emitterServer.ServerInfo()) {
return ErrDifferentServers
}
// Get emitter's entity ID
entityID, ok := emitter.CurrentServerEntityID()
if !ok {
return ErrNotConnected
}
// Construct packet
packet := &EntitySoundEffect{
Name: snd.Name,
Source: snd.Source,
EntityID: entityID,
Volume: snd.Volume,
Pitch: snd.Pitch,
Seed: snd.Seed,
}
// Send to target player
return target.WritePacket(packet)
}
Stopping Sounds
// Stop sounds based on filters
func Stop(target Player, source *SoundSource, soundName *string) error {
// Validate protocol support
if err := CheckSoundSupport(target.Protocol()); err != nil {
return err
}
// Construct packet
packet := &StopSound{
Source: source,
Sound: soundName,
}
// Send to target player
return target.WritePacket(packet)
}
// Helper: Stop all sounds
func StopAll(target Player) error {
return Stop(target, nil, nil)
}
// Helper: Stop sounds from source
func StopSource(target Player, source SoundSource) error {
return Stop(target, &source, nil)
}
// Helper: Stop specific sound
func StopSound(target Player, name string) error {
return Stop(target, nil, &name)
}
Entity ID Management
Sound packets require the entity ID of the emitter player on the backend server:
Getting Entity ID
// player.go
func (p *connectedPlayer) CurrentServerEntityID() (int, bool) {
serverConn := p.connectedServer()
if serverConn == nil {
return 0, false
}
return serverConn.entityID, true
}
The entity ID is stored when the player joins the backend server:
// serverConnection stores entity ID from JoinGame packet
type serverConnection struct {
entityID int
// ... other fields
}
func (s *serverConnection) handleJoinGame(packet *packet.JoinGame) {
s.entityID = packet.EntityID
// ... rest of join game handling
}
Server Matching
Verify both players are on the same backend server:
func (p *connectedPlayer) CheckServerMatch(other Player) bool {
// Get entity IDs
thisEntityID, thisOk := p.CurrentServerEntityID()
otherEntityID, otherOk := other.CurrentServerEntityID()
if !thisOk || !otherOk {
return false // One or both not connected
}
// Get server connections
thisServer := p.connectedServer()
otherServer := other.connectedServer()
if thisServer == nil || otherServer == nil {
return false
}
// Compare server info
return ServerInfoEqual(
thisServer.Server().ServerInfo(),
otherServer.Server().ServerInfo(),
)
}
Error Handling
The sound system defines specific errors:
var (
// Player version is below 1.19.3
ErrUnsupportedClientProtocol = errors.New("player version must be 1.19.3+ to use sounds")
// Player tried to use UI source on version < 1.21.5
ErrUISourceUnsupported = errors.New("UI sound source requires version 1.21.5+")
// Player not connected to a backend server
ErrNotConnected = errors.New("player is not connected to a server")
// Players are on different backend servers
ErrDifferentServers = errors.New("emitter and target must be on the same server")
)
Error Handling Example
import "errors"
func playSound(target, emitter Player, snd *Sound) {
err := sound.Play(target, snd, emitter)
if err != nil {
switch {
case errors.Is(err, sound.ErrUnsupportedClientProtocol):
// Player version too old
target.SendMessage(&component.Text{
Content: "Your client version doesn't support sounds",
})
case errors.Is(err, sound.ErrUISourceUnsupported):
// UI source not supported
log.Warn("Player tried to use UI sound source on old version")
case errors.Is(err, sound.ErrNotConnected):
// Not connected to server yet
log.Debug("Attempted to play sound but player not connected")
case errors.Is(err, sound.ErrDifferentServers):
// Players on different servers
log.Warn("Cannot play sound - players on different servers")
default:
log.Error(err, "Failed to play sound")
}
}
}
Packet Interception
Intercept sound packets in session handlers:
import (
"go.minekube.com/gate/pkg/edition/java/proto/packet"
"go.minekube.com/gate/pkg/gate/proto"
)
type CustomSessionHandler struct {
netmc.SessionHandler
}
func (h *CustomSessionHandler) HandlePacket(pc *proto.PacketContext) {
switch p := pc.Packet.(type) {
case *packet.SoundEffect:
h.log.Info("Sound effect",
"name", p.Name,
"source", p.Source,
"position", fmt.Sprintf("%.1f,%.1f,%.1f", p.X, p.Y, p.Z),
"volume", p.Volume,
"pitch", p.Pitch,
)
// Modify sound properties
if p.Volume > 2.0 {
p.Volume = 2.0 // Limit volume
}
h.SessionHandler.HandlePacket(pc)
case *packet.EntitySoundEffect:
h.log.Info("Entity sound effect",
"name", p.Name,
"source", p.Source,
"entityID", p.EntityID,
"volume", p.Volume,
"pitch", p.Pitch,
)
h.SessionHandler.HandlePacket(pc)
case *packet.StopSound:
sourceStr := "all"
if p.Source != nil {
sourceStr = p.Source.String()
}
soundStr := "all"
if p.Sound != nil {
soundStr = *p.Sound
}
h.log.Info("Stop sound",
"source", sourceStr,
"sound", soundStr,
)
h.SessionHandler.HandlePacket(pc)
default:
h.SessionHandler.HandlePacket(pc)
}
}
Minecraft Limitations
Be aware of known Minecraft bugs:
MC-146721: Stereo Sounds Not Positional
In Minecraft 1.14+, stereo sounds are played globally instead of positionally. This is a client bug that cannot be fixed by the proxy.
MC-138832: Volume/Pitch Ignored (1.14-1.16.5)
In Minecraft 1.14 through 1.16.5, the client ignores volume and pitch values in sound packets. Always use default volume (1.0) and pitch (1.0) for these versions.
func adjustSoundForVersion(snd *Sound, protocol proto.Protocol) *Sound {
if protocol.GreaterEqual(version.Minecraft_1_14) &&
protocol.Lower(version.Minecraft_1_17) {
// Volume and pitch ignored in 1.14-1.16.5
snd.Volume = 1.0
snd.Pitch = 1.0
}
return snd
}
Invalid Sound Names
The client silently ignores invalid sound names - no error is shown to the player. Always validate sound names against the Minecraft sounds.json.
Complete Example: Custom Sound System
package main
import (
"go.minekube.com/gate/pkg/edition/java/proxy"
"go.minekube.com/gate/pkg/edition/java/sound"
)
// Custom sound manager with caching
type SoundManager struct {
sounds map[string]*sound.Sound
}
func NewSoundManager() *SoundManager {
return &SoundManager{
sounds: make(map[string]*sound.Sound),
}
}
// Register a named sound
func (sm *SoundManager) Register(name string, snd *sound.Sound) {
sm.sounds[name] = snd
}
// Play a registered sound
func (sm *SoundManager) Play(name string, target, emitter proxy.Player) error {
snd, ok := sm.sounds[name]
if !ok {
return fmt.Errorf("sound not registered: %s", name)
}
return sound.Play(target, snd, emitter)
}
// Initialize with common sounds
func InitSoundManager() *SoundManager {
sm := NewSoundManager()
sm.Register("levelup", sound.NewSound(
"entity.player.levelup",
sound.SourcePlayer,
))
sm.Register("pling", sound.NewSound(
"block.note_block.pling",
sound.SourceBlock,
).WithPitch(1.5))
sm.Register("success", sound.NewSound(
"ui.button.click",
sound.SourceUI,
).WithVolume(0.5))
return sm
}
See Also