User Tools

Site Tools


tutorial:persistent_states

This is an old revision of the document!


Persistent State

Frequently we have information about the state of the world, or about the player, that we would like to keep across different play sessions, or server restarts. For instance, let's say your mod keeps tracks of the amount of times a player has crafted a furnace.

int furnacesCrafted = 0;

And it keeps track of how many times furnaces on the server have been crafted in total by all players.

int totalFurnacesCrafted = 0;

How do we save that information so that the next time the user logs in, or loads the world, we know how many furnaces they've crafted up to that point?

Server-Side or Client-Side

Before that, we have to decide who is keeping track of the data. The server or the client? In most cases, data should be saved server-side (this is especially true for mods that add new content and aren't client-side only like performance mods usually are). It's important to remember that server-side doesn't necessarily mean multiplayer. Singleplayer worlds talk to a logical server behind your back constantly so, if you were afraid of the word “server,” you really shouldn't be, and should feel safe saving your data on the “server”.

Using the furnaces crafted variable from before as an example, here is a thought experiment that shows you what you should be thinking about when deciding:

If we were on a server and another player wanted to know how many furnaces we've crafted, and currently that information is kept by clients instead of the server, this would be the process:

  1. Client sends a request to the server for another players information.
  2. Server receives request and sends its own request to the player we want the information from.
  3. Client replies with requested data.
  4. Server receives it, trusting the the right data was sent, and replies back to the original client.

Had the server been keeping track of the furnaces crafted on the players behalf instead, when it received the request for that information, it could've just replied directly.

There is also the problem that players could exploit the fact they are keeping track of the data and send the server the wrong information giving themselves more or less of something.

Side-Note:

  • Generally data the client is keeping track of should be transient (disappears when the player logs off), and only when they log in (to the server or world), do we pass them that information back. If they need it, that is; it could be the case that they have no use for the data themselves and is just something the server needs to know for your mod to work.

But in the case where the data you want to save is only ever used by the client, then go ahead and save it client-side.

Long Term Storage (Server)

So we've determined that the server will store the data. How do we actually save it so we have access to it again on server restarts or when the client asks us for it? By making use of the PersistentState class is how.

We'll start by making a new class ServerState which extends PersistentState. In it, we're going to keep track of how many furnaces we've seen be crafted in total.

public class ServerState extends PersistentState {
 
    int totalFurnacesCrafted = 0;
 
    @Override
    public NbtCompound writeNbt(NbtCompound nbt) {
        nbt.putInt("totalFurnacesCrafted", totalFurnacesCrafted);
        return nbt;
    }    
 
    public static ServerState createFromNbt(NbtCompound tag) {
        ServerState playerState = new ServerState();
        playerState.totalFurnacesCrafted = tag.getInt("totalFurnacesCrafted");
        return playerState;
    }
}

You'll notice the class implements the two methods writeNbt and createFromNbt. Anytime you want to add some new data to the ServerState like, in our example, if we want to add a boolean which tells us if we should keep track of furnaces being crafted.

boolean keepingTrack = true;

You have to make sure to update those two functions to reflect that, or your data will not be saved correctly.

public class ServerState extends PersistentState {
 
    int totalFurnacesCrafted = 0;
 
    boolean keepingTrack = true;
 
    @Override
    public NbtCompound writeNbt(NbtCompound nbt) {
        nbt.putInt("totalFurnacesCrafted", totalFurnacesCrafted);
        nbt.putBoolean("keepingTrack", keepingTrack);
        return nbt;
    }    
 
    public static ServerState createFromNbt(NbtCompound tag) {
        ServerState serverState = new ServerState();
        serverState.totalFurnacesCrafted = tag.getInt("totalFurnacesCrafted");
        serverState.keepingTrack = tag.getBoolean("keepingTrack");
        return serverState;
    }
}
  • Be mindful of the fact that NbtCompound, which is the object we need to fill with the data that should be saved to the disk, can't store arbitrary classes directly.

Now we need to register it to the PersistentStateManager which will be in charge of reading and writing this class to and from the disk. We'll do this in a static utility function inside our ServerState for ease of access from anywhere in our code.

public class ServerState extends PersistentState {
 
    // ... (What we'd written before)
 
    public static ServerState getServerState(MinecraftServer server) {
        // First we get the persistentStateManager for the OVERWORLD
        PersistentStateManager persistentStateManager = server
                .getWorld(World.OVERWORLD).getPersistentStateManager();
 
        // Calling this reads the file from the disk if it exists, or creates a new one and saves it to the disk
        // You need to use a unique string as the key. You should already have a MODID variable defined by you somewhere in your code. Use that.
        ServerState serverState = persistentStateManager.getOrCreate(
                ServerState::createFromNbt,
                ServerState::new,
                "YOUR_UNIQUE_MOD_ID (PLEASE CHANGE ME!!!)")); 
 
        return serverState;
    }
}

