Skip to content

server: Add typed entity components - #1328

Open
cqdetdev wants to merge 13 commits into
df-mc:masterfrom
cqdetdev:entity-overhaul/components
Open

server: Add typed entity components#1328
cqdetdev wants to merge 13 commits into
df-mc:masterfrom
cqdetdev:entity-overhaul/components

Conversation

@cqdetdev

@cqdetdev cqdetdev commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an open, typed component system to entities. This is the first of
three planned PRs modernising the entity API and is fully additive:
no existing API changes, no behavioural changes for existing entities,
and byte-identical network output for all built-in entity types.
The
existing Behaviour model is untouched and continues to work.

Problem

Entity state and logic currently live in a single opaque value
(EntityData.Data any) holding one Behaviour. In practice this closes
the entity system to external packages:

  • Custom state cannot be attached to an entity you didn't define.
  • Custom state can never render client-side: the session builds actor
    metadata from a fixed set of type assertions on known behaviours.
  • Custom state cannot persist: entity NBT is split between the handle
    and the EntityType.
  • Blocks and items can only ever spawn the entity types hardcoded in
    EntityRegistryConfig's closures.

What this adds

Typed components on entities, accessed through keys created at
registration:

var Charged = world.RegisterComponent[ChargedData]("myplugin:charged")

type ChargedData struct{ Level int }

// On any entity — a vanilla arrow, another package's entity, a player:
Charged.Attach(e, ChargedData{Level: 3})
if c := Charged.Of(e); c != nil { // pointer for in-place mutation
    c.Level++
    world.MarkMetaDirty(e) // resend metadata after mutating a syncer
}
Charged.Detach(e)

Components may implement optional capabilities:

  • TickerComponent — per-tick logic, run before the entity's
    behaviour. Ticker lists are cached per entity, so components may
    attach/detach components (including themselves) during their tick.
  • MetaSyncer — contributes client-visible metadata through typed
    setters (SetScoreTag, SetVariant, SetScale, SetColour and SetFlag).
    Runs after built-in metadata; Attach/Detach
    resend automatically.
  • NBTSaver — persists with the entity under a Components compound
    in chunk NBT. Data saved under component names not registered in the
    running process round-trips losslessly to the next save.

Declarative entity types replacing hand-written EntityType
boilerplate, validated eagerly at registration (unregistered component
types, duplicates and multiple behaviours panic at init, not at first
spawn):

var AcidBall = entity.RegisterType(entity.Spec{
    Name:      "myplugin:acid_ball",
    NetworkID: "minecraft:snowball", // rendered client-side as a snowball
    Box:       cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125),
    Components: func() []any {
        return []any{
            entity.ProjectileBehaviourConfig{Gravity: 0.05, Drag: 0.01}.New(),
            AcidTint{}, // MetaSyncer: green potion swirl, previously impossible
        }
    },
})

A Behaviour among a Spec's components becomes the entity's main
behaviour, so spec-based types get full existing physics today.

Open spawning — any registered spec type can be spawned by name
from block, item or user code, without touching EntityRegistryConfig:

ent, ok := tx.SpawnEntity("myplugin:acid_ball", opts, Owned{Owner: p.H()})

Dependency

This PR pins HashimTheArab/gophertunnel#89, which adds the typed protocol metadata keys used here. The dependency PR is based on the exact gophertunnel revision Dragonfly already used, so the replacement adds no unrelated fork changes.

cqdetdev added 4 commits July 13, 2026 23:01
Entities gain an open set of typed components stored on their handle,
accessed through ComponentKey created by RegisterComponent. Components
may implement TickerComponent to run per-tick logic, MetaSyncer to
contribute client-visible metadata and NBTSaver to persist with the
entity; saved data of unregistered component types or with malformed
values round-trips through the world save untouched. MarkMetaDirty
resends metadata after in-place mutation, ticking components are cached
per entity so components may attach and detach components during their
own tick, and Tx.SpawnEntity spawns any registered Spawner type by
name, opening entity creation to external packages.

Component attach order is preserved and deterministic, including for
components restored from NBT. EntityMetadata converts values to their
protocol-encodable equivalents and rejects values the protocol cannot
encode at the call site, rather than failing every metadata packet
sent later.
Components implementing world.MetaSyncer now contribute to the metadata
sent for their entity. Component metadata is applied after the built-in
metadata, overwriting values and combining flag bits, so external
packages can sync custom entity state to clients without forking. Flag
bits are combined matching the existing entry's type, as some flag
keys, such as the player flags, hold a byte instead of an int64.
RegisterType creates a world.EntityType from a declarative Spec: name,
optional client-side network ID, bounding box and default components.
Specs and extra spawn components are validated eagerly, so unregistered
component types, duplicates and multiple behaviours fail at init or at
the spawn call site instead of at first use. Ent and Player run ticking
components each tick before their own logic; an entity closed by one of
its ticking components is not ticked further. The main behaviour is now
optional so entities may be composed purely of components, and a
Behaviour returned among a Spec's components becomes the entity's main
behaviour, bridging existing behaviours into spec-based types at
runtime.
Entity age was encoded in 20-second units but decoded in ticks, so an
entity's age collapsed by a factor of 400 on every save and load cycle.
Encode in ticks, matching the decoder and vanilla.
@cqdetdev
cqdetdev force-pushed the entity-overhaul/components branch from a45d807 to 3e38174 Compare July 14, 2026 03:01
@RestartFU

Copy link
Copy Markdown
Collaborator

can you like stop giving up on your PRs 🙏🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants