User Tools

Site Tools


tutorial:persistent_states

Differences

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

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
tutorial:persistent_states [2022/12/21 04:44] – created jmanc3tutorial:persistent_states [2024/04/25 14:06] (current) mayaqq
Line 1: Line 1:
 ====== Persistent State ====== ====== 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.+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> <code java>
-int furnacesCrafted = 0;+int dirtBlocksBroken = 0;
 </code> </code>
  
-And it keeps track of how many times furnaces on the server have been crafted in total by all players.+And that the mod also keeps track of how many dirt blocks have been broken, on the serverin total:
  
 <code java> <code java>
-int totalFurnacesCrafted = 0;+int totalDirtBlocksBroken = 0;
 </code> </code>
  
-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?+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?
  
-===== Server-Side or Client-Side ====+====== Simple Message ======
  
-Before thatwe 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'client-side only like performance mods usually are). It'important to remember that server-side doesn't necessarily mean multiplayer. Singleplayer worlds talk to 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"+Firstsince the data will be saved on the 'server', (NOTE: that there is always a 'server' running even when you play offline, so don'be scared about that word), let'send a simple packet to the player when the server detects the player breaks dirt block, and print it to the chat.
  
-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:+Modify your class which ''**implements ModInitializer**'' as follows:
  
-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:+<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;
  
-  - Client sends a request to the server for another players information. +public class ExampleMod implements ModInitializer {
-  - Server receives request and sends its own request to the player we want the information from. +
-  - Client replies with requested data. +
-  - 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.+    public static final String MOD_ID = "your_unique_mod_id_change_me_please";
  
-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.+    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken");
  
-Side-Note: +    private Integer totalDirtBlocksBroken = 0;
  
-  * 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 itthat 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.+    @Override 
 +    public void onInitialize() 
 +        PlayerBlockBreakEvents.AFTER.register((world, playerpos, state, entity) -> { 
 +            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { 
 +                // Increment the amount of dirt blocks that have been broken 
 +                totalDirtBlocksBroken += 1;
  
-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.+                // Send a packet to the client 
 +                MinecraftServer server = world.getServer();
  
-===== Long Term Storage (Server=====+                PacketByteBuf data PacketByteBufs.create()
 +                data.writeInt(totalDirtBlocksBroken);
  
-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.+                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); 
 +                server.execute(() -> { 
 +                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data); 
 +                }); 
 +            } 
 +        }); 
 +    } 
 +
 +</code> 
 +  
 +  * The ''**MOD_ID**'' should be uniqueUsually 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.
  
-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.+Next modify your class which ''**implements ClientModInitializer**'' as follows:
  
 <code java> <code java>
-public class ServerState extends PersistentState {+import net.fabricmc.api.ClientModInitializer; 
 +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 
 +import net.minecraft.text.Text;  
 + 
 +public class ExampleModClient implements ClientModInitializer {
  
-    int totalFurnacesCrafted = 0; 
-     
     @Override     @Override
-    public NbtCompound writeNbt(NbtCompound nbt) { +    public void onInitializeClient() { 
-        nbt.putInt("totalFurnacesCrafted"totalFurnacesCrafted); +        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (client, handler, buf, responseSender-> 
-        return nbt; +            int totalDirtBlocksBroken buf.readInt(); 
-    }     +            client.execute(() -> { 
-     +                client.player.sendMessage(Text.literal("Total dirt blocks broken: + totalDirtBlocksBroken)); 
-    public static ServerState createFromNbt(NbtCompound tag) { +            }); 
-        ServerState playerState new ServerState(); +        });
-        playerState.totalFurnacesCrafted = tag.getInt("totalFurnacesCrafted"); +
-        return playerState;+
     }     }
 } }
 </code> </code>
  
-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.+  * 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.
  
-<code java> +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?
-boolean keepingTrack = true; +
-</code>+
  
-You have to make sure to update those two functions to reflect that, or your data will not be saved correctly.+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 donebut 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> <code java>
-public class ServerState extends PersistentState {+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;
  
-    int totalFurnacesCrafted = 0; 
-     
-    boolean keepingTrack = true; 
-     
     @Override     @Override
     public NbtCompound writeNbt(NbtCompound nbt) {     public NbtCompound writeNbt(NbtCompound nbt) {
-        nbt.putInt("totalFurnacesCrafted", totalFurnacesCrafted); +        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken);
-        nbt.putBoolean("keepingTrack", keepingTrack);+
         return nbt;         return nbt;
-    }     
-     
-    public static ServerState createFromNbt(NbtCompound tag) { 
-        ServerState serverState = new ServerState(); 
-        serverState.totalFurnacesCrafted = tag.getInt("totalFurnacesCrafted"); 
-        serverState.keepingTrack = tag.getBoolean("keepingTrack"); 
-        return serverState; 
     }     }
 } }
 </code> </code>
  
