Chapter 7, Creatures

A mob is easy to make and surprisingly deep to network. We'll do both, the easy class first, then the honest truth about how a server tells everyone it exists.

A block sits at a fixed position; the world always knows where it is. An entity moves. It has a position that changes every tick, a rotation, a velocity, and, on a dedicated server, it lives entirely in the server's memory until the server decides to tell the clients about it. That last part is where modded entities in Beta 1.7.3 get interesting, and we'll spend most of this chapter there. But first, the part that's genuinely simple: writing the creature.

The creature class

Here is a complete, working custom mob. It extends LivingEntity, picks a skin, refuses to despawn, and tells RetroAPI how to recognize it on the wire. That's all four lines of substance.

src/main/java/com/example/example_mod/ExampleEntity.java
package com.example.example_mod;

import com.periut.retroapi.entity.spawn.RetroMobSpawnData;

import net.minecraft.entity.LivingEntity;
import net.minecraft.world.World;

import net.ornithemc.osl.core.api.util.NamespacedIdentifier;

public class ExampleEntity extends LivingEntity implements RetroMobSpawnData {

	public static final NamespacedIdentifier ID = ExampleMod.id("example_mob");

	public ExampleEntity(World world) {
		super(world);
		// Skins are plain texture paths; this reuses the vanilla zombie skin.
		// Ship your own at assets/example_mod/... and point to it here if you like.
		this.texture = "/mob/zombie.png";
	}

	@Override
	protected boolean canDespawn() {
		return false; // stick around so you can find it again after re-logging
	}

	@Override
	public NamespacedIdentifier getHandlerId() {
		return ID; // ties multiplayer spawn packets back to the registration
	}
}

Extending LivingEntity directly gives you the machinery of a living thing, health, gravity, taking damage, the idle bob, but no brain. This mob just stands there. To make it walk, hunt, or flee, you'd extend a vanilla mob like ZombieEntity (and inherit its AI) or write your own tick logic. We keep it minimal on purpose; the lesson here is networking, not pathfinding.

Two fields earn their keep. texture is a plain classpath path, here we borrow the vanilla zombie skin, but you'd point it at your own PNG. And getHandlerId(), required by the RetroMobSpawnData interface, returns the entity's registration id. Hold onto that method; it's the linchpin of the whole networking story below.

Registering it

Registration is one fluent call in the common entrypoint, and the renderer is one call in the client entrypoint, and they must stay on those two sides.

src/main/java/com/example/example_mod/ExampleMod.java, init()
// Register a custom mob. The MobFactory tells RetroAPI (and the multiplayer
// spawn packet handler on the client) how to construct one from a World.
// The renderer is registered on the client only, see ExampleModClient.
EXAMPLE_MOB = RetroEntities.register(ExampleEntity.ID, ExampleEntity.class)
	.factory((MobFactory) ExampleEntity::new);
src/main/java/com/example/example_mod/ExampleModClient.java, initClient()
// Tell the client how to draw the example mob: a generic biped (player/zombie
// shaped) model with a 0.5-block shadow. Write your own EntityRenderer/model
// subclass for custom shapes.
RetroEntityRenderers.register(ExampleEntity.class,
	new LivingEntityRenderer(new BipedEntityModel(), 0.5F));

The renderer registration lives in initClient() and only there. LivingEntityRenderer and BipedEntityModel are client classes, they touch OpenGL. Put that line in the common init() and a dedicated server crashes the instant it tries to load a graphics class it doesn't have. Logic in common, drawing on the client: the same wall that separated the Container from the Screen in Chapter 6.

Spawning one

Putting a mob into the world is three lines: construct it from the world, place it, and hand it to spawnEntity. The showcase does this in its per-player setup so you find one waiting at your feet on every join.

src/main/java/com/example/example_mod/ExamplePlayerSetup.java, run()
// Spawn one example mob where the player stands.
ExampleEntity mob = new ExampleEntity(world);
mob.setPositionAndAngles(player.x, player.y, player.z, player.yaw, 0.0F);
world.spawnEntity(mob);