In order to signal that the server should save the state of the object at the next write cycle you must call markDirty() after you have finished modifying any variables you wish to change. Failing to do this will mean the server may not save the nbt data to disk so it cannot be guaranteed that when the nbt data is retrieved again after the server has restarted it will be the same.

ServerState serverState = ServerState.getServerState(server);
System.out.println("The server has seen this many furnaces crafted: " + serverState.totalFurnacesCrafted);
serverState.markDirty();

You'll notice we need to pass the getServerState function a MinecraftServer instance. Where do we get that? You could register for events like the server startup and save the variable at that time, but the easiest and most common way you'll get the server instance is from the player. For instance let's say we wanted to pass every player that joined that information (how many furnaces have been crafted total).

public class OurMod implements ModInitializer {
    public static final String MODID = "OUR_MOD_ID";
 
    @Override
    public void onInitialize() {
        ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
            // You can see we use the function getServer() that's on the player.
            ServerState serverState = ServerState.getServerState(handler.player.world.getServer());
 
            // Sending the packet to the player (look at the networking page for more information)
            PacketByteBuf data = PacketByteBufs.create();
            data.writeInt(serverState.totalFurnacesCrafted);
            ServerPlayNetworking.send(handler.player, NetworkingMessages.CRAFTED_FURNACES, data);
        });
    }
}

What About Persistent Data That Is Associated With a Player?

Let's say our example mod we've been making so far also wants to keep track of how many furnaces have been crafted by each user individually. That type of information should be associated to the player. How do we do that?

First let's create the class for our player data. (Remember we're saving our data on the server so the client has no idea about this information)

public class PlayerState {
    int furnacesCrafted = 0;
}

Now we need to add a hashmap to our server state where the key is the UUID of the player, and the value is a PlayerState instance. (The whole class will be re-printed piece-meal so you can just copy and paste as you go if you want).

public class ServerState extends PersistentState {
    int totalFurnacesCrafted = 0;
 
    boolean keepingTrack = true;
 
