Skip to content

Storing Data

Hytale provides several persistence mechanisms depending on what you’re storing. Use the table below to pick the right approach.

What you’re storingMechanismSaved toAuto-saved?
Mod settingswithConfig()mods/{mod}/config.jsonOn load
Per-player dataEntityStore componentplayers/{uuid}.jsonEvery 10s
Per-entity dataEntityStore componentChunk region filesOn chunk save
Per-chunk / block dataChunkStore componentChunk region filesOn chunk save
Per-world / dynamic filesFile-basedmods/{mod}/ (your choice)Manual
Session-only stateTransient componentNot saved

All component-based approaches use BuilderCodec for serialization. If you’re unfamiliar with codecs, read that page first.

Register an EntityStore component with a string ID and codec in setup(). The component is automatically serialized into each player’s players/{uuid}.json file and restored when they rejoin.

public class PlayerStats implements Component<EntityStore> {
public static final BuilderCodec<PlayerStats> CODEC = BuilderCodec.builder(
PlayerStats.class, PlayerStats::new
)
.append(new KeyedCodec<>("Kills", Codec.INTEGER),
(s, v) -> s.kills = v, s -> s.kills)
.add()
.append(new KeyedCodec<>("Deaths", Codec.INTEGER),
(s, v) -> s.deaths = v, s -> s.deaths)
.add()
.build();
private int kills = 0;
private int deaths = 0;
public int getKills() { return kills; }
public int getDeaths() { return deaths; }
public void addKill() { kills++; }
public void addDeath() { deaths++; }
}
private ComponentType<EntityStore, PlayerStats> statsType;
@Override
protected void setup() {
// string ID + codec = persistent
this.statsType = getEntityStoreRegistry()
.registerComponent(PlayerStats.class, "MyMod_PlayerStats", PlayerStats.CODEC);
}

The string ID ("MyMod_PlayerStats") becomes the key in the serialized JSON. Use a prefixed name to avoid collisions with other mods.

// read (returns null if player doesn't have the component yet)
PlayerStats stats = store.getComponent(ref, statsType);
// read or create with defaults
PlayerStats stats = store.ensureAndGetComponent(ref, statsType);
// write
stats.addKill();

No save calls needed — the player save system serializes all registered components automatically every 10 seconds and on world shutdown.

This is the same pattern used by built-in plugins: BuilderToolsPlugin stores per-player selection preferences, MemoriesPlugin stores NPC interaction memories, and ParkourPlugin stores checkpoint progress — all as EntityStore components.

The same EntityStore component approach works for any entity, not just players. NPCs, dropped items, and custom entities are all persisted in chunk region files as part of the entity storage system.

// register in setup() — same as per-player
this.bountyType = getEntityStoreRegistry()
.registerComponent(BountyTag.class, "MyMod_Bounty", BountyTag.CODEC);
// attach to any entity
store.putComponent(npcRef, bountyType, new BountyTag(500));

When a chunk unloads, all entities in it are serialized — including your custom components. When the chunk loads again, the components are restored.

Register a ChunkStore component for data tied to specific blocks or chunk regions. This is persisted in the chunk’s region file alongside block data.

public class ProtectedBlock implements Component<ChunkStore> {
public static final BuilderCodec<ProtectedBlock> CODEC = BuilderCodec.builder(
ProtectedBlock.class, ProtectedBlock::new
)
.append(new KeyedCodec<>("Owner", Codec.UUID),
(b, v) -> b.owner = v, b -> b.owner)
.add()
.build();
private UUID owner;
public UUID getOwner() { return owner; }
}
// register in setup()
this.protectedType = getChunkStoreRegistry()
.registerComponent(ProtectedBlock.class, "MyMod_Protected", ProtectedBlock.CODEC);

Built-in plugins use this pattern: PortalsPlugin stores portal devices, FarmingPlugin stores tilled soil state, and BlockSpawnerPlugin stores spawner configurations — all as ChunkStore components.

Chunk components participate in the standard chunk saving pipeline — they’re saved when the chunk is marked dirty and serialized alongside block data.

What makes a component persistent is the string ID and codec passed to registerComponent(). The string ID becomes the key in the serialized JSON, and the codec handles encoding/decoding. Without them, the serializer has no way to include the component — so it’s skipped entirely.

To register a transient (session-only) component, use the two-parameter overload that takes a class and supplier instead:

// no string ID, no codec — serializer skips this component
this.sessionType = getEntityStoreRegistry()
.registerComponent(MySessionData.class, MySessionData::new);

Use this for runtime state like cooldowns, combat tags, or cached computations. The data is lost when the entity unloads or the server restarts.

For data that doesn’t naturally attach to an entity or chunk — world-level settings, leaderboards, arena configurations, economy balances — use the codec system directly to read and write JSON files. See Configuration — Dynamic Data Files for the full pattern with RawJsonReader and BsonUtil.

// read
MyData data = RawJsonReader.readSyncWithBak(path, MyData.CODEC, getLogger());
// write
BsonUtil.writeSync(path, MyData.CODEC, data, getLogger());

Unlike ECS components, file-based persistence requires you to manage the save/load lifecycle yourself — choose when to read, when to write, and where to store the files under getDataDirectory().