Saved Data is Minecraft's built-in solution to save data across sessions.
The data is saved to disk and reloaded when the game is closed and opened again. This data is usually scoped (e.g. the level). Data is written to the disk as NBT and Codecs are used to serialize/deserialize this data.
Let's look at a simple scenario where we need to save the number of blocks broken by the player. We can keep this count on the logical server.
We can use the PlayerBlockBreakEvents.AFTER event with a simple static integer field to store this value and post it as a chat message.
java
private static int blocksBroken = 0; // keeps track of the number of blocks broken
PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, blockEntity) -> {
blocksBroken++; // increment the counter each time a block is broken
player.displayClientMessage(Component.literal("Blocks broken: " + blocksBroken), false);
});1
2
3
4
5
6
2
3
4
5
6
Now, when you break a block, you'll see a message with the count.

If you restart Minecraft, load the world and start breaking blocks, you'll notice the count has reset. This is where we need Saved Data. We can then store this count, so that the next time you load the world, we can get the saved count and start incrementing it from that point.
Saving Data
SavedData is the main class responsible for managing the saving/loading of data. As it is an abstract class, you're expected to provide an implementation.
Setting Up a Data Class
Let's name our data class SavedBlockData and have it extend SavedData.
This class will contain a field to keep track of the number of blocks broken as well as a method to get and a method to increment this number.
java
public class SavedBlockData extends SavedData {
private int blocksBroken = 0;
public SavedBlockData() {
}
public int getBlocksBroken() {
return blocksBroken;
}
public void incrementBlocksBroken() {
blocksBroken++;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
For serializing and deserializing this data, we need to define a Codec. We can compose a Codec using various primitive Codecs provided by Minecraft.
You'll need a constructor with an int argument to initialize the class.
java
public SavedBlockData(int count) {
blocksBroken = count;
}1
2
3
2
3
Then we can build a Codec.
java
private static final Codec<SavedBlockData> CODEC = Codec.INT.xmap(
SavedBlockData::new, // Create a new 'SavedBlockData' from the stored number.
SavedBlockData::getBlocksBroken // Return the number from the 'SavedBlockData' to be saved/
);1
2
3
4
2
3
4
Finally, we're required to have a SavedDataType that describes our saved data. The first argument corresponds to the name of the file that will be created in the data directory of the world.
java
private static final SavedDataType<SavedBlockData> TYPE = new SavedDataType<>(
"saved_block_data", // The unique name for this saved data.
SavedBlockData::new, // If there's no 'SavedBlockData', yet create one and refresh fields.
CODEC, // The codec used for serialization/deserialization.
null // A data fixer, which is not needed here.
);1
2
3
4
5
6
2
3
4
5
6
Accessing Saved Data
As mentioned earlier, saved data can be associated with a scope like the current level. In this case, our data will be a part of the level data. We can get the DimensionDataStorage of the level to add and modify our data.
We'll put this logic into a utility method.
java
public static SavedBlockData getSavedBlockData(MinecraftServer server) {
// This could be either the overworld or another dimension.
ServerLevel level = server.getLevel(ServerLevel.OVERWORLD);
if (level == null) {
return new SavedBlockData(); // Return a new instance if the level is null.
}
// The first time the following 'computeIfAbsent' function is called, it creates a new 'SavedBlockData'
// instance and stores it inside the 'DimensionDataStorage'.
// Subsequent calls to 'computeIfAbsent' returns the saved 'SavedBlockData' NBT on disk to the Codec in our type,
// using the Codec to decode the NBT into our saved data.
SavedBlockData savedData = level.getDataStorage().computeIfAbsent(TYPE);
// If saved data is not marked dirty, nothing will be saved when Minecraft closes.
// Technically it's 'cleaner' if you only mark saved data as dirty when there was actually a change,
// but the vast majority of mod developers are just going to be confused when their data isn't being saved,
// and so it's best just to 'setDirty' for them.
savedData.setDirty();
return savedData;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Using Saved Data
Now that we have everything set up, let's save some data.
We can reuse the first scenario and instead of incrementing the field, we can call our incrementBlocksBroken from our SavedBlockData.
java
PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, blockEntity) -> {
MinecraftServer server = world.getServer();
if (server == null) {
return;
}
// Retrieve the saved block data from the server.
SavedBlockData savedData = SavedBlockData.getSavedBlockData(server);
savedData.incrementBlocksBroken(); // Increment the counter each time a block is broken.
player.displayClientMessage(Component.literal("Blocks broken: " + savedData.getBlocksBroken()), false);
});1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
This should increment the value and save it to the disk.
If you restart Minecraft, load the world and break a block, you'll see that the previously saved count is now incremented.
If you go into the data directory of the world, you'll see a .dat file with the name of saved_block_data.dat. Opening this file in a NBT reader will show how our data is saved within.

