Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Participants

A participant is one person your application is responsible for. This page covers registering one, changing it, watching it and removing it.

Identifier and display name

A participant has two names, used for different things:

ParticipantId.of("069a79f4-44e9-4726-a5be-fca90e38aaf5")   // your identifier
ParticipantSpec.builder(SpaceKey.of("lobby"), "Notch")      // shown to players

ParticipantId is the key your own code uses. It must never change for the same person, so use a Minecraft UUID, a database primary key or an account identifier. It is never displayed to anyone.

The display name is what other players read in their Mumble client. It can change whenever you want and does not have to be unique.

Registering

ParticipantHandle handle = session.registerParticipant(
        ParticipantId.of(player.getUniqueId().toString()),
        ParticipantSpec.builder(SpaceKey.of("lobby"), player.getName()).build());

registerParticipant returns a ParticipantHandle immediately, before the runtime has confirmed anything. Every later operation on that participant goes through the handle, so store it in a map keyed by your own player identifier.

Registering neither blocks nor fails because the connection is down. When the session is not connected yet, the registration is part of the snapshot sent as soon as it is.

Calling it twice for the same identifier while the first registration is still alive throws IllegalStateException. Either unregister first, or retrieve the existing handle with session.participant(id).

The specification

ParticipantSpec is the complete description of a participant, in four fields:

FieldMeaning
spaceKeyWhich Space this participant belongs to
displayNameName shown in other players’ Mumble clients
serverMuteThis participant may not transmit
serverDeafThis participant may not receive
ParticipantSpec spec = ParticipantSpec.builder(SpaceKey.of("team-red"), "Notch")
        .serverMute(true)
        .build();

serverMute and serverDeaf are decisions you impose on the player. They are not the mute and deafen buttons in the player’s own Mumble client, which you can read but not set. See Reacting to players.

Changing a participant

A single call replaces the whole description:

handle.setSpec(ParticipantSpec.builder(SpaceKey.of("team-blue"), "Notch").build());

Changing spaceKey moves the participant. There is no separate move, rename or mute operation: build the description that should hold and set it.

Since the description is replaced as a whole, build it from your own current state rather than from a partly remembered earlier spec. Computing the whole spec in one place is usually simplest:

private ParticipantSpec specFor(Player player) {
    return ParticipantSpec.builder(spaceOf(player), player.getName())
            .serverMute(isSilenced(player))
            .build();
}

// wherever your game state changes
handle.setSpec(specFor(player));

Call it as often as you need. Setting the same value twice is harmless, and rapid successive changes are coalesced so that only the newest description is sent.

Knowing when a change took effect

setSpec returns a future that you can usually ignore. When you do need it, it completes with an AcceptedRevision carrying three separate milestones:

handle.setSpec(spec).thenAccept(revision -> {
    revision.acceptedSpecRevision();   // the runtime took your description
    revision.appliedSpecRevision();    // it was used to compute a new layout
    revision.publishedGeneration();    // that layout reached Mumble clients
});

They are separate because a successful call does not prove that any client has updated yet. Most application logic can ignore all three. Use them for diagnostics, or when something else has to wait until a move is visible.

Removing a participant

handle.unregister();

The participant leaves its Space and its Mumble connection is closed. Do this when a player leaves your server.

Unregistering is final for that handle. If the same player returns, call registerParticipant again and use the new handle it returns.

Reacting to players

The runtime reports what participants are actually doing through ParticipantListener. Every method has a default implementation, so override only the ones you need:

handle.addListener(new ParticipantListener() {
    @Override
    public void onStatusChanged(ParticipantHandle participant, ParticipantStatus status) {
        if (status.mumbleConnected()) {
            // their Mumble client is connected
        }
        if (status.selfMute()) {
            // they muted themselves in their own client
        }
    }

    @Override
    public void onOwnershipLost(ParticipantHandle participant, String reason) {
        // another application took this participant, or the lease expired
    }
});

ParticipantStatus reports observed reality, where the spec states your intent:

MethodReports
mumbleConnected()Whether a Mumble client is currently connected
appliedSpaceKey()Which Space they are actually in
selfMute(), selfDeaf()What the player set in their own client
applicationError()Why a description could not be honoured, when it could not

Reading selfMute() is how you show a muted icon above a player’s head, or refuse a voice-only feature to a player who has muted themselves.

Callbacks do not run on your main thread. They arrive on the session’s callback executor, serialized in order. Never block them, and return to your game’s own thread before touching game state. In Bukkit that means Bukkit.getScheduler().runTask(plugin, ...).

Handle states

handle.state() reports where a participant stands in its ownership lifecycle.

StateMeaningWhat to do
ACQUIRINGRegistered, waiting for the runtimeNothing, this is normal
OWNEDYours, workingNothing
SUSPENDEDConnection lost, still yoursNothing, it recovers by itself
REVOKEDPermanently lostRegister again if the player is still online
CLOSEDYou unregistered itNothing

SUSPENDED does not mean the player was disconnected from voice. Ownership is held by a server-side lease that outlives the gRPC connection, so a brief network problem or a controller restart leaves players talking. Your desired state is kept and re-sent on reconnect.

REVOKED is permanent. It happens when another application registers the same ParticipantId, or when the lease expires because you were gone too long. The handle is dead, and a new registration produces a new one.