Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ikeepcalm/coi-client/llms.txt

Use this file to discover all available pages before exploring further.

All visual effects in COI Client are triggered entirely from the server side by sending the coi-client:effect custom plugin messaging payload to a specific player. Once received, effects run entirely on the client — rendering directly onto the player’s HUD without any player interaction required. The client registers a global receiver for this channel as soon as it connects, so it is safe to send effects at any point after a player joins.

Setup

1

Register the outgoing channel in onEnable()

In your Paper plugin’s main class, register the coi-client:effect outgoing channel before you send any messages:
@Override
public void onEnable() {
    getServer().getMessenger().registerOutgoingPluginChannel(this, "coi-client:effect");
}
2

Create a sendEffect helper method

Add a helper method to your plugin that encodes an effect ID and parameter string into the binary payload format COI Client expects.The COI Client reads both strings using Minecraft’s FriendlyByteBuf.readUtf(), which expects each string to be length-prefixed with a varint, not a fixed 2-byte length. You must encode the payload with a matching varint writer — the easiest way on Paper is to use Netty’s ByteBuf directly:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

void sendEffect(Player player, String effectId, String params) {
    ByteBuf buf = Unpooled.buffer();
    writeUtf(buf, effectId);
    writeUtf(buf, params);
    byte[] data = new byte[buf.readableBytes()];
    buf.readBytes(data);
    buf.release();
    player.sendPluginMessage(plugin, "coi-client:effect", data);
}

/** Writes a UTF-8 string with a varint length prefix — matches FriendlyByteBuf.writeUtf(). */
private static void writeUtf(ByteBuf buf, String value) {
    byte[] bytes = value.getBytes(java.nio.charset.StandardCharsets.UTF_8);
    writeVarInt(buf, bytes.length);
    buf.writeBytes(bytes);
}

private static void writeVarInt(ByteBuf buf, int value) {
    while ((value & ~0x7F) != 0) {
        buf.writeByte((value & 0x7F) | 0x80);
        value >>>= 7;
    }
    buf.writeByte(value);
}
plugin should be a reference to your plugin instance (e.g. this if the method lives in your main class). Netty is bundled with Paper and does not need to be declared as a dependency.
3

Call the helper to trigger effects

With the channel registered and the helper in place, you can trigger any effect from anywhere in your plugin:
// Start a persistent vignette
sendEffect(player, "vignette", "intensity=0.7");

// Start a 10-second heartbeat at 90 bpm
sendEffect(player, "heartbeat", "intensity=0.85,bpm=90,duration=10000");

// Stop a specific effect
sendEffect(player, "vignette", "stop");

// Stop all active effects at once
sendEffect(player, "all", "stop");

The sendEffect Helper

The complete helper to copy into your Paper plugin. COI Client decodes both strings using Minecraft’s FriendlyByteBuf.readUtf(), which expects a varint length prefix before each string’s UTF-8 bytes. You must encode them the same way — use Netty’s ByteBuf (bundled with Paper), not DataOutputStream.writeUTF() (which writes a fixed 2-byte length prefix and is incompatible):
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.nio.charset.StandardCharsets;

// Inside your plugin class:
private final JavaPlugin plugin = this;

void sendEffect(Player player, String effectId, String params) {
    ByteBuf buf = Unpooled.buffer();
    writeUtf(buf, effectId);
    writeUtf(buf, params);
    byte[] data = new byte[buf.readableBytes()];
    buf.readBytes(data);
    buf.release();
    player.sendPluginMessage(plugin, "coi-client:effect", data);
}

/** Writes a UTF-8 string with a varint length prefix — matches FriendlyByteBuf.writeUtf(). */
private static void writeUtf(ByteBuf buf, String value) {
    byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
    writeVarInt(buf, bytes.length);
    buf.writeBytes(bytes);
}

private static void writeVarInt(ByteBuf buf, int value) {
    while ((value & ~0x7F) != 0) {
        buf.writeByte((value & 0x7F) | 0x80);
        value >>>= 7;
    }
    buf.writeByte(value);
}
Both strings are written with a varint length prefix followed by raw UTF-8 bytes. This matches FriendlyByteBuf.writeUtf() — the format the COI Client receiver uses to read them. Do not use DataOutputStream.writeUTF(), which writes a 2-byte big-endian length prefix and is not compatible.

Payload Structure

Every coi-client:effect message carries exactly two string fields:
FieldValueDescription
effectIdEffect name (e.g. vignette) or allIdentifies which effect to trigger or stop. Passing all only makes sense with params = "stop".
paramsComma-separated key=value pairs, or stopConfiguration overrides for the effect. Omitted params fall back to their defaults. Passing "stop" removes the named effect instead of starting it.
Example param strings:
"intensity=0.8,duration=5000"
"intensity=0.9,bpm=100,duration=15000"
"color=FF2200,intensity=0.7,duration=400"
"stop"

Stopping Effects

COI Client supports two stop patterns — stopping a single named effect or clearing everything at once:
// Stop one specific effect by name
sendEffect(player, "vignette", "stop");
sendEffect(player, "heartbeat", "stop");
sendEffect(player, "cracks", "stop");

// Stop every active effect immediately
sendEffect(player, "all", "stop");
When effectId is "all", the params string is ignored entirely — the client calls stopAll() regardless of what params contains.

Behavior Notes

  • Re-triggering replaces the effect. If you send an effect that is already active, the old instance is removed and a fresh one starts from scratch with the new params. There is no stacking of the same effect type.
  • Effects layer independently. Multiple different effects can be active simultaneously — for example, vignette, heartbeat, and cracks can all run at the same time, each managed as its own layer by the client.
  • Auto-expiry via duration. Effects with a finite duration value (in milliseconds) automatically remove themselves once the duration elapses. Effects started without a duration (or with duration=-1) are persistent and will remain active until explicitly stopped.
  • Photosensitivity mode. If the player has enabled Epilepsy Mode in the COI Client config, the flash, glitch, and heartbeat effects are silently suppressed on the client side. Design your gameplay logic to handle the absence of these effects gracefully.
Call sendEffect(player, "all", "stop") on player respawn or whenever you clear a player’s status effects. This prevents stale visual effects from persisting into a new gameplay state — for example, a lingering heartbeat after a player has been revived.

Build docs developers (and LLMs) love