Java API¶
The public API for other mods interacting with vfxweaver. Backward compatibility matters, so signatures are only broken for a good reason and with a version bump.
dev.vfxweaver.api.VFXAPI¶
A stateless class (all methods static), the entry point for other mods.
Server → client (over the network)¶
// Resolves the datapack/built-in effectId, merges its default constant params with overrides,
// takes the duration/easing from the definition (if not given explicitly), and sends a
// VFXTriggerPayload to the player. Returns false if effectId is unknown.
boolean sendEffect(ServerPlayer player, Identifier effectId, Map<String, Float> overrides, @Nullable EasingType easing);
// Same, but with an explicit world position: the client immediately re-anchors spatial
// bindings (screen_x/screen_y/proximity) to that point and uses it for the effect's positions —
// no pos_x/pos_y/pos_z hack.
boolean sendEffect(ServerPlayer player, Identifier effectId, Vec3 worldPos, Map<String, Float> overrides, @Nullable EasingType easing);
// Same with an explicit instance id (0 = the client assigns one). Lets you later stop exactly
// this instance via sendStop(player, effectId, instanceId) instead of every instance of the effect.
// The id is a unique handle: playing it again restarts that instance in place instead of adding a
// duplicate, so sendStop/sendMove always address the instance you played.
boolean sendEffect(ServerPlayer player, Identifier effectId, long instanceId, @Nullable Vec3 worldPos, Map<String, Float> overrides, @Nullable EasingType easing);
// Explicit variant without consulting the definition registry — all packet fields are set manually.
void sendEffect(ServerPlayer player, Identifier effectId, int durationTicks, Map<String, Float> params, EasingType easing);
// Full variant: duration, instance id, world position, entity UUID targets for entity effects
// (entity_tint/entity_outline) or entity-anchored world overlays, parameter overrides and
// easing. entityUuids — up to 16 UUIDs; an empty list for all other effect types. For world
// overlays whose definition declares entity-anchored "positions", the UUIDs fill the anchor
// slots in anchor order (the client tracks those entities per frame).
void sendEffect(ServerPlayer player, Identifier effectId, int durationTicks, long instanceId, @Nullable Vec3 worldPos, List<UUID> entityUuids, Map<String, Float> overrides, @Nullable EasingType easing);
// Stops the effect on the player's client (all its instances).
void sendStop(ServerPlayer player, Identifier effectId);
// Stops one specific instance of an effect (see sendEffect with instanceId).
void sendStop(ServerPlayer player, Identifier effectId, long instanceId);
// Stops every effect the server has recorded for the player, reusing the per-effect stop payload
// (no new wire action). Effects the player's client played locally (never seen by the server) are
// not covered - use the client-local stopAllEffects() for those.
void sendStopAll(ServerPlayer player);
// Live-overrides a parameter of a running effect (without restarting the timeline).
// Ignored by the client with a warning in the log if the effect is not currently running.
void sendSetParam(ServerPlayer player, Identifier effectId, String param, float value);
// Like sendSetParam, but replaces the parameter with a compiled math expression (same syntax
// as the JSON "expr" field; compiled per instance with its seed). Applies to every running
// instance of the effect on the player's client.
void sendSetParamExpr(ServerPlayer player, Identifier effectId, String param, String exprSource);
// Moves a running world-overlay effect instance to a new exact world point (sub-block
// precision; the instance keeps its timeline and fades). Call every tick for scripted motion.
// Ignored with a warning when no instance with that id (of that effect) is running.
void sendMove(ServerPlayer player, Identifier effectId, long instanceId, Vec3 worldPos);
// Moves one mask leaf of a running effect to a world position, by setting the three reserved
// mask.p<N>.center_x/y/z params (see "Mask leaf params" below). Applies to every running
// instance of the effect; ignored with a client-side warning when it is not running. This is
// also the server-driven workaround when a client cannot resolve an entity binding (the entity
// is outside the client's tracking range).
void sendMaskMove(ServerPlayer player, Identifier effectId, int primitive, Vec3 position);
// Adds/replaces a keyframe of a parameter of a running effect.
// A negative timeTicks means "from here": the value the parameter has right now is pinned at the
// current time and the animation runs to `value` over |timeTicks| ticks - so animation segments
// chain without knowing the current value (ramp a blur up, later send -20/0 to fade it back out).
void sendKeyframe(ServerPlayer player, Identifier effectId, String param, int timeTicks, float value, EasingType easing);
Client (locally, without the network)¶
// durationTicks: 0 = definition default, negative = persistent.
// Returns false if called off-client (e.g. on a dedicated server) —
// in that case use sendEffect().
boolean playEffect(Identifier effectId, int durationTicks, Map<String, Float> params, @Nullable EasingType easing);
boolean playEffect(Identifier effectId, int durationTicks, Map<String, Float> params); // linear easing
// Like playEffect, but returns the id of the created instance (0 on error) — so you can stop
// one specific instance out of several concurrent ones via stopEffect(instanceId).
long playEffectId(Identifier effectId, int durationTicks, Map<String, Float> params, @Nullable EasingType easing);
boolean stopEffect(Identifier effectId); // every instance of the effect
boolean stopEffect(long instanceId); // one specific instance
boolean stopAllEffects();
Anchored local playback¶
A client-local play can be anchored to a world position and/or to entities, so an effect lands
where the event happened instead of at the definition's default spot (e.g. dent, shockwave,
vortex centred on a hit). This is the local equivalent of /vfx playat / a network
sendEffect(player, id, pos, ...) — no packet is involved.
// Anchor to a world point: replaces the definition's position slots and re-anchors the spatial
// world bindings (screen_x, screen_y, proximity, ...) to that point.
boolean playEffect(Identifier effectId, int durationTicks, Vec3 position, Map<String, Float> params, @Nullable EasingType easing);
boolean playEffect(Identifier effectId, int durationTicks, Vec3 position, Map<String, Float> params); // linear easing
long playEffectId(Identifier effectId, int durationTicks, Vec3 position, Map<String, Float> params, @Nullable EasingType easing);
// Anchor to entities: when `position` is null and the definition declares entity-anchored
// positions, the UUIDs are zipped with them in declaration order and the effect follows those
// entities. Datapack `entity_selector`s resolve on the server only, so a client-side caller finds
// the entities itself and passes their UUIDs here.
boolean playEffect(Identifier effectId, int durationTicks, @Nullable Vec3 position, List<UUID> entityUuids, Map<String, Float> params, @Nullable EasingType easing);
long playEffectId(Identifier effectId, int durationTicks, @Nullable Vec3 position, List<UUID> entityUuids, Map<String, Float> params, @Nullable EasingType easing);
// Re-anchor a running instance locally (the local equivalent of the network MOVE action). Call it
// every tick to make an anchored effect follow a moving point or entity.
boolean moveEffect(Identifier effectId, long instanceId, Vec3 worldPos);
Notes:
- A non-null position wins over the definition's entity anchors (same rule as the network path).
- If a definition declares entity anchors and a local play passes no UUIDs, the play is rejected
(the placeholder slots would otherwise render at the world origin) — logged once.
- A null easing keeps its meaning on this path: the definition's default easing is used.
- moveEffect returns true when the request was applied (already on the render thread) or queued;
a queued request that references an unknown instance fails silently.
- Client-local plays with a position are recorded into Flashback replays with the same anchor, so
a replay reproduces the effect where it originally happened.
Live control (client-local)¶
The live edits the network exposes also work locally, so a pure client-side mod never needs a server round-trip:
// Constant override of a parameter on every running instance (starts one when none is running).
boolean setParam(Identifier effectId, String name, float value);
// Swap a parameter for a live math expression (same syntax as the JSON "expr" field).
boolean setParamExpr(Identifier effectId, String name, String exprSource);
// Add/replace a keyframe. A negative time means "from here": pin the value the parameter has now
// and run the segment to `value` over |time| ticks, so animation variations chain seamlessly.
boolean setKeyframe(Identifier effectId, String name, int time, float value, @Nullable EasingType easing);
boolean setKeyframe(Identifier effectId, String name, int time, float value, String easing); // named curve
// Move one mask leaf locally (no packet) - the local counterpart of sendMaskMove; expands into
// the three mask.p<N>.center_* params (see "Mask leaf params" below).
boolean maskMove(Identifier effectId, int primitive, Vec3 position);
Like moveEffect, these return true when applied on the render thread or queued for it. A null
(or blank) easing means linear - a keyframe segment has no "definition default".
Mask leaf params (reserved names)¶
A mask's numeric leaves are registered as ordinary animatable effect parameters, so the whole live-control and animation surface above works on them unchanged:
| Name | Meaning |
|---|---|
mask.p<N>.center_x / .center_y / .center_z |
leaf <N> centre (screen leaves use x/y) |
mask.p<N>.rotation |
leaf rotation (degrees) |
mask.p<N>.p<J> |
the leaf's per-shape parameter J (radius, half_width, … in VFXMaskShapeKind order) |
mask.p<N>.soft |
edge falloff width |
mask.p<N>.stroke |
stroke width (fill: "stroke") |
mask.p<N>.field_amount / .field_scale |
edge-field amount / scale |
<N> is the leaf index in mask declaration order (0-based; a composition flattens depth-first).
Every live path works on them: sendSetParam/setParam (also constant, expr/sendSetParamExpr,
sendKeyframe/setKeyframe), graph { "from": node } driven inputs, and datapack bindings.
sendMaskMove/maskMove are a convenience that sets a leaf's three centre components at once:
// Server: follow a moving point (or drive an entity binding the client cannot resolve).
VFXAPI.sendMaskMove(player, effectId, 0, entity.position());
// Client-local, same expansion, no packet:
VFXAPI.maskMove(effectId, 0, new Vec3(x, y, z));
mask. is reserved: a user parameter with that prefix would be shadowed by the mask, not merged.
Custom mask shapes (client-local)¶
A mask leaf may reference a shape registered from code instead of a built-in figure. Two kinds
exist, both registered by id and both usable in a datapack mask as a custom leaf.
Composed SDF - a figure built from primitives and operations:
VFXCustomShape shape = ...; // composed primitives + ops
VFXAPI.registerMaskShape(Identifier.fromNamespaceAndPath("mymod", "rune"), shape);
VFXAPI.unregisterMaskShape(id);
VFXCustomShape existing = VFXAPI.maskShape(id); // null when not registered
The composed parts are literal-only: the leaf's animatable mask.p<N>.p<J> params are not fed
into the parts (they are fed to a GLSL plugin, below).
GLSL plugin - a fragment of GLSL injected into the coverage shader. The plugin defines exactly one function, in the leaf's space (world units for a world leaf), negative inside:
world is the depth-reconstructed world position (zero for a screen leaf), uv the screen UV, and
p0/p1 are the leaf's eight animatable params, so a plugin shape stays drivable from the timeline
(keyframes, expr, graph inputs, setParam). Register it with:
VFXAPI.registerMaskShapeGlsl(Identifier.fromNamespaceAndPath("mymod", "pentagram"), plugin);
VFXAPI.unregisterMaskShapeGlsl(id);
Limits and failure behaviour, all of them deliberate:
- A mask may hold at most 2 custom leaves; a third is a per-file parse error (it would alias row 0 in the packed coverage UBO).
- The coverage shader is compiled per distinct set of plugin ids, capped at 4 variants; the variant re-reads the live plugin source, so re-registering a plugin (or a resource reload) does not leave a stale program.
- A plugin that fails to compile degrades only the masks using it to neutral coverage and is
reported once through
VFXLog.warnOnce; it never takes down the mod or another effect. - On the
1.21.11node there is no shader-source hook, so a GLSL-plugin shape renders nothing there; the composed-SDF kind works on every node. - Plugin coverage obeys the same per-leaf fail-closed contract as every other leaf: an unresolved leaf contributes zero and can never be inverted into "everywhere".
vfxweaver:ringed_glsl is a built-in plugin (a screen ring with 8 petal-modulated lobes) shipped
so the path is testable in game; see the guide for the datapack side.
Block/item-particle presets (client-local)¶
Model particles — the particles effect with "particle": "block", "particle": "item" or a
preset id — can be defined from code, mirroring registerDefinitions. A spec draws a block or
an item (exactly one). Each live particle renders as a client-side display entity
(BlockDisplay/ItemDisplay), so vanilla interpolates its motion; the spec's brightness maps onto
the display's brightness override, size onto the transformation scale and spin (degrees/tick)
into a rotation driven by the spec's spinMode/spinAxis (TUMBLE = physical full-3D spin about a
random or fixed axis, YAW = uniform spin about one axis, NONE = no rotation) with spinRandom/
spinFriction/spinRoll tuning its randomness and contact response. Presets are
client-local and never synced; the datapack layer (data/<ns>/vfx_particles/<name>.json) wins
for the same id.
import dev.vfxweaver.effect.VFXBlockParticleSpec;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.item.Items;
VFXBlockParticleSpec ember = VFXBlockParticleSpec.builder(Blocks.MAGMA_BLOCK.defaultBlockState())
.brightness(15, 15) // block-display semantics: -1 = world light, or separate block/sky levels
.gravity(0.8F).friction(0.94F).collide(1.0F).bounce(0.2F)
.size(0.35F).life(60).spin(12.0F)
.spinMode(VFXBlockParticleSpec.SpinMode.TUMBLE) // TUMBLE / YAW / NONE
.spinAxis(new Vector3f(0.0F, 1.0F, 0.0F)) // null = random axis
.spinRandom(1.0F).spinFriction(1.0F).spinRoll(0.5F)
.build();
// Item form: the item's baked model is drawn (skull/tool/ingot particles). `item(ItemStack)`
// is the shorthand for the default physics; a builder is available when you need to tune them.
VFXBlockParticleSpec skull = VFXBlockParticleSpec.item(new ItemStack(Items.SKELETON_SKULL));
boolean skullBlock = skull.hasBlock(); // false
boolean skullItem = skull.hasItem(); // true
// Register/unregister a preset in the local layer; register returns false when it is full (256).
boolean registered = VFXAPI.registerBlockParticle(Identifier.fromNamespaceAndPath("mymod", "ember"), ember);
boolean removed = VFXAPI.unregisterBlockParticle(Identifier.fromNamespaceAndPath("mymod", "ember"));
// Look one up (datapack layer wins). @Nullable.
VFXBlockParticleSpec found = VFXAPI.blockParticle(Identifier.fromNamespaceAndPath("mymod", "ember"));
// Spawn a single block particle immediately on this client (no packet, no effect instance);
// no-op on a dedicated server. `velocity` is in blocks/tick; the spec supplies the physics.
VFXAPI.spawnBlockParticle(ember, new Vec3(x, y, z), new Vec3(0.0, 0.25, 0.0));
A particles effect then reaches the preset with "particle": "mymod:ember"; the effect's
brightness/gravity/friction/collide/bounce/size/life/spin/spin_random/
spin_friction/spin_roll params override its fields. spin_mode and spin_axis are spec/preset
fields only (effect params are numeric), so an effect selects yaw/none by naming a preset. The same
methods register/spawn item presets — the spec carries the model, the API surface is unchanged. See
the particles block and item mode in the effects guide.
Spark presets use the same client-local registry: VFXAPI.registerSpark(id, VFXSparkSpec),
VFXAPI.unregisterSpark(id), VFXAPI.spark(id) and VFXAPI.spawnSpark(spec, position, velocity)
(the client-only spawnSpark is a default no-op on VFXLocalDispatcher, overridden by the mod's
client dispatcher). A spark preset is a vfx_particles file with "kind": "spark"; see the
Spark presets page.
VFXAPI.EffectRequest (fluent builder)¶
// Builds one request and plays it on a player (over the network) or locally on this client:
VFXAPI.EffectRequest.of()
.duration(60) // ticks, 0 = definition default
.param("radius", 2.0F) // constant overrides (repeatable)
.target(entityUuid) // entity UUID targets / entity-anchored overlay slots (repeatable)
.easing(EasingType.EASE_OUT_CUBIC) // or .easing("ease_out_cubic")
.play(player); // or .play() for a client-local play
VFXLocalDispatcher¶
A bridge the client entrypoint (VFXClient) registers via VFXAPI.setLocalDispatcher(...) so playEffect/stopEffect/stopAllEffects can run without a network packet. Other mods don't need to implement it — it's an internal part of the common↔client link of the mod. spawnBlockParticle is a default no-op on the interface (so a dispatcher compiled before block particles still links) and is overridden by the mod's client dispatcher.
Definition registry — VFXDefinitionManager¶
VFXDefinitionManager.get().get(effectId); // VFXDefinition or null
VFXDefinitionManager.get().contains(effectId); // whether the effect exists (built-in or datapack)
VFXDefinitionManager.get().getDefinitions(); // Map<Identifier, VFXDefinition> — snapshot of all known effects
Updated on every /reload (see VFXDefinitionManager.prepare/apply); one broken datapack entry is logged and skipped, the rest load normally.
Registering definitions from code¶
A client-only mod cannot own definitions through a datapack: Fabric does not reload mod-provided
SERVER_DATA packs on a multiplayer client (so data/<ns>/vfx/*.json only loads in single player),
and a server running this mod replaces the whole definition set over vfxweaver:vfx_sync. Register
them from code instead:
// Client init (or any time). Same JSON shape as a datapack file; each entry is parsed with the
// same validation - a broken entry is logged, reported by /vfx validate and skipped, and the
// layer is bounded. Returns the ids that were rejected (empty = all good).
Set<Identifier> failed = VFXAPI.registerDefinitions(Map.of(
Identifier.fromNamespaceAndPath("mymod", "burst"), """
{ "type": "particles", "duration": 40, "shape": "sphere", "params": { "rate": 80, "radius": 1.5 } }
""",
Identifier.fromNamespaceAndPath("mymod", "ring"), """
{ "type": "particles", "duration": 60, "shape": "ring", "params": { "rate": 60, "radius": 2.0 } }
"""));
boolean removed = VFXAPI.unregisterDefinition(Identifier.fromNamespaceAndPath("mymod", "burst"));
Semantics:
- Usable immediately and it survives
/reloadand a server sync — local definitions live in a layer of their own;applySyncedonly replaces the datapack/server set. (A local definition was previously lost on joining a server, andplayEffect("mymod:thing", ...)then failed to resolve.) - Private to this client: a client's local definitions are never synced to other players
(
getRawDefinitions(), the payload source, stays datapack-only). - The datapack/server layer wins for the same id, so a server can still override a local effect.
- Registered ids show up in
getDefinitions(),contains(),/vfx listand (when broken) ingetParseErrors()//vfx validate. - No protocol or datapack-format change; purely additive API.
Network protocol¶
Clientbound: vfxweaver:vfx_trigger (VFXTriggerPayload)¶
| Field | Type | Description |
|---|---|---|
protocolVersion |
byte | Current value — VFXTriggerPayload.PROTOCOL_VERSION (6). The client silently ignores the packet on a version mismatch (see VFXClient.handleTrigger). |
effectId |
Identifier |
Effect id (built-in or datapack) |
action |
VFXAction (PLAY/STOP/SET_PARAM/KEYFRAME/SET_EXPR/MOVE) |
SET_PARAM/KEYFRAME apply to running effect instances: params carries exactly one name → value entry, for KEYFRAME the frame time is in durationTicks (negative = start the segment at the current time and run over |time| ticks), the segment easing in easing; SET_EXPR uses exprParam+exprSource; MOVE uses position + instanceId |
durationTicks |
varint | 0 = definition default, negative = persistent (only for PLAY) |
elapsedTicks |
varint | Resume offset: how far into the timeline the effect already is (only for PLAY, 0 = start fresh). Used when the server re-applies effects after a reconnect. |
params |
Map<String, Float> |
Constant overrides, numbers only |
easing |
EasingType (string) |
|
exprParam / exprSource |
optional strings | Only for SET_EXPR: the parameter name and the raw expression source |
Serverbound: vfxweaver:vfx_request (VFXRequestPayload)¶
Lets a client mod ask the server to play an effect through the definition registry. Fields: protocolVersion (must match VFXTriggerPayload.PROTOCOL_VERSION), effectId, broadcast (boolean), instanceId (long, 0 = allocate), worldPos (optional Vec3), params (max 32), easing (built-in easing name string).
broadcast = false: the effect plays only on the requesting player's client.broadcast = true: the effect plays for every connected player, but the server only honours it from operators (gamemaster level) — anyone else is silently dropped (logged server-side).- Easing: built-in easing names resolve; custom datapack curve names fall back to
LINEARon this path.
Bump PROTOCOL_VERSION on any breaking packet-format change — otherwise old clients silently ignore new packets without a single warning in the log.
See also: guide/ — user guide (commands, datapacks), ARCHITECTURE.md — how rendering works under the hood.