-  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.+Note: ''**writeNbt**'' must be implemented when extending ''**PersistentState**''. In the functionyou get passed in an ''**NbtCompound**'' which we are supposed to pack with the data we want saved to disk. In our casewe moved the ''**public Integer totalDirtBlocksBroken**'' variable we had created earlier into this file.
  
-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.+  ''**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> <code java>
-public class ServerState extends PersistentState {+public class StateSaverAndLoader extends PersistentState {
  
-    // ... (What we'written before)+    // ... (Previously written code)
  
-    public static ServerState getServerState(MinecraftServer server) { +    public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { 
-        // First we get the persistentStateManager for the OVERWORLD +        StateSaverAndLoader state new StateSaverAndLoader(); 
-        PersistentStateManager persistentStateManager server +        state.totalDirtBlocksBroken tag.getInt("totalDirtBlocksBroken"); 
-                .getWorld(World.OVERWORLD).getPersistentStateManager(); +        return state; 
-     +    } 
-        // 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 keyYou should already have a MODID variable defined by you somewhere in your code. Use that. +</code>
-        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+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**''
-     + 
-        return serverState;+  * 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> </code>
  
-One thing to note is the line **serverState.markDirty();**. Had we not done that, Minecraft wouldn't ever save any changes we make to the state. Here's how to use it:+Your ''**StateSaverAndLoader**'' file should look as follows:
  
 <code java> <code java>
-ServerState serverState ServerState.getServerState(server); +import net.minecraft.nbt.NbtCompound; 
-System.out.println("The server has seen this many furnaces crafted" + serverState.totalFurnacesCrafted);+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) { 
 +        // (Notearbitrary 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> </code>
  
-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'say we wanted to pass every player that joined that information (how many furnaces have been crafted total).+You'll also have to update your class which ''**implements ModInitializer**'' so that it'as follows:
  
 <code java> <code java>
-public class OurMod implements ModInitializer { +import net.fabricmc.api.ModInitializer; 
-    public static final String MODID = "OUR_MOD_ID";+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     @Override
     public void onInitialize() {     public void onInitialize() {
-        ServerPlayConnectionEvents.JOIN.register((handlersenderserver) -> { +        PlayerBlockBreakEvents.AFTER.register((world, player, posstateentity) -> { 
-            // You can see we use the function getServer() that's on the player+            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { 
-            ServerState serverState = ServerState.getServerState(handler.player.world.getServer());+                StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer())
 +                // Increment the amount of dirt blocks that have been broken 
 +                serverState.totalDirtBlocksBroken += 1;
  
-            // Sending the packet to the player (look at the networking page for more information+                // Send a packet to the client 
-            PacketByteBuf data = PacketByteBufs.create(); +                MinecraftServer server = world.getServer(); 
-            data.writeInt(serverState.totalFurnacesCrafted); + 
-            ServerPlayNetworking.send(handler.player, NetworkingMessages.CRAFTED_FURNACES, data);+                PacketByteBuf data = PacketByteBufs.create(); 
 +                data.writeInt(serverState.totalDirtBlocksBroken); 
 + 
 +                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); 
 +                server.execute(() -> { 
 +                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data); 
 +                }); 
 +            }
         });         });
     }     }
Line 150: Line 270:
 </code> </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 About Persistent Data That Is Associated With a Player====+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 ======
  
-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 individuallyThat type of information should be associated to the player. How do we do that?+We can store player-specific data by extending what we already wrote
  
-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)+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> <code java>
-public class PlayerState +class PlayerData 
-    int furnacesCrafted = 0;+    public int dirtBlocksBroken = 0;
 } }
 </code> </code>
  
-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).+To simplify, we're just continuing with our simple examplebut 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 {
  
-<code java [highlight_lines_extra="6"]> +    public Integer totalDirtBlocksBroken = 0;
-public class ServerState extends PersistentState { +
-    int totalFurnacesCrafted = 0;+
          
-    boolean keepingTrack = true; +    public HashMap<UUID, PlayerData> players = new HashMap<>(); 
-     + 
-    public HashMap<UUID, PlayerState> players = new HashMap<>();+    // ... (Rest of the code) 
 + 
 +}
 </code> </code>
  
-Every player has unique UUID we can use to identify them specifically.+Note: We create ''**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).
  
