-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture Overview
Read this before adding any registered content (entities, items, menus, attributes) or trying to share code across loaders. It explains the one non-obvious decision the whole codebase is shaped around.
PlayerMob is an Architectury project with four Gradle modules:
| Module | Role |
|---|---|
common |
All gameplay logic. Compiles against vanilla Minecraft + the Fabric loader's mixin/annotation deps only. |
fabric |
Fabric ModInitializer entrypoint + client. |
forge |
Forge @Mod entrypoint + client. |
neoforge |
NeoForge @Mod entrypoint + client + optional Dungeon Train integration. |
The entity, AI goals, personality system, skin system, and menus all live in
common. The loader modules are thin — they register things and back-fill shared
references, then hand off to common.
Normally Architectury's API runtime abstracts registration so you write it once
in common. PlayerMob does not use the Architectury API runtime, because
Architectury API 13.x (the 1.21 line) ships Fabric + NeoForge only — Forge was
dropped upstream. To keep all three loaders alive, registration is performed in
each loader's entry class with that loader's native API:
| Fabric | Forge | NeoForge | |
|---|---|---|---|
| Registry calls | Registry.register(...) |
DeferredRegister |
DeferredRegister |
| Attributes | FabricDefaultAttributeRegistry |
EntityAttributeCreationEvent |
EntityAttributeCreationEvent |
| Creative tab | ItemGroupEvents |
BuildCreativeModeTabContentsEvent |
BuildCreativeModeTabContentsEvent |
| Menu type | ExtendedScreenHandlerType |
IForgeMenuType |
IMenuTypeExtension |
This mirrors the AdventureItemNames pattern. The three entry classes are nearly
identical apart from these loader-flavoured calls:
Implication for you: to add a new registered object, you must register it in
all three entry classes (or behind a single shared factory like
entityTypeBuilder() below), then store the result somewhere common can read it.
Because common can't perform registration but still needs the registered
EntityType, Item, and MenuType (for goal predicates, the renderer, spawn-egg
data, etc.), the resulting references are parked on a shared holder:
PlayerMobRegistry.
public final class PlayerMobRegistry {
public static final ResourceLocation PLAYER_MOB_ID = /* playermob:player_mob */;
// Set once by each loader entry on boot; read everywhere in common.
public static EntityType<PlayerMobEntity> PLAYER_MOB;
public static Item PLAYER_MOB_SPAWN_EGG;
public static MenuType<PlayerMobMenu> PLAYER_MOB_MENU;
public static PlayerMobMenuOpener MENU_OPENER;
// The single EntityType.Builder every loader uses, so size / category /
// tracking range stay identical across loaders.
public static EntityType.Builder<PlayerMobEntity> entityTypeBuilder() { ... }
}Each loader entry registers its objects, assigns them onto these static fields,
then calls
PlayerMob.init()
— the shared post-registration landing point.
Boot-order caveat. These fields are
nulluntil the loader's boot assigns them. On Fabric they're set inline inonInitialize; on Forge/NeoForge they're back-filled inFMLCommonSetupEvent(after everyDeferredRegisterhas run). Anycommoncode that reads them must run after boot — never from a static initializer that could fire earlier.
The same back-fill trick powers two extension seams:
-
MENU_OPENER(PlayerMobMenuOpener) — opens the inventory menu with a loader-specific call. -
TrainConfinement.install(...)— installs an optional cross-mod integration.
Both are documented in Soft-Dependency Integrations.
The renderer is client-only
(PlayerMobRenderer,
annotated @Environment(CLIENT)). To avoid accidentally classloading it on a
dedicated server, constants the server needs are defined on the entity, not the
renderer — e.g. PlayerMobEntity.SKIN_COUNT is public so server-side
finalizeSpawn can roll a skin index without touching the renderer. Keep this in
mind when adding shared constants: if the server reads it, it doesn't belong on a
client-only class.
Client registration (entity renderer, model layer, menu screen) happens in each
loader's *Client entrypoint.
A mixin config is wired up and ready but currently empty:
playermob.mixins.json
points at the package games.brennan.playermob.mixin with empty mixins,
client, and server lists. The config is referenced from fabric.mod.json,
neoforge.mods.toml, and mods.toml, so if you need a vanilla-class hook, drop a
mixin into that package and add it to the config — no new wiring required.
PlayerMob deliberately ships zero mixins today; all behaviour is achieved with vanilla entity AI, overrides, and events.
- Registering or configuring mobs without code → Spawning and NBT
- The public Java surface of the entity → Entity API Reference
- Extending the AI → AI Goals
- Adding an optional cross-mod integration → Soft-Dependency Integrations
Getting started
No-code (datapacks)
Java API
Patterns