User Tools

Site Tools


tutorial:persistent_states

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
tutorial:persistent_states [2022/07/12 10:29] – created mineblock11tutorial:persistent_states [2024/04/25 14:06] (current) mayaqq
Line 1: Line 1:
-====== Persistent States ======+====== Persistent State ======
  
-[TDBmineblock11 is currently writing this.]+Frequently in our mods we have information about the player, or about the state of the world that we would like to survive across player deaths and server restarts. For instance, let's say our mod keeps track of how many dirt blocks a player has broken: 
 + 
 +<code java> 
 +int dirtBlocksBroken = 0; 
 +</code> 
 + 
 +And that the mod also keeps track of how many dirt blocks have been broken, on the server, in total: 
 + 
 +<code java> 
 +int totalDirtBlocksBroken = 0; 
 +</code> 
 + 
 +How do we get fabric to save that information, or any other information that we want to keep (booleans, lists, strings) so that the next time a player loads the world, or logs in, we get the previous sessions data? 
 + 
 +====== Simple Message ====== 
 + 
 +First, since the data will be saved on the 'server', (NOTE: that there is always a 'server' running even when you play offline, so don't be scared about that word), let's send a simple packet to the player when the server detects the player breaks a dirt block, and print it to the chat. 
 + 
 +Modify your class which ''**implements ModInitializer**'' as follows: 
 + 
 +<code java> 
 +import net.fabricmc.api.ModInitializer; 
 +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; 
 +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 
 +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 
 +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 
 +import net.minecraft.block.Blocks; 
 +import net.minecraft.network.PacketByteBuf; 
 +import net.minecraft.server.MinecraftServer; 
 +import net.minecraft.server.network.ServerPlayerEntity; 
 +import net.minecraft.util.Identifier; 
 + 
 +public class ExampleMod implements ModInitializer { 
 + 
 +    public static final String MOD_ID = "your_unique_mod_id_change_me_please"; 
 + 
 +    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken"); 
 + 
 +    private Integer totalDirtBlocksBroken = 0; 
 + 
 +    @Override 
 +    public void onInitialize() { 
 +        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> { 
 +            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { 
 +                // Increment the amount of dirt blocks that have been broken 
 +                totalDirtBlocksBroken += 1; 
 + 
 +                // Send a packet to the client 
 +                MinecraftServer server = world.getServer(); 
 + 
 +                PacketByteBuf data = PacketByteBufs.create(); 
 +                data.writeInt(totalDirtBlocksBroken); 
 + 
 +                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); 
 +                server.execute(() -> { 
 +                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data); 
 +                }); 
 +            } 
 +        }); 
 +    } 
 +
 +</code> 
 +  
 +  * The ''**MOD_ID**'' should be unique. Usually people use their mods name. Make sure to change it, but be mindful of the fact that you can only use lowercase letters and underscores or you'll get errors.  
 +    * To send a packet we use: ''**ServerPlayNetworking.send**''. It takes three arguments; The ''**ServerPlayerEntity**''. An ''**Identifer**'' which is needed when sending a packet between server and client or client and server, and a ''**PacketByteBuf**'' which we are able to fill with arbritary data (that is: bools, ints, arrays, strings, and more). The ''**PacketByteBuf**'' is where we stuff the data we want to send across the line. In this case we send the total amount of dirt blocks that the server has seen broken. 
 + 
 +Next modify your class which ''**implements ClientModInitializer**'' as follows: 
 + 
 +<code java> 
 +import net.fabricmc.api.ClientModInitializer; 
 +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 
 +import net.minecraft.text.Text;  
 + 
 +public class ExampleModClient implements ClientModInitializer { 
 + 
 +    @Override 
 +    public void onInitializeClient() { 
 +        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (client, handler, buf, responseSender) -> { 
 +            int totalDirtBlocksBroken = buf.readInt(); 
 +            client.execute(() -> { 
 +                client.player.sendMessage(Text.literal("Total dirt blocks broken: " + totalDirtBlocksBroken)); 
 +            }); 
 +        }); 
 +    } 
 +
 +</code> 
 + 
 +  * You can see the ''**Identifier**'' we use is the exact same instance as we used server side and then ''**buf.readInt()**'' is used to extract the data that was packed into the ''**PackedByteBuf**''. You have to read out the data the same way you stuffed it in. 
 + 
 +If you run the mod now, and open a world up, you should see a message every time you break a dirt/grass block. The number should increase by one every time. You may even notice that if you close the world and open it again, that the number keeps increasing from where you left off, but this is misleading. If you fully close out the Minecraft client and run it again, if you open that world and break a block, you should see that it starts from zero again. But why? Why doesn't it continue from where it left off? 
 + 
 +The reason is that the number only ever lives in memory. It isn't saved anywhere. If it where, for instance to the hard-drive, then we could load it at start up and set the initial value to the value we ended up saving the last session, and therefore continue from where we left off. But we need to **actually** do the work of doing that: saving the value when minecraft closes and then loading it when minecraft starts up. There are many ways this could be done, but the built-in Minecraft way is to: implement a class which extends the ''**PersistentState**'' class. 
 + 
 +====== Persistent State ====== 
 + 
 +First make a new file ''**StateSaverAndLoader.java**'' in the same folder as your class which ''**implements ModInitializer**''
 + 
 +<code java> 
 +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; 
 + 
 +public class StateSaverAndLoader extends PersistentState { 
 + 
 +    public Integer totalDirtBlocksBroken = 0; 
 + 
 +    @Override 
 +    public NbtCompound writeNbt(NbtCompound nbt) { 
 +        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken); 
 +        return nbt; 
 +    } 
 +
 +</code> 
 + 
 +Note: ''**writeNbt**'' must be implemented when extending ''**PersistentState**''. In the function, you get passed in an ''**NbtCompound**'' which we are supposed to pack with the data we want saved to disk. In our case, we moved the ''**public Integer totalDirtBlocksBroken**'' variable we had created earlier into this file. 
 + 
 +  * ''**NbtCompound**'' doesn't just store ''**Integers**''. It has functions for strings, arrays, bools, floats, importantly other ''**NbtCompound**'''s as you'll see soon enough, and even arbitrary bytes. That means, if you want to store some ''**SuperCustomClass**'', what you should do is create a ''**new NbtCompound**'' and pack that new ''**NbtCompound**'' with the fields of your ''**SuperCustomClass**'' and then store it in the main ''**NbtCompound**'' you get passed in. (We're about to do just that!) 
 + 
 +Next add the following function to that same file: 
 + 
 +<code java> 
 +public class StateSaverAndLoader extends PersistentState { 
 + 
 +    // ... (Previously written code) 
 + 
 +    public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { 
 +        StateSaverAndLoader state = new StateSaverAndLoader(); 
 +        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken"); 
 +        return state; 
 +    } 
 +
 +</code> 
 + 
 +This function does the opposite of ''**writeNbt**''. It takes in an ''**NbtCompound**'' (the same one we wrote in ''**writeNbt**'') and a ''**RegistryWrapper.WrapperLookup**'', creates a brand ''**new StateSaverAndLoader**'' and stuffs it with the data inside the ''**NbtCompound**''
 + 
 +  * Note: how we pull out the int we stored earlier with ''**getInt**'' and how the string we pass in is the same one we used in ''**writeNbt**''
 + 
 +Now we just need to add one more utility function which hooks everything up together. This function will take a ''**MinecraftServer**'' and from it, get the ''**PersistentStateManager**''. ''**PersistentStateManager**'' has a function ''**getOrCreate**'' which will use our ''**MOD_ID**'' as a key to see if it has an instance of our ''**StateSaverAndLoader**'' or if it needs to create one. If it needs to create one, it'll call the function we just wrote ''**createFromNbt**'' passing in the previously saved-to-disk ''**NbtCompound**'' and a ''**RegistryWrapper.WrapperLookup**''. Ultimately the function returns the ''**StateSaverAndLoader**'' for the given ''**MinecraftServer**''
 + 
 +<code java> 
 +public class StateSaverAndLoader extends PersistentState { 
 + 
 +    // ... (Previously written code) 
 + 
 +    private static Type<StateSaverAndLoader> type = new Type<>
 +            StateSaverAndLoader::new, // If there's no 'StateSaverAndLoader' yet create one 
 +            StateSaverAndLoader::createFromNbt, // If there is a 'StateSaverAndLoader' NBT, parse it with 'createFromNbt' 
 +            null // Supposed to be an 'DataFixTypes' enum, but we can just pass null 
 +    ); 
 + 
 +    public static StateSaverAndLoader getServerState(MinecraftServer server) { 
 +        // (Note: arbitrary choice to use 'World.OVERWORLD' instead of 'World.END' or 'World.NETHER' Any work) 
 +        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); 
 + 
 +        // The first time the following 'getOrCreate' function is called, it creates a brand new 'StateSaverAndLoader' and 
 +        // stores it inside the 'PersistentStateManager'. The subsequent calls to 'getOrCreate' pass in the saved 
 +        // 'StateSaverAndLoader' NBT on disk to our function 'StateSaverAndLoader::createFromNbt'
 +        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID); 
 + 
 +        // If state is not marked dirty, when Minecraft closes, 'writeNbt' won't be called and therefore nothing will be saved. 
 +        // Technically it's 'cleaner' if you only mark state as dirty when there was actually a change, but the vast majority 
 +        // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to 'markDirty' for them. 
 +        // Besides, it's literally just setting a bool to true, and the only time there's a 'cost' is when the file is written to disk when 
 +        // there were no actual change to any of the mods state (INCREDIBLY RARE). 
 +        state.markDirty(); 
 + 
 +        return state; 
 +    } 
 +
 +</code> 
 + 
 +Your ''**StateSaverAndLoader**'' file should look as follows: 
 + 
 +<code java> 
 +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; 
 + 
 +public class StateSaverAndLoader extends PersistentState { 
 + 
 +    public Integer totalDirtBlocksBroken = 0; 
 + 
 +    @Override 
 +    public NbtCompound writeNbt(NbtCompound nbt) { 
 +        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken); 
 +        return nbt; 
 +    } 
 + 
 +    public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { 
 +        StateSaverAndLoader state = new StateSaverAndLoader(); 
 +        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken"); 
 +        return state; 
 +    } 
 + 
 +    private static Type<StateSaverAndLoader> type = new Type<>
 +            StateSaverAndLoader::new, // If there's no 'StateSaverAndLoader' yet create one 
 +            StateSaverAndLoader::createFromNbt, // If there is a 'StateSaverAndLoader' NBT, parse it with 'createFromNbt' 
 +            null // Supposed to be an 'DataFixTypes' enum, but we can just pass null 
 +    ); 
 + 
 +    public static StateSaverAndLoader getServerState(MinecraftServer server) { 
 +        // (Note: arbitrary choice to use 'World.OVERWORLD' instead of 'World.END' or 'World.NETHER' Any work) 
 +        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); 
 + 
 +        // The first time the following 'getOrCreate' function is called, it creates a brand new 'StateSaverAndLoader' and 
 +        // stores it inside the 'PersistentStateManager'. The subsequent calls to 'getOrCreate' pass in the saved 
 +        // 'StateSaverAndLoader' NBT on disk to our function 'StateSaverAndLoader::createFromNbt'
 +        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID); 
 + 
 +        // If state is not marked dirty, when Minecraft closes, 'writeNbt' won't be called and therefore nothing will be saved. 
 +        // Technically it's 'cleaner' if you only mark state as dirty when there was actually a change, but the vast majority 
 +        // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to 'markDirty' for them. 
 +        // Besides, it's literally just setting a bool to true, and the only time there's a 'cost' is when the file is written to disk when 
 +        // there were no actual change to any of the mods state (INCREDIBLY RARE). 
 +        state.markDirty(); 
 + 
 +        return state; 
 +    } 
 +
 +</code> 
 + 
 +You'll also have to update your class which ''**implements ModInitializer**'' so that it's as follows: 
 + 
 +<code java> 
 +import net.fabricmc.api.ModInitializer; 
 +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; 
 +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 
 +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 
 +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 
 +import net.minecraft.block.Blocks; 
 +import net.minecraft.network.PacketByteBuf; 
 +import net.minecraft.server.MinecraftServer; 
 +import net.minecraft.server.network.ServerPlayerEntity; 
 +import net.minecraft.util.Identifier; 
 + 
 +public class ExampleMod implements ModInitializer { 
 + 
 +    public static final String MOD_ID = "your_unique_mod_id_change_me_please"; 
 + 
 +    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken"); 
 + 
 +    @Override 
 +    public void onInitialize() { 
 +        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> { 
 +            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { 
 +                StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer()); 
 +                // Increment the amount of dirt blocks that have been broken 
 +                serverState.totalDirtBlocksBroken += 1; 
 + 
 +                // Send a packet to the client 
 +                MinecraftServer server = world.getServer(); 
 + 
 +                PacketByteBuf data = PacketByteBufs.create(); 
 +                data.writeInt(serverState.totalDirtBlocksBroken); 
 + 
 +                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); 
 +                server.execute(() -> { 
 +                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data); 
 +                }); 
 +            } 
 +        }); 
 +    } 
 +
 +</code> 
 + 
 +If you run your game now, you should see the counter going up, but now, if you fully close Minecraft, and open it again, you should see the number keeps increasing from where it left off. 
 + 
 +What you might or might not expect is that ''**totalDirtBlocksBroken**'' is not specific to a player, that is, if you made a server with this mod, and a few people were running around breaking dirt blocks, all of them would increment that same number. This is fine for certain types of data we'd like to store for our mods, but most of the time, we want to store player-specific data. For instance, as we mentioned at the start of this article, what if we would like to store how many dirt blocks any //specific// player has broken? 
 +====== Player Specific Persistent State ====== 
 + 
 +We can store player-specific data by extending what we already wrote.  
 + 
 +First write a new class ''**PlayerData.java**'' (again placing it in the same folder as our class which ''**implements ModInitializer**''). 
 + 
 +  * Extremely important note: Since we'll be creating a HashMap which seemingly stores ''**PlayerData**'''s, it'll be tempting when you want to know some ''**PlayerData**'' client-side to use that hashmap, **but**, you won't be able to, **because** this data only exists on the server. If you want the client to see some or all the player's data. You'll have to send it through a packet. (We'll create an ''**INITIAL_SYNC**'' packet later which we will use to demonstrate this). 
 + 
 +<code java> 
 +class PlayerData { 
 +    public int dirtBlocksBroken = 0; 
 +
 +</code> 
 + 
 +To simplify, we're just continuing with our simple example, but you could put any fields you'd like in the ''**PlayerData**'' (and it's expected that you will). 
 + 
 +Next, we'll modify the top of our ''**StateSaverAndLoader.java**'' class as follows: 
 + 
 +<code java> 
 +// ... (Previous imports) 
 +import java.util.HashMap; 
 +import java.util.UUID; 
 + 
 +public class StateSaverAndLoader extends PersistentState { 
 + 
 +    public Integer totalDirtBlocksBroken = 0; 
 +     
 +    public HashMap<UUID, PlayerData> players = new HashMap<>(); 
 + 
 +    // ... (Rest of the code) 
 + 
 +
 +</code> 
 + 
 +Note: We create a ''**HashMap**'' of ''**UUID**'''s to ''**PlayereData**'''s. If you don't know what a hashmap does: you give them a 'key', in our case a ''**UUID**'' and they give you back something, in our case ''**PlayerData**''. The reason we use ''**UUID**'''s is because every player that connects to our 'server' has a unique ''**UUID**'' that //only// they are associated with. This lets us differentiate between different players and lets us 'pull' the right data for them. (Or create it if it doesn't exist yet). 
 + 
 +Let's add a utility function to ''**StateSaverAndLoader**'' which will take a ''**LivingEntity**'' and return the associated ''**PlayerData**'' in our '''**HashMap**'''
 + 
 +<code java> 
 +public class StateSaverAndLoader extends PersistentState { 
 + 
 +    // ... (Previously written code) 
 + 
 +    public static PlayerData getPlayerState(LivingEntity player) { 
 +        StateSaverAndLoader serverState = getServerState(player.getWorld().getServer()); 
 + 
 +        // Either get the player by the uuid, or we don't have data for him yet, make a new player state 
 +        PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData()); 
 + 
 +        return playerState; 
 +    } 
 +
 +</code> 
 + 
 + 
 +  * If our ''**StateSaverAndLoader**'' has never seen the ''**UUID**'' before then it makes sense that our ''**HashMap**'' won't return anything. That's why we use the convenience function ''**computeIfAbsent**'' so that in the case where //that// ''**UUID**'' doesn't exist in the hashmap yet, it will automatically create a new ''**PlayerData**'' and puts it in our hashmap, using //that// players specific ''**UUID**'' as the key so that it can be retrieved later. 
 + 
 +Now update the class which ''**implements ModInitializer**'' as follows: 
 + 
 +<code java> 
 +import net.fabricmc.api.ModInitializer; 
 +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; 
 +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 
 +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 
 +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 
 +import net.minecraft.block.Blocks; 
 +import net.minecraft.network.PacketByteBuf; 
 +import net.minecraft.server.MinecraftServer; 
 +import net.minecraft.server.network.ServerPlayerEntity; 
 +import net.minecraft.util.Identifier; 
 + 
 +public class ExampleMod implements ModInitializer { 
 + 
 +    public static final String MOD_ID = "your_unique_mod_id_change_me_please"; 
 + 
 +    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken"); 
 + 
 +    @Override 
 +    public void onInitialize() { 
 +        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> { 
 +            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { 
 +                StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer()); 
 +                // Increment the amount of dirt blocks that have been broken 
 +                serverState.totalDirtBlocksBroken += 1; 
 + 
 +                PlayerData playerState = StateSaverAndLoader.getPlayerState(player); 
 +                playerState.dirtBlocksBroken += 1; 
 + 
 +                // Send a packet to the client 
 +                MinecraftServer server = world.getServer(); 
 + 
 +                PacketByteBuf data = PacketByteBufs.create(); 
 +                data.writeInt(serverState.totalDirtBlocksBroken); 
 +                data.writeInt(playerState.dirtBlocksBroken); 
 + 
 +                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); 
 +                server.execute(() -> { 
 +                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data); 
 +                }); 
 +            } 
 +        }); 
 +    } 
 +
 +</code> 
 + 
 +You'll also have to modify the class which ''**implements ClientModInitializer**'' as follows: 
 + 
 +<code java> 
 +import net.fabricmc.api.ClientModInitializer; 
 +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 
 +import net.minecraft.text.Text; 
 + 
 +public class ExampleModClient implements ClientModInitializer { 
 + 
 +    @Override 
 +    public void onInitializeClient() { 
 +        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (client, handler, buf, responseSender) -> { 
 +            int totalDirtBlocksBroken = buf.readInt(); 
 +            int playerSpecificDirtBlocksBroken = buf.readInt(); 
 + 
 +            client.execute(() -> { 
 +                client.player.sendMessage(Text.literal("Total dirt blocks broken: " + totalDirtBlocksBroken)); 
 +                client.player.sendMessage(Text.literal("Player specific dirt blocks broken: " + playerSpecificDirtBlocksBroken)); 
 +            }); 
 +        }); 
 +    } 
 +
 +</code> 
 + 
 +If you ran the client now, it would seem as if everything is working, but we are forgetting a crucial step: We haven't updated our ''**writeNbt**'' and ''**createFromNbt**'' to save and load our hashmap. 
 + 
 +The updated functions are as follows: 
 + 
 +<code java> 
 +public class StateSaverAndLoader extends PersistentState { 
 + 
 +    // ... (Rest of code) 
 + 
 +    @Override 
 +    public NbtCompound writeNbt(NbtCompound nbt) { 
 +        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken); 
 + 
 +        NbtCompound playersNbt = new NbtCompound(); 
 +        players.forEach((uuid, playerData) -> { 
 +            NbtCompound playerNbt = new NbtCompound(); 
 + 
 +            playerNbt.putInt("dirtBlocksBroken", playerData.dirtBlocksBroken); 
 + 
 +            playersNbt.put(uuid.toString(), playerNbt); 
 +        }); 
 +        nbt.put("players", playersNbt); 
 + 
 +        return nbt; 
 +    } 
 + 
 +    public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { 
 +        StateSaverAndLoader state = new StateSaverAndLoader(); 
 +        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken"); 
 + 
 +        NbtCompound playersNbt = tag.getCompound("players"); 
 +        playersNbt.getKeys().forEach(key -> { 
 +            PlayerData playerData = new PlayerData(); 
 + 
 +            playerData.dirtBlocksBroken = playersNbt.getCompound(key).getInt("dirtBlocksBroken"); 
 + 
 +            UUID uuid = UUID.fromString(key); 
 +            state.players.put(uuid, playerData); 
 +        }); 
 + 
 +        return state; 
 +    } 
 + 
 +    // ... (Rest of code) 
 + 
 +
 +</code> 
 + 
 +The final ''**StateSaverAndLoader**'' should be as follows: 
 + 
 +<code java> 
 +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 StateSaverAndLoader extends PersistentState { 
 + 
 +    public Integer totalDirtBlocksBroken = 0; 
 + 
 +    public HashMap<UUID, PlayerData> players = new HashMap<>(); 
 + 
 +    @Override 
 +    public NbtCompound writeNbt(NbtCompound nbt) { 
 +        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken); 
 + 
 +        NbtCompound playersNbt = new NbtCompound(); 
 +        players.forEach((uuid, playerData) -> { 
 +            NbtCompound playerNbt = new NbtCompound(); 
 + 
 +            playerNbt.putInt("dirtBlocksBroken", playerData.dirtBlocksBroken); 
 + 
 +            playersNbt.put(uuid.toString(), playerNbt); 
 +        }); 
 +        nbt.put("players", playersNbt); 
 + 
 +        return nbt; 
 +    } 
 + 
 +    public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { 
 +        StateSaverAndLoader state = new StateSaverAndLoader(); 
 +        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken"); 
 + 
 +        NbtCompound playersNbt = tag.getCompound("players"); 
 +        playersNbt.getKeys().forEach(key -> { 
 +            PlayerData playerData = new PlayerData(); 
 + 
 +            playerData.dirtBlocksBroken = playersNbt.getCompound(key).getInt("dirtBlocksBroken"); 
 + 
 +            UUID uuid = UUID.fromString(key); 
 +            state.players.put(uuid, playerData); 
 +        }); 
 + 
 +        return state; 
 +    } 
 + 
 +    private static Type<StateSaverAndLoader> type = new Type<>
 +            StateSaverAndLoader::new, // If there's no 'StateSaverAndLoader' yet create one 
 +            StateSaverAndLoader::createFromNbt, // If there is a 'StateSaverAndLoader' NBT, parse it with 'createFromNbt' 
 +            null // Supposed to be an 'DataFixTypes' enum, but we can just pass null 
 +    ); 
 + 
 +    public static StateSaverAndLoader getServerState(MinecraftServer server) { 
 +        // (Note: arbitrary choice to use 'World.OVERWORLD' instead of 'World.END' or 'World.NETHER' Any work) 
 +        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); 
 + 
 +        // The first time the following 'getOrCreate' function is called, it creates a brand new 'StateSaverAndLoader' and 
 +        // stores it inside the 'PersistentStateManager'. The subsequent calls to 'getOrCreate' pass in the saved 
 +        // 'StateSaverAndLoader' NBT on disk to our function 'StateSaverAndLoader::createFromNbt'
 +        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID); 
 + 
 +        // If state is not marked dirty, when Minecraft closes, 'writeNbt' won't be called and therefore nothing will be saved. 
 +        // Technically it's 'cleaner' if you only mark state as dirty when there was actually a change, but the vast majority 
 +        // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to 'markDirty' for them. 
 +        // Besides, it's literally just setting a bool to true, and the only time there's a 'cost' is when the file is written to disk when 
 +        // there were no actual change to any of the mods state (INCREDIBLY RARE). 
 +        state.markDirty(); 
 + 
 +        return state; 
 +    } 
 + 
 +    public static PlayerData getPlayerState(LivingEntity player) { 
 +        StateSaverAndLoader serverState = getServerState(player.getWorld().getServer()); 
 + 
 +        // Either get the player by the uuid, or we don't have data for him yet, make a new player state 
 +        PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData()); 
 + 
 +        return playerState; 
 +    } 
 +
 +</code> 
 + 
 +Running the client now, all our player-specific data is correctly saved. 
 + 
 +==== Important Caveat ==== 
 + 
 +  * Each time you restart the minecraft client with fabric, you're assigned a new random UUID each launch, so it may seem like our code is not working because it's pulling data for a new UUID never before seen. If you want to verify everything is working correctly, download [[https://www.curseforge.com/minecraft/mc-mods/auth-me|AuthMe]] and drop the ''AuthMe.jar'' into run/mods. Login to your Minecraft account from the multiplayer screen. This will now change the random UUID to your actual UUID and therefore you can test if the data is correctly saved and associated with one UUID even across restarts. (You don't have to keep doing thisonce you've verified once that, if the UUID is the same across launches, the data is correctly saved, you can just develop the mod normally safe in the knowledge that your data will be saved) 
 + 
 +Just remember if you add new fields to ''**PlayerData**'' or ''**StateSaveAndLoader**'' you need to correctly do the work of writing and loading those fields in the ''**writeNbt**'' and ''**createFromNbt**'' functions //always//. If you forget this step, your data won't be properly saved or loaded from disk. 
 + 
 +====== Initial Sync ====== 
 + 
 +What if it's important for our mod that as soon as a player joins they receive some or all the PlayerData associated with them? For this, we will crate a new packet ''**INITIAL_SYNC**'' which will send the player, their specific player data as soon as they join the world. 
 + 
 +Modify your class which ''**implements ModInitializer**'' as follows: 
 + 
 +<code java> 
 +import net.fabricmc.api.ModInitializer; 
 +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; 
 +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 
 +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 
 +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 
 +import net.minecraft.block.Blocks; 
 +import net.minecraft.network.PacketByteBuf; 
 +import net.minecraft.server.MinecraftServer; 
 +import net.minecraft.server.network.ServerPlayerEntity; 
 +import net.minecraft.util.Identifier; 
 + 
 +public class ExampleMod implements ModInitializer { 
 + 
 +    public static final String MOD_ID = "your_unique_mod_id_change_me_please"; 
 + 
 +    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken"); 
 + 
 +    public static final Identifier INITIAL_SYNC = new Identifier(MOD_ID, "initial_sync"); 
 + 
 +    @Override 
 +    public void onInitialize() { 
 +        ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { 
 +            PlayerData playerState = StateSaverAndLoader.getPlayerState(handler.getPlayer()); 
 +            PacketByteBuf data = PacketByteBufs.create(); 
 +            data.writeInt(playerState.dirtBlocksBroken); 
 +            server.execute(() -> { 
 +                ServerPlayNetworking.send(handler.getPlayer(), INITIAL_SYNC, data); 
 +            }); 
 +        }); 
 + 
 +        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> { 
 +            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { 
 +                StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer()); 
 +                // Increment the amount of dirt blocks that have been broken 
 +                serverState.totalDirtBlocksBroken += 1; 
 + 
 +                PlayerData playerState = StateSaverAndLoader.getPlayerState(player); 
 +                playerState.dirtBlocksBroken += 1; 
 + 
 +                // Send a packet to the client 
 +                MinecraftServer server = world.getServer(); 
 + 
 +                PacketByteBuf data = PacketByteBufs.create(); 
 +                data.writeInt(serverState.totalDirtBlocksBroken); 
 +                data.writeInt(playerState.dirtBlocksBroken); 
 + 
 +                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); 
 +                server.execute(() -> { 
 +                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data); 
 +                }); 
 +            } 
 +        }); 
 +    } 
 +
 +</code> 
 + 
 +Then modify your class which ''**implements ClientModInitializer**'' as follows: 
 + 
 +<code java> 
 +import net.fabricmc.api.ClientModInitializer; 
 +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 
 +import net.minecraft.text.Text; 
 + 
 +public class ExampleModClient implements ClientModInitializer { 
 + 
 +    public static PlayerData playerData = new PlayerData(); 
 + 
 +    @Override 
 +    public void onInitializeClient() { 
 +        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (client, handler, buf, responseSender) -> { 
 +            int totalDirtBlocksBroken = buf.readInt(); 
 +            playerData.dirtBlocksBroken = buf.readInt(); 
 + 
 +            client.execute(() -> { 
 +                client.player.sendMessage(Text.literal("Total dirt blocks broken: " + totalDirtBlocksBroken)); 
 +                client.player.sendMessage(Text.literal("Player specific dirt blocks broken: " + playerData.dirtBlocksBroken)); 
 +            }); 
 +        }); 
 + 
 +        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.INITIAL_SYNC, (client, handler, buf, responseSender) -> { 
 +            playerData.dirtBlocksBroken = buf.readInt(); 
 + 
 +            client.execute(() -> { 
 +                client.player.sendMessage(Text.literal("Initial specific dirt blocks broken: " + playerData.dirtBlocksBroken)); 
 +            }); 
 +        }); 
 +    } 
 +
 +</code> 
 + 
 +As soon as you join the world/server you should see a message popup telling you the amount of dirt blocks you've specifically broken. 
 + 
 +  * Note: The ''**playerData**'' we created isn't the up-to-date one that lives on the server. We simply create our own copy of ''**PlayerData**'' client-side and update it as we receive packets. Since it's ''**public static**'' that means you can access it from anywhere on the client. 
 + 
 +==== Important Caveat ==== 
 + 
 +  * Each time you restart the minecraft client with fabric, you're assigned a new random UUID each launch, so it may seem like our code is not working because it's pulling data for a new UUID never before seen. If you want to verify everything is working correctly, download [[https://www.curseforge.com/minecraft/mc-mods/auth-me|AuthMe]] and drop the ''AuthMe.jar'' into run/mods. Login to your Minecraft account from the multiplayer screen. This will now change the random UUID to your actual UUID and therefore you can test if the data is correctly saved and associated with one UUID even across restarts. (You don't have to keep doing this, once you've verified once that, if the UUID is the same across launches, the data is correctly saved, you can just develop the mod normally safe in the knowledge that your data will be saved) 
 + 
 +====== More Involved Player Data ====== 
 + 
 +And just for good measure, let's see an example of how our ''**StateSaverAndLoader**'' class would look if our ''**PlayerData**'' has more than primitives, like lists, and even its own hashmap. How would that look? 
 + 
 +Let's say this is our ''**PlayerData**''': 
 + 
 +<code java> 
 +import java.util.ArrayList; 
 +import java.util.HashMap; 
 +import java.util.List; 
 + 
 +public class PlayerData { 
 +    public int dirtBlocksBroken = 0; 
 + 
 +    public HashMap<Integer, Integer> fatigue = new HashMap<>(); 
 +  
 +    public List<Integer> oldCravings = new ArrayList<>(); 
 +
 +</code> 
 + 
 +This would be our ''**StateSaverAndLoader**'': 
 + 
 +<code java> 
 +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 StateSaverAndLoader extends PersistentState { 
 + 
 +    public Integer totalDirtBlocksBroken = 0; 
 + 
 +    public HashMap<UUID, PlayerData> players = new HashMap<>(); 
 + 
 +    @Override 
 +    public NbtCompound writeNbt(NbtCompound nbt) { 
 +        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken); 
 + 
 +        NbtCompound playersNbt = new NbtCompound(); 
 +        players.forEach((uuid, playerData) -> { 
 +            NbtCompound playerNbt = new NbtCompound(); 
 + 
 +            playerNbt.putInt("dirtBlocksBroken", playerData.dirtBlocksBroken); 
 + 
 +            playerNbt.putIntArray("oldCravings", playerData.oldCravings); 
 + 
 +            NbtCompound fatigueTag = new NbtCompound(); 
 +            playerData.fatigue.forEach((foodID, fatigueAmount) -> fatigueTag.putInt(String.valueOf(foodID), fatigueAmount)); 
 +            playerNbt.put("fatigue", fatigueTag); 
 + 
 +            playersNbt.put(uuid.toString(), playerNbt); 
 +        }); 
 +        nbt.put("players", playersNbt); 
 + 
 +        return nbt; 
 +    } 
 + 
 +    public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { 
 +        StateSaverAndLoader state = new StateSaverAndLoader(); 
 +        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken"); 
 + 
 +        NbtCompound playersNbt = tag.getCompound("players"); 
 +        playersNbt.getKeys().forEach(key -> { 
 +            PlayerData playerData = new PlayerData(); 
 + 
 +            playerData.dirtBlocksBroken = playersNbt.getCompound(key).getInt("dirtBlocksBroken"); 
 + 
 +            NbtCompound fatigueCompound = playersNbt.getCompound(key).getCompound("fatigue"); 
 +            fatigueCompound.getKeys().forEach(s -> { 
 +                Integer foodID = Integer.valueOf(s); 
 +                int fatigueAmount = fatigueCompound.getInt(s); 
 +                playerData.fatigue.put(foodID, fatigueAmount); 
 +            }); 
 + 
 +            for (int oldCravings : playersNbt.getCompound(key).getIntArray("oldCravings")) { 
 +                playerData.oldCravings.add(oldCravings); 
 +            } 
 + 
 +            UUID uuid = UUID.fromString(key); 
 +            state.players.put(uuid, playerData); 
 +        }); 
 + 
 +        return state; 
 +    } 
 + 
 +    private static Type<StateSaverAndLoader> type = new Type<>
 +            StateSaverAndLoader::new, // If there's no 'StateSaverAndLoader' yet create one 
 +            StateSaverAndLoader::createFromNbt, // If there is a 'StateSaverAndLoader' NBT, parse it with 'createFromNbt' 
 +            null // Supposed to be an 'DataFixTypes' enum, but we can just pass null 
 +    ); 
 + 
 +    public static StateSaverAndLoader getServerState(MinecraftServer server) { 
 +        // (Note: arbitrary choice to use 'World.OVERWORLD' instead of 'World.END' or 'World.NETHER' Any work) 
 +        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); 
 + 
 +        // The first time the following 'getOrCreate' function is called, it creates a brand new 'StateSaverAndLoader' and 
 +        // stores it inside the 'PersistentStateManager'. The subsequent calls to 'getOrCreate' pass in the saved 
 +        // 'StateSaverAndLoader' NBT on disk to our function 'StateSaverAndLoader::createFromNbt'
 +        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID); 
 + 
 +        // If state is not marked dirty, when Minecraft closes, 'writeNbt' won't be called and therefore nothing will be saved. 
 +        // Technically it's 'cleaner' if you only mark state as dirty when there was actually a change, but the vast majority 
 +        // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to 'markDirty' for them. 
 +        // Besides, it's literally just setting a bool to true, and the only time there's a 'cost' is when the file is written to disk when 
 +        // there were no actual change to any of the mods state (INCREDIBLY RARE). 
 +        state.markDirty(); 
 + 
 +        return state; 
 +    } 
 + 
 +    public static PlayerData getPlayerState(LivingEntity player) { 
 +        StateSaverAndLoader serverState = getServerState(player.getWorld().getServer()); 
 + 
 +        // Either get the player by the uuid, or we don't have data for him yet, make a new player state 
 +        PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData()); 
 + 
 +        return playerState; 
 +    } 
 +
 +</code> 
 + 
 +Our classes which implement ''**ClientModInitializer**'' and ''**ModInitializer**'' would be the same as before, but we include them here so that this section of the article is easy to copy and paste into you program and experiment with persistent state. 
 + 
 +<code java> 
 +import net.fabricmc.api.ClientModInitializer; 
 +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 
 +import net.minecraft.text.Text; 
 + 
 +public class ExampleModClient implements ClientModInitializer { 
 + 
 +    public static PlayerData playerData = new PlayerData(); 
 + 
 +    @Override 
 +    public void onInitializeClient() { 
 +        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (client, handler, buf, responseSender) -> { 
 +            int totalDirtBlocksBroken = buf.readInt(); 
 +            playerData.dirtBlocksBroken = buf.readInt(); 
 + 
 +            client.execute(() -> { 
 +                client.player.sendMessage(Text.literal("Total dirt blocks broken: " + totalDirtBlocksBroken)); 
 +                client.player.sendMessage(Text.literal("Player specific dirt blocks broken: " + playerData.dirtBlocksBroken)); 
 +            }); 
 +        }); 
 + 
 +        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.INITIAL_SYNC, (client, handler, buf, responseSender) -> { 
 +            playerData.dirtBlocksBroken = buf.readInt(); 
 + 
 +            client.execute(() -> { 
 +                client.player.sendMessage(Text.literal("Initial specific dirt blocks broken: " + playerData.dirtBlocksBroken)); 
 +            }); 
 +        }); 
 +    } 
 +
 +</code> 
 + 
 + 
 +<code java> 
 +import net.fabricmc.api.ModInitializer; 
 +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; 
 +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 
 +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 
 +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 
 +import net.minecraft.block.Blocks; 
 +import net.minecraft.network.PacketByteBuf; 
 +import net.minecraft.server.MinecraftServer; 
 +import net.minecraft.server.network.ServerPlayerEntity; 
 +import net.minecraft.util.Identifier; 
 + 
 +public class ExampleMod implements ModInitializer { 
 + 
 +    public static final String MOD_ID = "your_unique_mod_id_change_me_please"; 
 + 
 +    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken"); 
 + 
 +    public static final Identifier INITIAL_SYNC = new Identifier(MOD_ID, "initial_sync"); 
 + 
 +    @Override 
 +    public void onInitialize() { 
 +        ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { 
 +            PlayerData playerState = StateSaverAndLoader.getPlayerState(handler.getPlayer()); 
 +            PacketByteBuf data = PacketByteBufs.create(); 
 +            data.writeInt(playerState.dirtBlocksBroken); 
 +            server.execute(() -> { 
 +                ServerPlayNetworking.send(handler.getPlayer(), INITIAL_SYNC, data); 
 +            }); 
 +        }); 
 + 
 +        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> { 
 +            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { 
 +                StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer()); 
 +                // Increment the amount of dirt blocks that have been broken 
 +                serverState.totalDirtBlocksBroken += 1; 
 + 
 +                PlayerData playerState = StateSaverAndLoader.getPlayerState(player); 
 +                playerState.dirtBlocksBroken += 1; 
 + 
 +                // Send a packet to the client 
 +                MinecraftServer server = world.getServer(); 
 + 
 +                PacketByteBuf data = PacketByteBufs.create(); 
 +                data.writeInt(serverState.totalDirtBlocksBroken); 
 +                data.writeInt(playerState.dirtBlocksBroken); 
 + 
 +                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); 
 +                server.execute(() -> { 
 +                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data); 
 +                }); 
 +            } 
 +        }); 
 +    } 
 +
 +</code>
tutorial/persistent_states.1657621760.txt.gz · Last modified: 2022/07/12 10:29 by mineblock11