Skip to main content

saved-data

Building a DataWrapper​

DataWrapper has different initialization methods depending what type of object you want to attach data to.

  • GlobalDataWrapper<ExampleData> DATA = DataWrapper.global(ExampleData.class)
    • one instance of ExampleData is stored per server
  • LevelDataWrapper<ExampleData> DATA = DataWrapper.level(ExampleData.class)
    • one instance of ExampleData is stored per dimension
  • PlayerDataWrapper<ExampleData> DATA = DataWrapper.player(ExampleData.class)
    • one instance of ExampleData is stored per player

The return value of these can just be stored in a static variable for later use. The methods below are setup like a Builder so they can be chained.

Even above 1.16, the data class can NOT be a record (the version of Gson shipped with Minecraft does not support them) or have an final fields. Since you need to have a no parameter constructor that sets default values anyway, the record limitation doesn't really add more code. See Serialization for more information on what types of data are supported in your data class.

saved​

Call DataWrapper#saved to save the data to disk when the world is saved so it will persist across server restarts.

synced​

Call DataWrapper#synced to send the data to all clients on load and when you call DataWrapper#setDirty.

File Location​

Even if you aren't saving to disk, you must set a unique file location to be used for syncing.

  • You must call the DataWrapper#named method to set the name of the file to save data to.
  • You can put your config file in a subdirectory of its normal location by calling DataWrapper#dir
  • If you call DataWrapper#named with a resource location, instead of just a string, the path will be used as the file name and the namespace will be used for the subdirectory.
  • The file extension can be set by calling DataWrapper#ext (this does not actually change the data format!) but generally leaving it as the default (.json5) is fine.

The file will be at world/data/[dir]/[name].[ext].

MapDataWrappers​

Most data wrapper types (level, player) store a map of keys to values. These have some additional methods you may call to configure them.

splitFiles​

This method store each object in a separate file. Instead of a single file storing a map of (resource locations or uuids) to data, there will be many files whose names are (resource locations or uuids) containing one piece of data. This option does nothing if the data is not also saved.

The files will be at world/data/[dir]/[name]/[id].[ext]

lazy​

This method will have the same effect as splitFiles. Additionally, it will not load any of the data when the server starts. An entry's data will only be loaded from the file system when specifically requested by MapDataWrapper#get (see Reading Data Values).

This means even if synced, retrieving an entry on the client will return default values until it is retrieved at least once on the server side.

Data Loading​

As long as your DataWrapper is constructed in time (ie. it is in your mod initializer class or its parent class gets class loaded during your mod initializer), the values will automatically be loaded during ServerStartingEvent / ServerLifecycleEvents.SERVER_STARTING with no extra work from you. You can safely retrieve your config anytime after these events.

When a player joins a server, any synced data wrappers will be sent their client.

If you try to retrieve your data value before it gets loaded, no MinecraftServer instance is available so it won't know where to find the world folder. Default values will be returned and an error will be logged.

Reading Data Values​

Simply call DataWrapper#get to retrieve an object of your data class. The key parameter required will depend on the type of DataWrapper. The first type you call this for a given key, a new object will be created with your default values.

  • global: DATA.get()
  • level: DATA.get(level)
  • player: DATA.get(player)

Writing Data Values​

Simply retrieve the data object as above and change the values of its fields.

After changing a value you must call DataWrapper#setDirty. This will cause the data to save with the world (if saved) and immediately sync to all clients (if synced). You can pass a player/level, depending on the type of DataWrapper, to only resync that one value; which is more efficient that resending everything. setDirty can only be called on the logical server, the client cannot sync or save anything.

Removing Data Values​

  • Calling DataWrapper#clear will DELETE ALL DATA, including the files saved to disk. Be careful!
  • Calling MapDataWrapper#remove and passing in a specific key will delete the data of only that one entry (including its file on the disk).

In both cases the change will be synced to the client by sending the default value.

Simple Example​

This creates an item that will deal an extra heart of damage per Wither killed by the player (item registration omitted).

// ExampleModMain.java
public static final PlayerDataWrapper<ExampleData> DATA = DataWrapper.player(ExampleData.class).saved().named(MOD_ID);

// ExampleData.java
public class ExampleData {
public int withersKilled = 0;
}

// ForgeEventHandlers.java
@SubscribeEvent
public static void onDeath(LivingDeathEvent event){
if (event.getEntity().level.isClientSide()) return;
if (!(event.getEntity() instanceof WitherBoss)) return;
if (event.getSource().getEntity() instanceof Player player){
ExampleModMain.DATA.get(player).withersKilled++;
ExampleModMain.DATA.setDirty(player);
}
}

// ScalingAttackItem.java
@Override
public boolean hurtEnemy(ItemStack stack, LivingEntity target, LivingEntity attacker) {
if (!target.level.isClientSide() && attacker instanceof Player player) {
int damage = ExampleModMain.DATA.get(player).withersKilled * 2;
target.hurt(DamageSource.GENERIC, damage);
}
}

NbtDataWrapper​

WIP.