-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).+Let's add a utility function to ''**StateSaverAndLoader**'' which will take a ''**LivingEntity**'' and return the associated ''**PlayerData**'' in our '''**HashMap**'''.
  
-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.+<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");
  
-<code java [highlight_lines_extra="3,4,5,6,7,8,9,10,11,12,13"]> 
     @Override     @Override
-    public NbtCompound writeNbt(NbtCompound nbt) { +    public void onInitialize() { 
-        // Putting the 'players' hashmap, into the 'nbt' which will be saved. +        PlayerBlockBreakEvents.AFTER.register((worldplayer, pos, state, entity) -> { 
-        NbtCompound playersNbtCompound = new NbtCompound(); +            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { 
-        players.forEach((UUIDplayerSate) -> { +                StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer()); 
-            NbtCompound playerStateNbt new NbtCompound();+                // Increment the amount of dirt blocks that have been broken 
 +                serverState.totalDirtBlocksBroken += 1;
  
-            // ANYTIME YOU PUT NEW DATA IN THE PlayerState CLASS YOU NEED TO REFLECT THAT HERE!!! +                PlayerData playerState = StateSaverAndLoader.getPlayerState(player)
-            playerStateNbt.putInt("furnacesCrafted", playerSate.furnacesCrafted);+                playerState.dirtBlocksBroken += 1;
  
-            playersNbtCompound.put(String.valueOf(UUID), playerStateNbt);+                // 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(playerEntityDIRT_BROKEN, data); 
 +                }); 
 +            }
         });         });
-        nbt.put("players", playersNbtCompound);+    } 
 +
 +</code>
  
-        // Putting the 'ServerStatedata on the 'nbtso it'll be saved as well. +You'll also have to modify the class which ''**implements ClientModInitializer**'' as follows:
-        nbt.putInt("totalFurnacesCrafted", totalFurnacesCrafted); +
-        nbt.putBoolean("keepingTrack", keepingTrack);+
  
-        return nbt;+<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> </code>
  
-The **createFromNbt** is where the program passes you an NbtCompound and you need to convert back into your state.+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.
  
-<code java [highlight_lines_extra="4,5,6,7,8,9,10,11,12,13"]> +The updated functions are as follows:
-    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 +<code java> 
-        NbtCompound playersTag = tag.getCompound("players"); +public class StateSaverAndLoader extends PersistentState {
-        playersTag.getKeys().forEach(key -> { +
-            PlayerState playerState = new PlayerState();+
  
-            playerState.furnacesCrafted = playersTag.getCompound(key).getInt("furnacesCrafted");+    // ... (Rest of code)
  
-            UUID uuid UUID.fromString(key); +    @Override 
-            serverState.players.put(uuid, playerState);+    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);
  
-        serverState.totalFurnacesCrafted = tag.getInt("totalFurnacesCrafted"); +        return nbt; 
-        serverState.keepingTrack = tag.getBoolean("keepingTrack");+    } 
 + 
 +    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 serverState;+        return state;
     }     }
 +
 +    // ... (Rest of code)
 +
 +}
 </code> </code>
  
-Finally we add a utility function which takes a player, looks through the server state 'playershashmap with it'**UUID**, and either creates a new **PlayerState** and adds itself to the hashmap, or just returns the one it already found.+The final ''**StateSaverAndLoader**'' should be as follows:
  
-<code java [highlight_lines_extra="14,15,16,17,18,19,20,21"]+<code java
-    public static ServerState getServerState(MinecraftServer server) {+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((uuidplayerData) -> { 
 +            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 tagRegistryWrapper.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();         PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager();
  
-        ServerState serverState = persistentStateManager.getOrCreate( +        // The first time the following 'getOrCreate' function is called, it creates a brand new 'StateSaverAndLoader' and 
-                ServerState::createFromNbt, +        // stores it inside the 'PersistentStateManager'The subsequent calls to 'getOrCreate' pass in the saved 
-                ServerState::new, +        // 'StateSaverAndLoader' NBT on disk to our function 'StateSaverAndLoader::createFromNbt'. 
-                "YOUR_UNIQUE_MOD_ID (PLEASE CHANGE ME!!!)");+        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID);
  
-        serverState.markDirty(); // YOU MUST DO THIS!!!! Or data wont be saved correctly.+        // 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 serverState;+        return state;
     }     }
  
-    public static PlayerState getPlayerState(LivingEntity player) { +    public static PlayerData getPlayerState(LivingEntity player) { 
-        ServerState serverState = getServerState(player.world.getServer());+        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         // 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());+        PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData());
  
         return playerState;         return playerState;
Line 255: Line 539:
 </code> </code>
  
-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.+Running the client now, all our player-specific data is correctly saved.
  
-<code java [highlight_lines_extra="8,13"]+==== Important Caveat ==== 
-public class OurMod implements ModInitializer { + 
-    public static final String MODID = "OUR_MOD_ID";+  * 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) 
 + 
 +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 thiswe 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     @Override
     public void onInitialize() {     public void onInitialize() {
         ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {         ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
-            ServerState serverState ServerState.getServerState(handler.player.world.getServer()); +            PlayerData playerState StateSaverAndLoader.getPlayerState(handler.getPlayer());
-            PlayerState playerState = ServerState.getPlayerState(handler.player); +
- +
-            // Sending the packet to the player (look at the networking page for more information)+
             PacketByteBuf data = PacketByteBufs.create();             PacketByteBuf data = PacketByteBufs.create();
-            data.writeInt(serverState.totalFurnacesCrafted); +            data.writeInt(playerState.dirtBlocksBroken); 
-            data.writeInt(playerState.furnacesCrafted); +            server.execute(() -> { 
-            ServerPlayNetworking.send(handler.player, NetworkingMessages.CRAFTED_FURNACES, data);+                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); 
 +                }); 
 +            }
         });         });
     }     }
Line 277: Line 610:
 </code> </code>
  
------+Then modify your class which ''**implements ClientModInitializer**'' as follows:
  
-===== Long Term Storage (Client) ====+<code java> 
 +import net.fabricmc.api.ClientModInitializer; 
 +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 
 +import net.minecraft.text.Text;
  
-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.+public class ExampleModClient implements ClientModInitializer {
  
-  * Note because of the use MinecraftClient.getInstance(), this function should only ever be called by the client. Never the server.+    public static PlayerData playerData = new PlayerData();
  
-<code java> +    @Override 
-public static ClientState getPlayerState() { +    public void onInitializeClient() { 
-    ClientPlayerEntity player = MinecraftClient.getInstance().player; +        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (client, handler, buf, responseSender-> { 
-     +            int totalDirtBlocksBroken buf.readInt(); 
-    PersistentStateManager persistentStateManager player.world.getServer() +            playerData.dirtBlocksBroken = buf.readInt();
-        .getWorld(World.OVERWORLD).getPersistentStateManager();+
  
-    ClientState clientState = persistentStateManager.getOrCreate+            client.execute(() -> { 
-        ClientState::createFromNbt, +                client.player.sendMessage(Text.literal("Total dirt blocks broken" + totalDirtBlocksBroken)); 
-        ClientState::new, +                client.player.sendMessage(Text.literal("Player specific dirt blocks broken: " + playerData.dirtBlocksBroken)); 
-        player.getUuidAsString()); // We use the UUID of the player as the key so they can retreive their data later+            }); 
 +        });
  
-    return clientState;+        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>
  
 +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.
  
-===== More Involved Player State =====+  * 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.
  
-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?+==== Important Caveat ====
  
-Let's say this is our **PlayerState**:+  * 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> <code java>
-public class PlayerState { +import java.util.ArrayList; 
-    int furnacesCrafted = 0;+import java.util.HashMap; 
 +import java.util.List;
  
-    public HashMap<Integer, Integer> fatigue new HashMap<>();+public class PlayerData { 
 +    public int dirtBlocksBroken 0;
  
 +    public HashMap<Integer, Integer> fatigue = new HashMap<>();
 + 
     public List<Integer> oldCravings = new ArrayList<>();     public List<Integer> oldCravings = new ArrayList<>();
 } }
 </code> </code>
  
-This is how the **ServerState** should look.+This would be our ''**StateSaverAndLoader**'':
  
 <code java> <code java>
-public class ServerState extends PersistentState { +import net.minecraft.entity.LivingEntity; 
-    int totalFurnacesCrafted = 0;+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;
  
-    boolean keepingTrack = true;+import java.util.HashMap; 
 +import java.util.UUID;
  
-    public HashMap<UUID, PlayerState> players = new HashMap<>();+public class StateSaverAndLoader extends PersistentState {
  
-    public static ServerState createFromNbt(NbtCompound tag) { +    public Integer totalDirtBlocksBroken 0;
-        ServerState serverState new ServerState();+
  
-        NbtCompound playersTag = tag.getCompound("players"); +    public HashMap<UUID, PlayerDataplayers = new HashMap<>();
-        playersTag.getKeys().forEach(key -+
-            PlayerState playerState = new PlayerState();+
  
-            playerState.previousFoodAteID playersTag.getCompound(key).getInt("furnacesCrafted");+    @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 = playersTag.getCompound(key).getCompound("fatigue");+            NbtCompound fatigueCompound = playersNbt.getCompound(key).getCompound("fatigue");
             fatigueCompound.getKeys().forEach(s -> {             fatigueCompound.getKeys().forEach(s -> {
                 Integer foodID = Integer.valueOf(s);                 Integer foodID = Integer.valueOf(s);
                 int fatigueAmount = fatigueCompound.getInt(s);                 int fatigueAmount = fatigueCompound.getInt(s);
-                playerState.fatigue.put(foodID, fatigueAmount);+                playerData.fatigue.put(foodID, fatigueAmount);
             });             });
  
-            for (int oldCravings : playersTag.getCompound(key).getIntArray("oldCravings")) { +            for (int oldCravings : playersNbt.getCompound(key).getIntArray("oldCravings")) { 
-                playerState.oldCravings.add(oldCravings);+                playerData.oldCravings.add(oldCravings);
             }             }
  
             UUID uuid = UUID.fromString(key);             UUID uuid = UUID.fromString(key);
-            serverState.players.put(uuid, playerState);+            state.players.put(uuid, playerData);
         });         });
  
-        serverState.totalFurnacesCrafted = tag.getInt("totalFurnacesCrafted")+        return state
-        serverState.keepingTrack = tag.getBoolean("keepingTrack");+    }
  
-        return serverState;+    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;
     }     }
  
-    @Override +    public static PlayerData getPlayerState(LivingEntity player) { 
-    public NbtCompound writeNbt(NbtCompound nbt) { +        StateSaverAndLoader serverState getServerState(player.getWorld().getServer());
-        NbtCompound playersNbt new NbtCompound();+
  
-        players.forEach((UUID, playerData) -> +        // Either get the player by the uuid, or we don't have data for him yet, make a new player state 
-            NbtCompound playerDataAsNbt = new NbtCompound();+        PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData());
  
-            playerDataAsNbt.putInt("furnacesCrafted", playerData.furnacesCrafted);+        return playerState; 
 +    } 
 +
 +</code>
  
-            playerDataAsNbt.putIntArray("oldCravings"playerData.oldCravings);+Our classes which implement ''**ClientModInitializer**'' and ''**ModInitializer**'' would be the same as beforebut 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.
  
-            NbtCompound fatigueTag = new NbtCompound()+<code java> 
-            playerData.fatigue.forEach((foodID, fatigueAmount) -> fatigueTag.putInt(String.valueOf(foodID), fatigueAmount))+import net.fabricmc.api.ClientModInitializer
-            playerDataAsNbt.put("fatigue", fatigueTag);+import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
 +import net.minecraft.text.Text;
  
-            playersNbt.put(String.valueOf(UUID)playerDataAsNbt);+public class ExampleModClient implements ClientModInitializer { 
 + 
 +    public static PlayerData playerData = new PlayerData(); 
 + 
 +    @Override 
 +    public void onInitializeClient() { 
 +        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (clienthandler, 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)); 
 +            });
         });         });
-        nbt.put("players", playersNbt); 
  
-        nbt.putInt("totalFurnacesCrafted"totalFurnacesCrafted); +        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.INITIAL_SYNC(client, handler, buf, responseSender-> { 
-        nbt.putBoolean("keepingTrack", keepingTrack);+            playerData.dirtBlocksBroken = buf.readInt();
  
-        return nbt;+            client.execute(() -> { 
 +                client.player.sendMessage(Text.literal("Initial specific dirt blocks broken: " + playerData.dirtBlocksBroken)); 
 +            }); 
 +        });
     }     }
 +}
 +</code>
  
-    public static ServerState getServerState(MinecraftServer server) { 
-        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); 
  
-        ServerState serverState = persistentStateManager.getOrCreate( +<code java> 
-                ServerState::createFromNbt, +import net.fabricmc.api.ModInitializer; 
-                ServerState::new, +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; 
-                "YOUR_UNIQUE_MOD_ID (PLEASE CHANGE ME!!!)");+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;
  
-        serverState.markDirty();+public class ExampleMod implements ModInitializer {
  
-        return serverState; +    public static final String MOD_ID = "your_unique_mod_id_change_me_please";
-    }+
  
-    public static PlayerState getPlayerState(LivingEntity player) { +    public static final Identifier DIRT_BROKEN new Identifier(MOD_ID, "dirt_broken");
-        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 +    public static final Identifier INITIAL_SYNC = new Identifier(MOD_ID"initial_sync");
-        PlayerState playerState = serverState.players.computeIfAbsent(player.getUuid()uuid -> new PlayerState());+
  
-        return playerState;+    @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> </code>
- 
tutorial/persistent_states.1671597846.txt.gz · Last modified: 2022/12/21 04:44 by jmanc3