Particles

The particle registry: your own particles from one line of code, a ready-made sprite particle, and the multiplayer bridge that makes them show up for everyone.

Beta decides what a particle name means inside a hardcoded chain of ifs in the world renderer. There is no registry, no factory, no hook, so for fifteen years the answer to "how do I add a particle to b1.7.3" was "you don't, you fake it with an entity". RetroAPI turns that chain into a lookup: a name with a namespace in it (example_mod:spark) is resolved against a registry first, and only unknown names fall through to vanilla.

Three pieces, in the order you'll use them:

  • Register a factory per identifier, on the client, from initRetroClient().
  • Spawn from anywhere, including common gameplay code, with RetroParticles.
  • Draw whatever you like: the built-in RetroSpriteParticle, or your own Particle subclass.

The smallest possible particle

A particle is a texture plus a bit of behavior. Register the texture like any other block texture (it goes into the same expanded terrain atlas, Textures, names & files), then hand the registry a factory that builds the particle:

src/main/java/com/example/example_mod/ExampleMod.java, initRetro()
// Common code: the identifier both sides agree on, and the texture handle.
public static final NamespacedIdentifier SPARK = id("spark");
public static RetroTexture SPARK_TEXTURE;

@Override
public void initRetro() {
    // textures/block/spark.png, reserved on both sides (see Entrypoints & sides)
    SPARK_TEXTURE = RetroTextures.addBlockTexture(id("spark"));
}
src/main/java/com/example/example_mod/ExampleModClient.java, initRetroClient()
RetroParticleRegistry.register(ExampleMod.SPARK, (world, x, y, z, vx, vy, vz) ->
    new RetroSpriteParticle(world, x, y, z, vx, vy, vz, ExampleMod.SPARK_TEXTURE)
        .lifetime(24)        // ticks, ±25% so a burst doesn't vanish all at once
        .scale(0.9F)        // 1.0 is about a vanilla smoke puff
        .gravity(0.02F)      // 0 floats, negative rises
        .drag(0.96F)        // velocity kept per tick
        .tint(0xFFD070)     // 0xRRGGBB multiply
        .shrink());          // fade out by shrinking

That's the whole registration. RetroSpriteParticle is a ready-made Particle that draws one registered texture, so "I want my own particle" doesn't have to mean "I want to write a particle class". Unlike vanilla's digging particle it draws the whole sprite rather than a random quarter of it, so small pixel art reads correctly.

Spawning, from ordinary code

Spawning lives in common code, and mentions no client types at all, so a block's onUse, a block entity's tick, or an item's use method can all do it:

anywhere in common code
// one particle, at rest
RetroParticles.spawn(world, ExampleMod.SPARK, x + 0.5, y + 1.0, z + 0.5);

// one particle, thrown upward
RetroParticles.spawn(world, ExampleMod.SPARK, x + 0.5, y + 1.0, z + 0.5,
    0.0, 0.12, 0.0);

// a puff: 12 of them scattered in a quarter-block, each drifting a little
RetroParticles.spawnCloud(world, ExampleMod.SPARK, x + 0.5, y + 1.0, z + 0.5, 12, 0.25);

// scattered over the volume of one block, the way a furnace dresses itself
RetroParticles.spawnOnBlock(world, ExampleMod.SPARK, x, y, z, 6);

// and vanilla's own names still work, by their beta strings
RetroParticles.spawnVanilla(world, "smoke", x, y, z, 0, 0, 0);
CallWhat it's for
spawn(world, id, x, y, z)one particle, no velocity
spawn(world, id, x, y, z, vx, vy, vz)one particle with a velocity in blocks/tick
spawnCloud(world, id, x, y, z, count, spread)a burst scattered in a cube, each with a small random drift
spawnOnBlock(world, id, x, y, z, count)spread through one block's volume
spawnVanilla(world, name, …)a vanilla particle by its beta name

Multiplayer: the part that used to be silently broken

b1.7.3's protocol has no particle packet, and vanilla's server-side world listener has an empty addParticle body. So particles spawned by server logic reached nobody: your machine puffed smoke in singleplayer and stood there in stony silence on a server. RetroAPI bridges it, exactly the way it bridges sounds: the server sends name, position and velocity to every player within 32 blocks, and the client replays it through its own world, where the registry lookup happens.

Nothing to configure. Spawn from wherever your gameplay code runs. In singleplayer the client is the authority so the particle is made locally, on a dedicated server the bridge carries it. The identifier is what travels, so both sides need the same mod (they already do).

Writing your own particle class

When you want behavior the built-in one doesn't have, physics, trails, a particle that seeks a target, register a factory that builds your own subclass. The factory signature is the spawn call's arguments, so you can vary anything from the spawn site:

src/main/java/com/example/example_mod/ExampleModClient.java
RetroParticleRegistry.register(ExampleMod.RUNE, MyRuneParticle::new);
src/main/java/com/example/example_mod/MyRuneParticle.java
@Environment(EnvType.CLIENT)
public class MyRuneParticle extends RetroSpriteParticle {

    public MyRuneParticle(World world, double x, double y, double z,
            double vx, double vy, double vz) {
        super(world, x, y, z, vx, vy, vz, ExampleMod.RUNE_TEXTURE);
        exactLifetime(40).gravity(-0.01F);   // rises, lives two seconds
    }

    @Override
    public void tick() {
        super.tick();
        // spiral: nudge the velocity each tick
        this.velocityX += Math.sin(this.particleAge * 0.4) * 0.004;
        this.velocityZ += Math.cos(this.particleAge * 0.4) * 0.004;
    }
}

Subclass RetroSpriteParticle (as above) to keep the terrain-atlas drawing and the chainable setup, or extend vanilla's Particle directly if you want to draw something else entirely. RetroAPI widens the fields you need, textureId, scale, red/green/blue, particleAge, maxParticleAge, gravityStrength, so a subclass can set itself up without reimplementing the base class.

Client only. RetroParticleRegistry, RetroSpriteParticle and any subclass of them are client classes: register them from initRetroClient() and never mention them from common code (Entrypoints & sides's classloading rule). RetroParticles.spawn(...) is the common-side half, and it's the only half your gameplay code needs.

Where particles are the right answer

Particles are cheap and they cost nothing when nobody is looking, which makes them the natural fit for feedback: a machine working, an item enchanting, a portal humming, a block rejecting an input. They are not a good fit for state that must persist or that another player must be able to inspect later, that's what block states and components are for. If you catch yourself encoding information in particle color that gameplay depends on, reach for a state instead and use the particle to announce it.

Next: the things that are bigger than one block, and the position maths that makes them bearable.