-
Notifications
You must be signed in to change notification settings - Fork 0
Addon API
Mixer addons are ordinary Paper plugins. They declare Mixer as a dependency, obtain
MixerApi from Bukkit's Services API, and register an addon instance. An addon must
only depend on the api module; classes under me.andromedov.mixer.core are internal.
The API examples below target Mixer 2.3.0, Paper 1.21.4+, and Java 21. Mixer itself is validated on Paper versions from 1.21.4 through 26.2.
Until the API artifact is published to a remote Maven repository, install the current checkout into Maven Local:
git clone https://github.com/Andromedov/Mixer.git
cd Mixer
gradlew :api:publishToMavenLocalThen configure the addon project:
repositories {
mavenLocal()
maven("https://repo.papermc.io/repository/maven-public/")
}
dependencies {
compileOnly("me.andromedov:mixer-api:2.3.0")
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}The addon's plugin.yml must load after Mixer:
name: RadioAddon
version: 1.0.0
main: com.example.radio.RadioAddonPlugin
api-version: '1.21.4'
depend: [Mixer]package com.example.radio;
import me.andromedov.mixer.api.MixerApi;
import me.andromedov.mixer.api.addon.MixerAddon;
import me.andromedov.mixer.api.addon.MixerAddonContext;
import me.andromedov.mixer.api.source.MixerAudioSourceResolver;
import org.bukkit.plugin.java.JavaPlugin;
public final class RadioAddonPlugin extends JavaPlugin {
@Override
public void onEnable() {
MixerApi.get().addons().register(this, new MixerAddon() {
@Override
public String id() {
return "radio";
}
@Override
public String name() {
return "Radio Addon";
}
@Override
public String version() {
return getPluginMeta().getVersion();
}
@Override
public void onEnable(MixerAddonContext context) {
context.registerSourceResolver(new MixerAudioSourceResolver() {
@Override
public String id() {
return "station";
}
@Override
public int priority() {
return 100;
}
@Override
public boolean supports(String source) {
return source.startsWith("radio:");
}
@Override
public String resolve(String source) {
String station = source.substring("radio:".length());
return switch (station) {
case "ambient" -> "https://radio.example/ambient.ogg";
case "rock" -> "https://radio.example/rock.ogg";
default -> throw new IllegalArgumentException("Unknown station: " + station);
};
}
});
}
});
}
}Players and burned discs can now use radio:ambient. Mixer stores that stable source
on the disc and asks the addon to resolve it each time playback starts. This allows an
addon to return expiring URLs without writing those URLs into item data.
Source resolver supports and resolve methods run asynchronously. They may perform
blocking HTTP or database work, but must not access Bukkit APIs that require the main
server thread. Resolvers run by descending priority; equal priorities use their stable
registration key as a tiebreaker.
All registrations created through MixerAddonContext are automatically removed when
the addon is disabled. Manual cleanup is only necessary for resources created outside
the Mixer API.
MixerApi mixer = MixerApi.get();
var audioPlayer = mixer.getOrCreatePlayer(jukebox.getLocation());
audioPlayer.clearAndPlay("radio:ambient");
audioPlayer.currentTrack().ifPresent(track ->
getLogger().info(track.author() + " - " + track.title()));
audioPlayer.dsp().setGain(0.75);
audioPlayer.dsp().setHighPass(120.0F);
audioPlayer.dsp().setFlanger(0.003, 0.5, 1.5);
audioPlayer.clearQueue();
mixer.stopPlayer(jukebox.getLocation());Creating/stopping players, changing DSP, and registering/unregistering addons or resolvers must happen on the Bukkit main thread. Reading player state and resolving audio sources is thread-safe. Collections returned by the API are immutable snapshots.
Portable players are also available through createPortablePlayer,
findPortablePlayer, and stopPortablePlayer.
Mixer 2.3.0 exposes two APIs intended for approval workflows:
-
MixerApi.discs()probes sources and creates or reads Mixer disc items. -
MixerApi.playbackPolicies()controls whether a physical disc may play.
Discord requests, moderation state, persistence, and economy transactions belong to the addon. Mixer does not depend on a Discord library or Vault.
probe resolves addon protocols and Cobalt sources, loads the first selected track,
and returns the original stable source with its metadata. It always completes
asynchronously and does not create an item.
MixerApi mixer = MixerApi.get();
mixer.discs().probe(submittedUrl).whenComplete((disc, error) -> {
if (error != null) {
// Mark the request as invalid and notify the player later on the main thread.
return;
}
// Persist the pending request before sending the Discord message.
// This callback is asynchronous: database and Discord I/O are allowed here,
// but Bukkit ItemStack, inventory, world, and player APIs are not.
pendingRepository.save(requestId, playerId, disc);
discordModeration.sendRequest(requestId, disc.track().orElseThrow());
});Discord Accept/Reject buttons should carry an opaque request ID rather than a raw URL. Verify the Discord interaction signature or bot token, make decisions idempotent, and persist the moderator ID and decision time.
Use a stable addon-owned source such as private:<request-id>. Store the approved
original URL in the addon's database and resolve it only during playback. This avoids
putting private or expiring URLs into item PDC and lets policies recognize the source
even when no ItemStack is available during server restore.
Bukkit.getScheduler().runTask(this, () -> {
MixerDisc approved = new MixerDisc(
"private:" + requestId,
pendingDisc.track()
);
ItemStack template = new ItemStack(Material.MUSIC_DISC_13);
ItemStack item = MixerApi.get().discs().createDisc(template, approved);
item.editMeta(meta -> meta.getPersistentDataContainer().set(
new NamespacedKey(this, "private_owner"),
PersistentDataType.STRING,
ownerId.toString()
));
// Give it to an online player or persist it for delivery on their next login.
owner.getInventory().addItem(item);
});createDisc clones the template and never modifies the input. It and readDisc must
run on the Bukkit main thread. Newly created discs preserve source, title, author, URI,
duration, and stream metadata. readDisc can also read older Mixer discs, but their
track metadata may be absent.
Register a resolver that accepts only approved IDs. Resolver methods run off the main thread, so a database lookup is allowed.
context.registerSourceResolver(new MixerAudioSourceResolver() {
@Override
public String id() {
return "private-disc";
}
@Override
public boolean supports(String source) {
return source.startsWith("private:");
}
@Override
public String resolve(String source) {
String requestId = source.substring("private:".length());
return approvedRepository.findPlayableUrl(requestId)
.orElseThrow(() -> new IllegalStateException("Disc is not approved"));
}
});Playback policies cover jukebox interaction, portable speakers, redstone playlists, and server restore. Policies are synchronous and fail closed when they throw. They must use an in-memory cache and must not call Discord, Vault, HTTP, or a database.
context.registerPlaybackPolicy(new MixerPlaybackPolicy() {
@Override
public String id() {
return "private-disc-owner";
}
@Override
public int priority() {
return 100;
}
@Override
public MixerPlaybackDecision evaluate(MixerPlaybackRequest request) {
if (!request.source().startsWith("private:")) {
return MixerPlaybackDecision.allow();
}
String requestId = request.source().substring("private:".length());
UUID ownerId = ownershipCache.get(requestId);
boolean allowed = ownerId != null && request.actor()
.map(player -> player.getUniqueId().equals(ownerId)
|| player.hasPermission("privateplates.admin"))
.orElse(false);
return allowed
? MixerPlaybackDecision.allow()
: MixerPlaybackDecision.deny(Component.text("This disc is private."));
}
});An absent actor means redstone automation or server restore. The example denies private
playback in both cases. Programmatic MixerAudioPlayer.load calls are trusted addon
operations and do not represent a physical disc, so an addon must authorize those calls
itself.
Keep payment policy in the addon and make the chosen point explicit:
- Charge on submission and refund on rejection or technical failure; or
- Charge on approval immediately before creating/delivering the disc.
Check for both Vault and an economy provider. Run economy and Bukkit operations on the main thread, record a unique transaction state before acting, and make Discord button retries idempotent so one request cannot be charged or delivered twice.
All source-resolver and playback-policy registrations made through MixerAddonContext
are automatically removed when the addon plugin is disabled.