    public HashMap<UUID, PlayerState> players = new HashMap<>();

Every player has a unique UUID we can use to identify them specifically.

Next is to update the createFromNbt and writeNbt which gets a bit more involved but nothing too crazy. (If you add any new data to the PlayerState class you want saved, you'll have to make sure you update these functions).

The writeNbt is where the program should take the state it wants to save and stuff it into an NbtCompound to be written out to disk.

    @Override
    public NbtCompound writeNbt(NbtCompound nbt) {
        // Putting the 'players' hashmap, into the 'nbt' which will be saved.
        NbtCompound playersNbtCompound = new NbtCompound();
        players.forEach((UUID, playerSate) -> {
            NbtCompound playerStateNbt = new NbtCompound();
 
            // ANYTIME YOU PUT NEW DATA IN THE PlayerState CLASS YOU NEED TO REFLECT THAT HERE!!!
            playerStateNbt.putInt("furnacesCrafted", playerSate.furnacesCrafted);
 
            playersNbtCompound.put(String.valueOf(UUID), playerStateNbt);
        });
        nbt.put("players", playersNbtCompound);
 
        // Putting the 'ServerState' data on the 'nbt' so it'll be saved as well.
        nbt.putInt("totalFurnacesCrafted", totalFurnacesCrafted);
        nbt.putBoolean("keepingTrack", keepingTrack);
 
        return nbt;
    }

The createFromNbt is where the program passes you an NbtCompound and you need to convert back into your state.

    public static ServerState createFromNbt(NbtCompound tag) {
        ServerState serverState = new ServerState();
 
        // Here we are basically reversing what we did in ''writeNbt'' and putting the data inside the tag back to our hashmap
        NbtCompound playersTag = tag.getCompound("players");
        playersTag.getKeys().forEach(key -> {
            PlayerState playerState = new PlayerState();
 
            playerState.furnacesCrafted = playersTag.getCompound(key).getInt("furnacesCrafted");
 
            UUID uuid = UUID.fromString(key);
            serverState.players.put(uuid, playerState);
        });
 
        serverState.totalFurnacesCrafted = tag.getInt("totalFurnacesCrafted");
        serverState.keepingTrack = tag.getBoolean("keepingTrack");
 
        return serverState;
    }

Finally we add a utility function which takes a player, looks through the server state players hashmap with it's UUID, and either creates a new PlayerState and adds itself to the hashmap, or just returns the one it already found.

    public static ServerState getServerState(MinecraftServer server) {
        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager();
 
        ServerState serverState = persistentStateManager.getOrCreate(
                ServerState::createFromNbt,
                ServerState::new,
                "YOUR_UNIQUE_MOD_ID (PLEASE CHANGE ME!!!)");
 
        serverState.markDirty(); // YOU MUST DO THIS!!!! Or data wont be saved correctly.
 
        return serverState;
    }
 
    public static PlayerState getPlayerState(LivingEntity player) {
        ServerState serverState = getServerState(player.world.getServer());
 
        // Either get the player by the uuid, or we don't have data for him yet, make a new player state
        PlayerState playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerState());
 
        return playerState;
    }
}

How do I use this? Using our earlier example where we passed each player who joined the server the amount of furnaces that have been crafted on the server in total, let's make it so they are also passed how many they themselves have crafted.

public class OurMod implements ModInitializer {
    public static final String MODID = "OUR_MOD_ID";
 
    @Override
    public void onInitialize() {
        ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
            ServerState serverState = ServerState.getServerState(handler.player.world.getServer());
            PlayerState playerState = ServerState.getPlayerState(handler.player);
 
            // Sending the packet to the player (look at the networking page for more information)
            PacketByteBuf data = PacketByteBufs.create();
            data.writeInt(serverState.totalFurnacesCrafted);
            data.writeInt(playerState.furnacesCrafted);
            ServerPlayNetworking.send(handler.player, NetworkingMessages.CRAFTED_FURNACES, data);
        });
    }
}

Long Term Storage (Client)

If you're sure the data should live on the client, but still need it to persist across sessions (load/unload world) then we can use a utility function very similar to the one we created for our server. The only difference between being that we created a new type ClientState to return. Look at the PlayerState we wrote earlier to see how to write ClientState class.

  • Note because of the use MinecraftClient.getInstance(), this function should only ever be called by the client. Never the server.
public static ClientState getPlayerState() {
    ClientPlayerEntity player = MinecraftClient.getInstance().player;
 
    PersistentStateManager persistentStateManager = player.world.getServer()
        .getWorld(World.OVERWORLD).getPersistentStateManager();
 
    ClientState clientState = persistentStateManager.getOrCreate(
        ClientState::createFromNbt,
        ClientState::new,
        player.getUuidAsString()); // We use the UUID of the player as the key so they can retreive their data later
 
    return clientState;
}

