Custom Effects
This guide walks through creating a custom effect from scratch and registering it so it works in markup and programmatically.
Step 1: Implement the Effect
Section titled “Step 1: Implement the Effect”Create a class that extends BaseEffect:
package com.yourmod.effects;
import net.minecraft.Util;import net.minecraft.util.Mth;import net.tysontheember.emberstextapi.immersivemessages.effects.BaseEffect;import net.tysontheember.emberstextapi.immersivemessages.effects.EffectSettings;import net.tysontheember.emberstextapi.immersivemessages.effects.params.Params;import org.jetbrains.annotations.NotNull;
/** * A custom effect that rotates each character based on its index, * creating a fan-out rotation pattern. */public class FanEffect extends BaseEffect {
private final float maxAngle; private final float speed;
public FanEffect(@NotNull Params params) { super(params); this.maxAngle = params.getDouble("angle") .map(Number::floatValue) .orElse(0.5f); // Default: ~28 degrees this.speed = params.getDouble("f") .map(Number::floatValue) .orElse(1.0f); }
@Override public void apply(@NotNull EffectSettings settings) { if (settings.isShadow) { return; // Don't rotate shadows }
float time = Util.getMillis() * 0.002f * speed; float normalizedIndex = settings.index * 0.1f; settings.rot += Mth.sin(time + normalizedIndex) * maxAngle * normalizedIndex; }
@NotNull @Override public String getName() { return "fan"; }}Key Points
Section titled “Key Points”- Extend
BaseEffect— it handles parameter storage and provides color parsing helpers. - Accept
Paramsin the constructor — this is how markup parameters are passed in. - Modify
EffectSettingsinapply()— this is where the visual transformation happens. - Return a consistent name from
getName()— used for serialization and registry lookup. - Check
settings.isShadowif your effect shouldn’t modify shadow rendering.
Step 2: Register the Effect
Section titled “Step 2: Register the Effect”Register during your mod’s client setup event:
import net.minecraftforge.eventbus.api.SubscribeEvent;import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;import net.tysontheember.emberstextapi.immersivemessages.effects.EffectRegistry;
public class YourMod {
@SubscribeEvent public static void onClientSetup(FMLClientSetupEvent event) { event.enqueueWork(() -> { EffectRegistry.register("fan", FanEffect::new); }); }}import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;import net.neoforged.bus.api.SubscribeEvent;import net.tysontheember.emberstextapi.immersivemessages.effects.EffectRegistry;
public class YourMod {
@SubscribeEvent public static void onClientSetup(FMLClientSetupEvent event) { event.enqueueWork(() -> { EffectRegistry.register("fan", FanEffect::new); }); }}import net.fabricmc.api.ClientModInitializer;import net.tysontheember.emberstextapi.immersivemessages.effects.EffectRegistry;
public class YourModClient implements ClientModInitializer {
@Override public void onInitializeClient() { EffectRegistry.register("fan", FanEffect::new); }}Step 3: Use It
Section titled “Step 3: Use It”Once registered, your effect is available in markup:
<fan>Fan rotation effect</fan><fan angle=1.0 f=2.0>Strong fast fan</fan>And programmatically:
TextSpan span = new TextSpan("Fan text") .effect("fan angle=0.8 f=1.5");
// Or via the registry:Effect fanEffect = EffectRegistry.parseTag("fan angle=0.8");span.addEffect(fanEffect);Parameter Parsing
Section titled “Parameter Parsing”The Params interface provides typed access to markup parameters:
| Method | Returns | Use For |
|---|---|---|
getDouble(key) | OptionalDouble | Numeric values (speed, amplitude, etc.) |
getBoolean(key) | OptionalBoolean | Flag values |
getString(key) | Optional<String> | Text values (color hex, sound IDs, etc.) |
Always provide sensible defaults using .orElse(). Never assume a parameter is present.
this.amplitude = params.getDouble("a") .map(Number::floatValue) .orElse(1.0f);
this.enabled = params.getBoolean("on") .orElse(true);
this.colorHex = params.getString("col") .orElse("FFFFFF");Validation
Section titled “Validation”Use ValidationHelper.clamp() to keep parameter values in safe ranges:
import net.tysontheember.emberstextapi.immersivemessages.effects.params.ValidationHelper;
this.amplitude = ValidationHelper.clamp( "myeffect", // Effect name (for log messages) "a", // Parameter name (for log messages) params.getDouble("a").map(Number::floatValue).orElse(1.0f), 0f, // Minimum 50f // Maximum);This logs a warning if the value was clamped and prevents extreme values from causing rendering issues.
Multi-Layer Effects (Siblings)
Section titled “Multi-Layer Effects (Siblings)”If your effect needs to render additional character layers (glow, shadow copy, displaced slice), add siblings:
@Overridepublic void apply(@NotNull EffectSettings settings) { // Create a glow copy of this character EffectSettings glow = settings.copy(); glow.r = 1.0f; glow.g = 0.8f; glow.b = 0.0f; // Orange glow glow.a *= 0.25f; // 25% opacity glow.scale *= 1.3f; // Slightly larger
settings.addSibling(glow);
// The main character renders normally (no changes to settings)}Siblings are rendered as additional passes after the main character. The neon and glitch effects use this mechanism extensively.
Color Parsing
Section titled “Color Parsing”BaseEffect provides color parsing helpers:
// From a Params parameter named "col"float[] rgb = parseColor(params, "col", new float[]{1f, 1f, 1f}); // Default white
// From a hex string directlyOptional<float[]> rgb = parseColor("#FF0000");
// Then use in apply():settings.r = rgb[0];settings.g = rgb[1];settings.b = rgb[2];Testing Your Effect
Section titled “Testing Your Effect”- Register the effect during client setup.
- Run the game and use
/eta send @p 100 <fan>Test</fan>. - Check the client log for registration warnings or errors.
- Adjust parameters and iterate.
For unit tests, create effect instances directly and verify their behavior on mock EffectSettings objects:
Params params = TypedParams.of("angle", 0.5, "f", 1.0);FanEffect effect = new FanEffect(params);
EffectSettings settings = new EffectSettings();settings.index = 0;effect.apply(settings);
// Verify the effect modified rot as expected