| name | hytale-persistent-data |
| description | Stores persistent data on players and entities using custom components with Codec serialization in Hytale plugins. Use when saving player data across sessions, creating custom player components, serializing complex data types to BSON, or persisting entity state. Triggers - persistent data, player data, save data, BuilderCodec, KeyedCodec, ComponentType, Ref, Store, putComponent, ensureAndGetComponent, BSON serialization, player state, custom component, session data. |
Hytale Persistent Data Storage
This skill provides comprehensive documentation for storing persistent data on players and entities using custom components with Codec serialization.
Current server builds use com.hypixel.hytale.component.* for Component, ComponentType, Ref, and Store, plus com.hypixel.hytale.server.core.universe.world.storage.EntityStore for the world storage type.
Quick Reference
| Task | Approach |
|---|
| Create persistent component | Class implementing Component<EntityStore> with BuilderCodec |
| Register component | getEntityStoreRegistry().registerComponent(Class, name, CODEC) in setup() |
| Add temporary component | store.addComponent(ref, componentType, instance) |
| Add persistent component | store.putComponent(ref, componentType, instance) |
| Get or create component | store.ensureAndGetComponent(ref, componentType) |
| Check if exists | store.getComponent(ref, componentType) != null |
| Serialize primitives | Codec.INTEGER, Codec.STRING, Codec.BOOLEAN, Codec.FLOAT, Codec.DOUBLE |
| Serialize collections | MapCodec, ListCodec, SetCodec |
Component Class Structure
Required Elements
Every persistent component must have:
- Fields - Data to persist
- BuilderCodec - Serialization definition with getters/setters for each field
- Default constructor - Initializes default values
- Copy constructor - For cloning
- clone() method - Returns new instance via copy constructor
Basic Template
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.codec.validation.Validators;
import com.hypixel.hytale.component.Component;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
public class CustomPlayerData implements Component<EntityStore> {
private int someInteger;
private String someString;
public static final BuilderCodec<CustomPlayerData> CODEC =
BuilderCodec.builder(CustomPlayerData.class, CustomPlayerData::new)
.append(new KeyedCodec<>("SomeInteger", Codec.INTEGER),
(data, value) -> data.someInteger = value,
data -> data.someInteger)
.addValidator(Validators.nonNull())
.add()
.append(new KeyedCodec<>("SomeString", Codec.STRING),
(data, value) -> data.someString = value,
data -> data.someString)
.add()
.build();
public CustomPlayerData() {
this.someInteger = 0;
this.someString = "";
}
public CustomPlayerData(CustomPlayerData clone) {
this.someInteger = clone.someInteger;
this.someString = clone.someString;
}
@Nonnull
@Override
public Component<EntityStore> clone() {
return new CustomPlayerData(this);
}
public int getSomeInteger() { return someInteger; }
public void setSomeInteger(int value) { this.someInteger = value; }
public String getSomeString() { return someString; }
public void setSomeString(String value) { this.someString = value; }
}
Codec System
KeyedCodec Requirements
IMPORTANT: The key in KeyedCodec must start with a Capital Letter, otherwise serialization may fail.
new KeyedCodec<>("SomeInteger", Codec.INTEGER)
new KeyedCodec<>("someInteger", Codec.INTEGER)
Primitive Codecs
| Type | Codec |
|---|
int | Codec.INTEGER |
long | Codec.LONG |
float | Codec.FLOAT |
double | Codec.DOUBLE |
boolean | Codec.BOOLEAN |
String | Codec.STRING |
Collection Codecs
new KeyedCodec<>("SomeMap",
new MapCodec<>(Codec.STRING, HashMap::new))
new KeyedCodec<>("SomeList",
new ListCodec<>(Codec.STRING, ArrayList::new))
new KeyedCodec<>("SomeSet",
new SetCodec<>(Codec.INTEGER, HashSet::new))
BuilderCodec Chain Pattern
public static final BuilderCodec<MyComponent> CODEC =
BuilderCodec.builder(MyComponent.class, MyComponent::new)
.append(new KeyedCodec<>("FieldOne", Codec.INTEGER),
(data, value) -> data.fieldOne = value,
data -> data.fieldOne)
.addValidator(Validators.nonNull())
.add()
.append(new KeyedCodec<>("FieldTwo", Codec.STRING),
(data, value) -> data.fieldTwo = value,
data -> data.fieldTwo)
.add()
.append(new KeyedCodec<>("FieldThree",
new MapCodec<>(Codec.STRING, HashMap::new, false)),
(data, value) -> data.fieldThree = value,
data -> data.fieldThree)
.add()
.build();
Component Registration
Register the component in your plugin's setup() method:
public class MyPlugin extends JavaPlugin {
private ComponentType<EntityStore, CustomPlayerData> customPlayerDataComponent;
public MyPlugin(@Nonnull JavaPluginInit init) {
super(init);
}
@Override
protected void setup() {
this.customPlayerDataComponent = this.getEntityStoreRegistry().registerComponent(
CustomPlayerData.class,
"CustomPlayerDataComponent",
CustomPlayerData.CODEC
);
}
public ComponentType<EntityStore, CustomPlayerData> getCustomPlayerDataComponent() {
return this.customPlayerDataComponent;
}
}
Using Components
addComponent vs putComponent
| Method | Persistence | Use Case |
|---|
addComponent | Temporary | Component removed when entity leaves world |
putComponent | Persistent | Component saved and loaded across sessions |
Adding/Updating Data
private void updatePlayerData(
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store
) {
ComponentType<EntityStore, CustomPlayerData> componentType =
MyPlugin.getInstance().getCustomPlayerDataComponent();
CustomPlayerData existing = store.getComponent(ref, componentType);
if (existing != null) {
existing.setSomeString("Updated Value");
existing.setSomeInteger(existing.getSomeInteger() + 1);
} else {
CustomPlayerData newData = new CustomPlayerData();
newData.setSomeString("Initial Value");
newData.setSomeInteger(1);
store.putComponent(ref, componentType, newData);
}
}
Retrieving Data (with auto-creation)
Use ensureAndGetComponent to get the component, creating it with default values if it doesn't exist:
public class MyCommand extends AbstractPlayerCommand {
public MyCommand() {
super("mycommand", "Description here");
}
@Override
protected void execute(
@Nonnull CommandContext commandContext,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
ComponentType<EntityStore, CustomPlayerData> componentType =
MyPlugin.getInstance().getCustomPlayerDataComponent();
CustomPlayerData data = store.ensureAndGetComponent(ref, componentType);
int currentValue = data.getSomeInteger();
String currentString = data.getSomeString();
data.setSomeInteger(currentValue + 1);
}
}
Complete Example
Component Class
package com.example.plugin.components;
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.codec.codecs.map.MapCodec;
import com.hypixel.hytale.codec.validation.Validators;
import com.hypixel.hytale.component.Component;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;
public class PlayerStats implements Component<EntityStore> {
private int kills;
private int deaths;
private long playTime;
private Map<String, Integer> achievements;
public static final BuilderCodec<PlayerStats> CODEC =
BuilderCodec.builder(PlayerStats.class, PlayerStats::new)
.append(new KeyedCodec<>("Kills", Codec.INTEGER),
(data, value) -> data.kills = value,
data -> data.kills)
.addValidator(Validators.nonNull())
.add()
.append(new KeyedCodec<>("Deaths", Codec.INTEGER),
(data, value) -> data.deaths = value,
data -> data.deaths)
.addValidator(Validators.nonNull())
.add()
.append(new KeyedCodec<>("PlayTime", Codec.LONG),
(data, value) -> data.playTime = value,
data -> data.playTime)
.add()
.append(new KeyedCodec<>("Achievements",
new MapCodec<>(Codec.INTEGER, HashMap::new)),
(data, value) -> data.achievements = value,
data -> data.achievements)
.add()
.build();
public PlayerStats() {
this.kills = 0;
this.deaths = 0;
this.playTime = 0L;
this.achievements = new HashMap<>();
}
public PlayerStats(PlayerStats clone) {
this.kills = clone.kills;
this.deaths = clone.deaths;
this.playTime = clone.playTime;
this.achievements = new HashMap<>(clone.achievements);
}
@Nonnull
@Override
public Component<EntityStore> clone() {
return new PlayerStats(this);
}
public int getKills() { return kills; }
public void setKills(int kills) { this.kills = kills; }
public void incrementKills() { this.kills++; }
public int getDeaths() { return deaths; }
public void setDeaths(int deaths) { this.deaths = deaths; }
public void incrementDeaths() { this.deaths++; }
public long getPlayTime() { return playTime; }
public void setPlayTime(long playTime) { this.playTime = playTime; }
public void addPlayTime(long time) { this.playTime += time; }
public Map<String, Integer> getAchievements() { return achievements; }
public void unlockAchievement(String id) {
achievements.put(id, achievements.getOrDefault(id, 0) + 1);
}
}
Plugin Registration
package com.example.plugin;
import com.example.plugin.components.PlayerStats;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
public class MyPlugin extends JavaPlugin {
private static MyPlugin instance;
private ComponentType<EntityStore, PlayerStats> playerStatsComponent;
public MyPlugin(@Nonnull JavaPluginInit init) {
super(init);
instance = this;
}
@Override
protected void setup() {
this.playerStatsComponent = this.getEntityStoreRegistry().registerComponent(
PlayerStats.class,
"PlayerStatsComponent",
PlayerStats.CODEC
);
}
public static MyPlugin getInstance() { return instance; }
public ComponentType<EntityStore, PlayerStats> getPlayerStatsComponent() {
return this.playerStatsComponent;
}
}
Best Practices
Naming Conventions
| Element | Convention | Example |
|---|
| Component class | PascalCase, descriptive | PlayerStats, QuestProgress |
| Component name (registration) | PascalCase + "Component" | "PlayerStatsComponent" |
| KeyedCodec keys | PascalCase, starts with capital | "Kills", "PlayTime" |
| Fields | camelCase | kills, playTime |
Performance Tips
- Avoid frequent getComponent calls - Cache the component reference when processing multiple operations
- Use ensureAndGetComponent wisely - It creates a new component if none exists, which may not always be desired
- Batch updates - Modify multiple fields before the component is saved
- Keep components focused - One component per logical data grouping
Common Pitfalls
| Issue | Solution |
|---|
| Data not persisting | Use putComponent instead of addComponent |
| Serialization fails | Ensure KeyedCodec keys start with capital letter |
| NullPointerException | Initialize collections in default constructor |
| Clone issues | Deep copy collections in copy constructor |
Related Resources