Storing Data
Hytale provides several persistence mechanisms depending on what you’re storing. Use the table below to pick the right approach.
Which Approach?
Section titled “Which Approach?”| What you’re storing | Mechanism | Saved to | Auto-saved? |
|---|---|---|---|
| Mod settings | withConfig() | mods/{mod}/config.json | On load |
| Per-player data | EntityStore component | players/{uuid}.json | Every 10s |
| Per-entity data | EntityStore component | Chunk region files | On chunk save |
| Per-chunk / block data | ChunkStore component | Chunk region files | On chunk save |
| Per-world / dynamic files | File-based | mods/{mod}/ (your choice) | Manual |
| Session-only state | Transient component | Not saved | — |
All component-based approaches use BuilderCodec for serialization. If you’re unfamiliar with codecs, read that page first.
Per-Player Data
Section titled “Per-Player Data”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.
1. Define the component
Section titled “1. Define the component”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++; }}2. Register in setup()
Section titled “2. Register in setup()”private ComponentType<EntityStore, PlayerStats> statsType;
@Overrideprotected 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.
3. Access at runtime
Section titled “3. Access at runtime”// read (returns null if player doesn't have the component yet)PlayerStats stats = store.getComponent(ref, statsType);
// read or create with defaultsPlayerStats stats = store.ensureAndGetComponent(ref, statsType);
// writestats.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.
Per-Entity Data
Section titled “Per-Entity Data”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-playerthis.bountyType = getEntityStoreRegistry() .registerComponent(BountyTag.class, "MyMod_Bounty", BountyTag.CODEC);
// attach to any entitystore.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.
Per-Chunk Data
Section titled “Per-Chunk Data”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.
Transient Components
Section titled “Transient Components”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 componentthis.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.
File-Based Persistence
Section titled “File-Based Persistence”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.
// readMyData data = RawJsonReader.readSyncWithBak(path, MyData.CODEC, getLogger());
// writeBsonUtil.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().