That's the whole spawning API: build, position, spawn. On the server, world.spawnEntity is also the moment the networking machinery wakes up, which brings us to the real subject of this chapter.

The networking brief

This is the part the easy class above quietly depends on, and it's worth understanding rather than trusting. Entities are the one place where modding Beta 1.7.3 forces you to think about the client/server split in detail, so we'll take it slowly.

Singleplayer changes nothing, because there's no server

In Beta 1.7.3 there is no integrated server. Singleplayer runs the whole world client-side, in one process (Chapter 2 tells that story in full). The mob you spawn is the mob you render; the position field you read is the position field that ticked. Nothing crosses a network because there is no network. Everything below this point is about the dedicated server case, and on a dedicated server, the server simulates the mob and the clients have to be told it exists.

Why vanilla's spawn packet can't carry your mob

When a vanilla server spawns a mob, it sends a spawn packet that says, in effect, "make entity type 54 (a zombie) at these coordinates." The client looks up type 54 in a fixed table and builds a zombie. Your mob has no number in that table. Hand a vanilla spawn packet an unknown type and the client throws IllegalArgumentException and crashes, there's no graceful fallback in the old code.

So RetroAPI replaces the spawn path for registered entities with its own OSL channel. For living entities that channel is retroapi:entity_spawn_mob. The packet carries everything the client needs to rebuild the mob from scratch:

  • your registration id, the string from getHandlerId(), so the client knows which modded entity this is;
  • the entity's network id and its position (sent as fixed-point integers, ×32, the way beta encodes coordinates);
  • its yaw and pitch;
  • and a snapshot of its DataTracker, the small bag of synced values (is it on fire? is it a baby?) that the engine keeps in step automatically.

The client receives this frame, looks up your registration by id, runs the registered factory to make a fresh instance, and then stamps the position, rotation, and tracker data onto it. That round trip, server id out, client factory in, is exactly why ExampleEntity implements RetroMobSpawnData.getHandlerId(). Without it, the server-side instance has no way to tell the client which factory to run, and the mob would never appear.

Living entities: the MobFactory

Look back at the registration: .factory((MobFactory) ExampleEntity::new). A MobFactory has one method, and its signature is the key:

com.periut.retroapi.entity.client.MobFactory
@FunctionalInterface
public interface MobFactory {
	LivingEntity create(World world);
}

It takes a World and nothing else. That's deliberate. A living entity is constructed empty, at the world's origin, and then the spawn frame's position and tracker data are applied to it. The client receives the frame, calls your factory to get a blank mob, and fills it in. The cast (MobFactory) in the registration isn't decoration, it's how RetroAPI knows you're registering a living entity, which we'll see matters in a moment.

Non-living entities: the EntityFactory

Projectiles, minecarts, falling blocks, anything that isn't alive takes a different factory with a different signature:

com.periut.retroapi.entity.client.EntityFactory
@FunctionalInterface
public interface EntityFactory {
	Entity create(World world, double x, double y, double z);
}

The difference is the three doubles. A non-living entity gets its spawn position as part of construction, not as an after-the-fact stamp. Think of an arrow: it comes into existence already in flight, at a specific point, and where it starts is inseparable from what it is. So you register it with the four-argument factory instead:

the non-living variant (illustrative)
RetroEntities.register(MyArrow.ID, MyArrow.class)
	.factory((EntityFactory) MyArrow::new);

Many non-living entities don't take their position in the constructor, though, they're built from just a World exactly like a mob, and positioned afterward. For those, use the SimpleEntityFactory: it's the extends Entity twin of MobFactory, same one-argument shape, but it returns a plain Entity instead of a LivingEntity. So an entity with a (World) constructor registers just as cleanly as a mob does, no need to make it living just to satisfy the API:

