Chapter 2, Three Doors In
Three entrypoints, one rule about which classes meet which side, and why your textures are safe everywhere.
When the game starts, the loader knocks on up to three doors in your mod and walks through whichever ones it finds. Each door opens onto a different side of the game. Get them straight now and the rest of the guide reads like a list of things you do behind the first door.
You already met the entrypoints block in Chapter 1's fabric.mod.json. Here it is again, all three doors wired up:
"entrypoints": {
"init": [
"com.example.example_mod.ExampleMod"
],
"client-init": [
"com.example.example_mod.ExampleModClient"
],
"server-init": [
"com.example.example_mod.ExampleModServer"
]
},| Entrypoint | Class & method | Runs on… |
|---|---|---|
init | ExampleMod.init() | both sides, client and dedicated server |
client-init | ExampleModClient.initClient() | the client only |
server-init | ExampleModServer.initServer() | a dedicated server only |
Each entrypoint is an OSL interface with exactly one method to implement. The three are ModInitializer.init(), ClientModInitializer.initClient(), and ServerModInitializer.initServer(). You may list several classes under one entrypoint, the loader calls each in turn, but one of each is plenty to start, and that's how the showcase is built.
The skeletons
Three small classes, each implementing one interface. This is the whole shape of them; the bodies come later.
import net.ornithemc.osl.entrypoints.api.ModInitializer;
public class ExampleMod implements ModInitializer {
public static final String MOD_ID = "example_mod";
@Override
public void init() {
LOGGER.info("initializing example mod!");
// … register everything here (see below) …
}
}import net.ornithemc.osl.entrypoints.api.client.ClientModInitializer;
public class ExampleModClient implements ClientModInitializer {
@Override
public void initClient() {
// renderers, GUI screens, client packet listeners …
}
}import net.ornithemc.osl.entrypoints.api.server.ServerModInitializer;
public class ExampleModServer implements ServerModInitializer {
@Override
public void initServer() {
// server packet listeners, dedicated-server-only logic …
}
}The big idea: register everything in init()
Here is the rule that shapes every later chapter: your content is registered in the common init(), on both sides. Blocks, items, block entities, the dimension, the mob, achievements, recipes, network channels, all of it goes through the one door both sides walk through, so both sides agree on what exists.
That makes ExampleMod.init() the table of contents for the whole mod. Read it top to bottom and you've read the syllabus:
public void init() {
LOGGER.info("initializing example mod!");
// blocks, Chapter 4
EXAMPLE_BLOCK = RetroBlockAccess.create(Material.STONE)
.strength(1.5f, 10.0f).texture(id("example_block")).register(id("example_block"));
// … SIDED_BLOCK, PIPE_BLOCK …
// block entities, Chapter 6
RetroBlockEntities.register(id("counter"), ExampleCounterBlockEntity.class);
// … COUNTER_BLOCK, CRATE_BLOCK, FREEZER_BLOCK …
// items, Chapter 5
SUSPICIOUS_SUBSTANCE = RetroItemAccess.create()
.maxStackSize(64).texture(id("suspicious_substance")).register(id("suspicious_substance"));
// … JUMP_STICK …
// dimension, Chapter 10
EXAMPLE_DIMENSION = RetroDimensions.register(id("example_dim"), ExampleDimension::new);
// … EXAMPLE_PORTAL …
// entities, Chapter 7
EXAMPLE_MOB = RetroEntities.register(ExampleEntity.ID, ExampleEntity.class)
.factory((MobFactory) ExampleEntity::new);
// recipes, Chapter 11
RecipeRegistrationCallback.EVENT.register(ExampleMod::registerRecipes);
// achievements, Chapter 9
ExampleAchievements.register();
// networking, Chapter 12
ExampleNetworking.registerChannels();
}Every line above is one of the next chapters. Chapter 4 takes the blocks, Chapter 5 the items, Chapter 6 the block entities, Chapter 7 the mob, Chapter 9 the achievements, Chapter 10 the dimension and portal, Chapter 11 the recipes, Chapter 12 the channels. Notice what's not here: nothing about how things look. Renderers and screens live behind the client door, that's the discipline we get to next.
Two registrations are deferred behind events rather than run inline. Recipes go through RecipeRegistrationCallback so they land after vanilla's recipe list exists; network channels are opened in init() (both sides must agree they exist), while the listeners that use them are wired up per side. More on both in their chapters.
The thing about Beta 1.7.3: there is no integrated server
Modern Minecraft runs a hidden "integrated server" inside singleplayer, a real server thread the client talks to over an in-memory connection. Beta 1.7.3 has no such thing. Singleplayer is just the client, simulating the world directly, by itself.
This single fact reshapes how you think about sides:
- Singleplayer is the client. The world ticks, mobs spawn, blocks save, all inside the client process. So
client-initruns in singleplayer, andserver-initdoes not. - "Server" means a dedicated server, a separate, headless process with no window, launched with
runServeror a server jar. That's the only placeinitServer()ever fires. world.isRemotetells you "am I a remote multiplayer client?" In singleplayer it isfalse, you are the authority, the same way a server is. It'strueonly on a client connected to a remote server, where the world is a shadow of the server's.
If you're coming from modern Fabric/Forge, retrain this reflex: "client-side" and "logical server-side" are not two threads in singleplayer here, they're the same code. Gameplay logic guarded by if (!world.isRemote) runs in singleplayer (good, it's the authority) and on a dedicated server, but is skipped on a remote multiplayer client.
Why your textures are safe on a server (the question everyone asks)
Look back at the init() table of contents: it calls .texture(id("example_block")) and, in Chapter 4's sided block, RetroTextures.addBlockTexture(...). Those run on a dedicated server too, there's no window there, no GPU, no atlas. Does the server try to decode a PNG and fall over?
No. And this is by design. A texture call in common init() is pure bookkeeping. It records a RetroTexture handle and reserves a sprite slot (a number ≥ 256), that's all. No file is opened, no pixels are read. The actual PNG decoding and atlas compositing happen client-side only, much later, when the texture atlas is being built for rendering.
That's exactly why texture registration does not need to move into client-init. The bookkeeping has to happen on both sides anyway, so that both sides assign the same sprite slots and stay in sync. The server keeps the ledger; only the client ever paints from it.
The shape of a RetroTexture: it carries a NamespacedIdentifier and a public int id (the reserved sprite index). On a server that id is a number nobody ever draws. On a client it's the live index into the atlas. Same object, same number, harmless either way. Full story in Chapter 3.
Classloading discipline: the rule that prevents crashes
Here is the one rule that, if you keep it, you'll almost never see a sided crash:
Never reference a client-only class from code that loads on a server, and never reference a server-only class from code that loads on a client.
"Client-only" means anything under net.minecraft.client.*, screens, renderers, models, the Minecraft class itself, plus client networking like ClientPlayNetworking. "Server-only" means ServerPlayerEntity, ServerPlayNetworking, MinecraftServer, and friends. A dedicated server jar simply does not contain the client classes; touching one from server-loaded code throws NoClassDefFoundError the instant the JVM tries to load the referencing class, often before your method even runs.
The fix isn't to scatter if checks. It's to keep dangerous classes physically out of reach of the wrong side, by which class mentions which. The showcase does this cleanly.
Client-only stays behind the client door
ExampleCrateScreen extends a net.minecraft.client class, so it must never be classloaded on a server. It isn't, it's only ever named inside a lambda registered in ExampleModClient, and lambdas don't load their bodies' classes until they actually run (which, on a server, is never):
RetroGuiRegistry.register(ExampleMod.id("crate"), new RetroGuiHandler(
(player, inventory) -> new ExampleCrateScreen(player.inventory, (ExampleCrateBlockEntity) inventory),
ExampleCrateBlockEntity::new));The common init() never types the word ExampleCrateScreen. A server loads ExampleMod happily, because nothing in it points at a client class.
Server-only stays behind the server door
Symmetrically, ServerPlayerEntity appears only in ExampleModServer and in a mixin listed under "server" (its ServerPlayerTickMixin). The client never loads either:
import net.minecraft.entity.player.ServerPlayerEntity;
import net.ornithemc.osl.networking.api.server.ServerPlayNetworking;
public class ExampleModServer implements ServerModInitializer {
public static void welcomeOnceReady(ServerPlayerEntity player) {
// … server-only types live here, and only here …
ServerPlayNetworking.send(player, ExampleNetworking.WELCOME, buf ->
buf.writeString("Welcome to the server, " + player.name + "!"));
}
}Mixins obey the same rule
A mixin is just another class that gets loaded onto whatever side it's listed under, so the mixin config splits its entries into three lists for exactly this reason. A client-only mixin (one that @Mixin-targets a client class) goes under "client"; a server-only one under "server"; everything common under "mixins":
{
"required": true,
"minVersion": "0.8",
"package": "com.example.example_mod.mixin",
"compatibilityLevel": "JAVA_8",
"mixins": [
"LivingEntityJumpMixin",
// … common mixins, loaded on both sides …
],
"client": [
"PlayerTickMixin",
// … client-only mixins …
],
"server": [
"ServerPlayerTickMixin"
],
"injectors": {
"defaultRequire": 1
}
}Put a mixin that touches a client class in the "client" list and a dedicated server never tries to load it. Same idea as the entrypoints, same payoff: the wrong class never reaches the wrong side. The full mixin tour, all twelve graded examples, is Chapter 13.
Rule of thumb. Common code may freely mention shared classes (Block, Item, World, PlayerEntity, your own block/item classes). The moment a type is named ...client... or is a Server*, ask "which door does this belong behind?" and put the class that mentions it there.
Three doors, one ledger shared between them. Next we walk through the first door's quietest job: turning PNGs and text files into sprites, names, and sounds.