Skip to main content

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.
For the high-level Sound API documentation, see Sound API - Play and Control Sounds.

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

SourceSoundEffect
nilnilStop all sounds
&sourcenilStop all sounds from source
nil&nameStop specific sound from all sources
&source&nameStop 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