com.periut.retroapi.entity.client.SimpleEntityFactory
@FunctionalInterface
public interface SimpleEntityFactory {
	Entity create(World world);
}
a non-living entity with a (World) constructor
RetroEntities.register(GravelChunk.ID, GravelChunk.class)
	.factory((SimpleEntityFactory) GravelChunk::new);

The cast (SimpleEntityFactory) plays the same role as (MobFactory): it tells RetroAPI which overload you mean, and it's required only because a class with more than one constructor makes GravelChunk::new ambiguous, a plain lambda (world) -> new GravelChunk(world) needs no cast. The three factory shapes never overlap: a (World) reference returning a LivingEntity is a MobFactory, one returning a plain Entity is a SimpleEntityFactory, and a four-argument one is an EntityFactory.

And here's the rule that ties them together: a registration is living or non-living based on which factory you hand it. Pass a MobFactory and it's marked living; pass an EntityFactory or SimpleEntityFactory and it isn't. If you ever need to be explicit, or override the inference, the registration also exposes .living() and .nonLiving().

Tracking: how far, how often, with velocity?

Once an entity exists on the server, the server has to decide who gets told about it and how chatty those updates are. That's the tracking configuration, and you tune it on the registration:

EntityRegistration.tracking(...)
public EntityRegistration tracking(int distance, int period, int sendVelocity)

Three knobs: distance is how far away (in blocks) a client has to be before the server bothers telling it about this entity; period is how many ticks between position updates; and sendVelocity chooses whether each update also carries the entity's velocity. That last one is a tri-state, with named constants on EntityRegistration:

ConstantMeaning
SEND_VELOCITY_UNSETlet the tracker decide (the vanilla default)
SEND_VELOCITY_FALSEnever send velocity
SEND_VELOCITY_TRUEalways send velocity

The defaults are sensible, our mob never calls tracking at all and behaves fine. You'd reach for it on a fast projectile, where the client needs velocity to smoothly extrapolate the path between updates rather than teleporting it in jerks. Mobs that mostly stand and walk are happy with the defaults.

What syncs after spawn, and what doesn't

The spawn frame is a one-time event. After it, the entity's position and rotation ride vanilla's normal entity tracker, ticking out to nearby clients at the period you set. That part is automatic.

What is not automatic is custom state. The DataTracker carries a fixed set of small synced values, "on fire," "sneaking," and a handful of others the engine manages, and those stay in step for free. Anything beyond that bag is yours to sync. If your mob has a custom rage meter or a charge level you want every client to see, the engine will not move it for you; you send it with your own packet (Chapter 12), or, if it's a number you're drawing in a GUI rather than on the entity, with the container properties from Chapter 6. The freezer's lesson reappears here: anything you visualize, you budget a sync for.

Need to ship a few extra fields in the spawn packet itself, a variant, a color, a name picked at birth? RetroMobSpawnData has a writeExtra/readExtra hook for exactly that, appending your own bytes after the standard mob payload. It's an advanced pointer; the showcase doesn't need it, but it's there when you do.

Persistence: the world stays vanilla-openable

The same sidecar discipline from Chapter 6 applies to entities. A modded entity in vanilla's Entities NBT would break the world for unmodded players, so RetroAPI's entity sidecar saves modded entities by their string id, off to the side. Open the world in plain Beta 1.7.3 and it loads, the mob simply isn't there. Put the mod back and the mob returns, exactly where it was. (And because our mob sets canDespawn() to false, it really does wait for you across re-logs, rather than wandering off into the void.)

That's the whole bargain: an easy class on top, a careful spawn protocol underneath, and a sidecar keeping your world portable. You write four lines of mob and RetroAPI handles the rest of the conversation.

Our world now has machines that work and a simple creature that wanders it. But that mob just stands there. The next chapter takes this same entity foundation and builds the showcase's most COMPLEX mob on it, the Aerbunny: it rides on your head and glides you safely down, breeds, panics and flees when struck, and along the way it teaches the render-state and input networking a living thing actually needs. On to Chapter 8.