More Involved Player State

And just for good measure, let's see an example of how our ServerState class should look if our PlayerState has more than primitives, like lists, and even its own hashmap. How does that look?

Let's say this is our PlayerState:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
 
public class PlayerState {
    int furnacesCrafted = 0;
 
    public HashMap<Integer, Integer> fatigue = new HashMap<>();
 
    public List<Integer> oldCravings = new ArrayList<>();
}

This is how the ServerState should look.

import net.minecraft.entity.LivingEntity;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.PersistentState;
import net.minecraft.world.PersistentStateManager;
import net.minecraft.world.World;
 
import java.util.HashMap;
import java.util.UUID;
 
public class ServerState extends PersistentState {
    int totalFurnacesCrafted = 0;
 
    boolean keepingTrack = true;
 
    public HashMap<UUID, PlayerState> players = new HashMap<>();
 
    public static ServerState createFromNbt(NbtCompound tag) {
        ServerState serverState = new ServerState();
 
        NbtCompound playersTag = tag.getCompound("players");
        playersTag.getKeys().forEach(key -> {
            PlayerState playerState = new PlayerState();
 
            playerState.furnacesCrafted = playersTag.getCompound(key).getInt("furnacesCrafted");
 
            NbtCompound fatigueCompound = playersTag.getCompound(key).getCompound("fatigue");
            fatigueCompound.getKeys().forEach(s -> {
                Integer foodID = Integer.valueOf(s);
                int fatigueAmount = fatigueCompound.getInt(s);
                playerState.fatigue.put(foodID, fatigueAmount);
            });
 
            for (int oldCravings : playersTag.getCompound(key).getIntArray("oldCravings")) {
                playerState.oldCravings.add(oldCravings);
            }
 
            UUID uuid = UUID.fromString(key);
            serverState.players.put(uuid, playerState);
        });
 
        serverState.totalFurnacesCrafted = tag.getInt("totalFurnacesCrafted");
        serverState.keepingTrack = tag.getBoolean("keepingTrack");
 
        return serverState;
    }
 
    @Override
    public NbtCompound writeNbt(NbtCompound nbt) {
        NbtCompound playersNbt = new NbtCompound();
 
        players.forEach((UUID, playerData) -> {
            NbtCompound playerDataAsNbt = new NbtCompound();
 
            playerDataAsNbt.putInt("furnacesCrafted", playerData.furnacesCrafted);
 
            playerDataAsNbt.putIntArray("oldCravings", playerData.oldCravings);
 
            NbtCompound fatigueTag = new NbtCompound();
            playerData.fatigue.forEach((foodID, fatigueAmount) -> fatigueTag.putInt(String.valueOf(foodID), fatigueAmount));
            playerDataAsNbt.put("fatigue", fatigueTag);
 
            playersNbt.put(String.valueOf(UUID), playerDataAsNbt);
        });
        nbt.put("players", playersNbt);
 
        nbt.putInt("totalFurnacesCrafted", totalFurnacesCrafted);
        nbt.putBoolean("keepingTrack", keepingTrack);
 
        return nbt;
    }
 
    public static ServerState getServerState(MinecraftServer server) {
        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager();
 
        ServerState serverState = persistentStateManager.getOrCreate(
                ServerState::createFromNbt,
                ServerState::new,
                "YOUR_UNIQUE_MOD_ID (PLEASE CHANGE ME!!!)");
 
        return serverState;
    }
 
    public static PlayerState getPlayerState(LivingEntity player) {
        ServerState serverState = getServerState(player.world.getServer());
 
        // Either get the player by the uuid, or we don't have data for him yet, make a new player state
        PlayerState playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerState());
 
        return playerState;
    }
}
tutorial/persistent_states.1681126863.txt.gz · Last modified: 2023/04/10 11:41 by lunathelemon