tutorial:persistent_states
Differences
This shows you the differences between two versions of the page.
Next revision | Previous revision | ||
tutorial:persistent_states [2022/07/12 10:29] – created mineblock11 | tutorial:persistent_states [2024/04/25 14:06] (current) – mayaqq | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Persistent | + | ====== Persistent |
- | [TDB, mineblock11 | + | 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; | ||
+ | </ | ||
+ | |||
+ | 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; | ||
+ | </ | ||
+ | |||
+ | How do we get fabric to save that information, | ||
+ | |||
+ | ====== Simple Message ====== | ||
+ | |||
+ | First, since the data will be saved on the ' | ||
+ | |||
+ | Modify your class which '' | ||
+ | |||
+ | <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 = " | ||
+ | |||
+ | public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, | ||
+ | |||
+ | private Integer totalDirtBlocksBroken = 0; | ||
+ | |||
+ | @Override | ||
+ | public void onInitialize() { | ||
+ | PlayerBlockBreakEvents.AFTER.register((world, | ||
+ | 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, | ||
+ | }); | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | * The '' | ||
+ | * To send a packet we use: '' | ||
+ | |||
+ | Next modify your class which '' | ||
+ | |||
+ | <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, | ||
+ | int totalDirtBlocksBroken = buf.readInt(); | ||
+ | client.execute(() -> { | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | }); | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | * You can see the '' | ||
+ | |||
+ | 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' | ||
+ | |||
+ | 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 '' | ||
+ | |||
+ | ====== Persistent State ====== | ||
+ | |||
+ | First make a new file '' | ||
+ | |||
+ | <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(" | ||
+ | return nbt; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Note: '' | ||
+ | |||
+ | * '' | ||
+ | |||
+ | 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(" | ||
+ | return state; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | This function does the opposite of '' | ||
+ | |||
+ | * Note: how we pull out the int we stored earlier with '' | ||
+ | |||
+ | Now we just need to add one more utility function which hooks everything up together. This function will take a '' | ||
+ | |||
+ | <code java> | ||
+ | public class StateSaverAndLoader extends PersistentState { | ||
+ | |||
+ | // ... (Previously written code) | ||
+ | |||
+ | private static Type< | ||
+ | StateSaverAndLoader:: | ||
+ | StateSaverAndLoader:: | ||
+ | null // Supposed to be an ' | ||
+ | ); | ||
+ | |||
+ | public static StateSaverAndLoader getServerState(MinecraftServer server) { | ||
+ | // (Note: arbitrary choice to use ' | ||
+ | PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); | ||
+ | |||
+ | // The first time the following ' | ||
+ | // stores it inside the ' | ||
+ | // ' | ||
+ | StateSaverAndLoader state = persistentStateManager.getOrCreate(type, | ||
+ | |||
+ | // If state is not marked dirty, when Minecraft closes, ' | ||
+ | // Technically it's ' | ||
+ | // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to ' | ||
+ | // Besides, it's literally just setting a bool to true, and the only time there' | ||
+ | // there were no actual change to any of the mods state (INCREDIBLY RARE). | ||
+ | state.markDirty(); | ||
+ | |||
+ | return state; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Your '' | ||
+ | |||
+ | <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(" | ||
+ | return nbt; | ||
+ | } | ||
+ | |||
+ | public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = tag.getInt(" | ||
+ | return state; | ||
+ | } | ||
+ | |||
+ | private static Type< | ||
+ | StateSaverAndLoader:: | ||
+ | StateSaverAndLoader:: | ||
+ | null // Supposed to be an ' | ||
+ | ); | ||
+ | |||
+ | public static StateSaverAndLoader getServerState(MinecraftServer server) { | ||
+ | // (Note: arbitrary choice to use ' | ||
+ | PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); | ||
+ | |||
+ | // The first time the following ' | ||
+ | // stores it inside the ' | ||
+ | // ' | ||
+ | StateSaverAndLoader state = persistentStateManager.getOrCreate(type, | ||
+ | |||
+ | // If state is not marked dirty, when Minecraft closes, ' | ||
+ | // Technically it's ' | ||
+ | // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to ' | ||
+ | // Besides, it's literally just setting a bool to true, and the only time there' | ||
+ | // there were no actual change to any of the mods state (INCREDIBLY RARE). | ||
+ | state.markDirty(); | ||
+ | |||
+ | return state; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | You'll also have to update your class which '' | ||
+ | |||
+ | <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 = " | ||
+ | |||
+ | public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, | ||
+ | |||
+ | @Override | ||
+ | public void onInitialize() { | ||
+ | PlayerBlockBreakEvents.AFTER.register((world, | ||
+ | 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, | ||
+ | }); | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 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 '' | ||
+ | ====== Player Specific Persistent State ====== | ||
+ | |||
+ | We can store player-specific data by extending what we already wrote. | ||
+ | |||
+ | First write a new class '' | ||
+ | |||
+ | * Extremely important note: Since we'll be creating a HashMap which seemingly stores '' | ||
+ | |||
+ | <code java> | ||
+ | class PlayerData { | ||
+ | public int dirtBlocksBroken = 0; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | To simplify, we're just continuing with our simple example, but you could put any fields you'd like in the '' | ||
+ | |||
+ | Next, we'll modify the top of our '' | ||
+ | |||
+ | <code java> | ||
+ | // ... (Previous imports) | ||
+ | import java.util.HashMap; | ||
+ | import java.util.UUID; | ||
+ | |||
+ | public class StateSaverAndLoader extends PersistentState { | ||
+ | |||
+ | public Integer totalDirtBlocksBroken = 0; | ||
+ | |||
+ | public HashMap< | ||
+ | |||
+ | // ... (Rest of the code) | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | Note: We create a '' | ||
+ | |||
+ | Let's add a utility function to '' | ||
+ | |||
+ | <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(), | ||
+ | |||
+ | return playerState; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | * If our '' | ||
+ | |||
+ | Now update the class which '' | ||
+ | |||
+ | <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 = " | ||
+ | |||
+ | public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, | ||
+ | |||
+ | @Override | ||
+ | public void onInitialize() { | ||
+ | PlayerBlockBreakEvents.AFTER.register((world, | ||
+ | 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, | ||
+ | }); | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | You'll also have to modify the class which '' | ||
+ | |||
+ | <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, | ||
+ | int totalDirtBlocksBroken = buf.readInt(); | ||
+ | int playerSpecificDirtBlocksBroken = buf.readInt(); | ||
+ | |||
+ | client.execute(() -> { | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | }); | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | If you ran the client now, it would seem as if everything is working, but we are forgetting a crucial step: We haven' | ||
+ | |||
+ | The updated functions are as follows: | ||
+ | |||
+ | <code java> | ||
+ | public class StateSaverAndLoader extends PersistentState { | ||
+ | |||
+ | // ... (Rest of code) | ||
+ | |||
+ | @Override | ||
+ | public NbtCompound writeNbt(NbtCompound nbt) { | ||
+ | nbt.putInt(" | ||
+ | |||
+ | NbtCompound playersNbt = new NbtCompound(); | ||
+ | players.forEach((uuid, | ||
+ | NbtCompound playerNbt = new NbtCompound(); | ||
+ | |||
+ | playerNbt.putInt(" | ||
+ | |||
+ | playersNbt.put(uuid.toString(), | ||
+ | }); | ||
+ | nbt.put(" | ||
+ | |||
+ | return nbt; | ||
+ | } | ||
+ | |||
+ | public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = tag.getInt(" | ||
+ | |||
+ | NbtCompound playersNbt = tag.getCompound(" | ||
+ | playersNbt.getKeys().forEach(key -> { | ||
+ | PlayerData playerData = new PlayerData(); | ||
+ | |||
+ | playerData.dirtBlocksBroken = playersNbt.getCompound(key).getInt(" | ||
+ | |||
+ | UUID uuid = UUID.fromString(key); | ||
+ | state.players.put(uuid, | ||
+ | }); | ||
+ | |||
+ | return state; | ||
+ | } | ||
+ | |||
+ | // ... (Rest of code) | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | The final '' | ||
+ | |||
+ | <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< | ||
+ | |||
+ | @Override | ||
+ | public NbtCompound writeNbt(NbtCompound nbt) { | ||
+ | nbt.putInt(" | ||
+ | |||
+ | NbtCompound playersNbt = new NbtCompound(); | ||
+ | players.forEach((uuid, | ||
+ | NbtCompound playerNbt = new NbtCompound(); | ||
+ | |||
+ | playerNbt.putInt(" | ||
+ | |||
+ | playersNbt.put(uuid.toString(), | ||
+ | }); | ||
+ | nbt.put(" | ||
+ | |||
+ | return nbt; | ||
+ | } | ||
+ | |||
+ | public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = tag.getInt(" | ||
+ | |||
+ | NbtCompound playersNbt = tag.getCompound(" | ||
+ | playersNbt.getKeys().forEach(key -> { | ||
+ | PlayerData playerData = new PlayerData(); | ||
+ | |||
+ | playerData.dirtBlocksBroken = playersNbt.getCompound(key).getInt(" | ||
+ | |||
+ | UUID uuid = UUID.fromString(key); | ||
+ | state.players.put(uuid, | ||
+ | }); | ||
+ | |||
+ | return state; | ||
+ | } | ||
+ | |||
+ | private static Type< | ||
+ | StateSaverAndLoader:: | ||
+ | StateSaverAndLoader:: | ||
+ | null // Supposed to be an ' | ||
+ | ); | ||
+ | |||
+ | public static StateSaverAndLoader getServerState(MinecraftServer server) { | ||
+ | // (Note: arbitrary choice to use ' | ||
+ | PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); | ||
+ | |||
+ | // The first time the following ' | ||
+ | // stores it inside the ' | ||
+ | // ' | ||
+ | StateSaverAndLoader state = persistentStateManager.getOrCreate(type, | ||
+ | |||
+ | // If state is not marked dirty, when Minecraft closes, ' | ||
+ | // Technically it's ' | ||
+ | // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to ' | ||
+ | // Besides, it's literally just setting a bool to true, and the only time there' | ||
+ | // 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(), | ||
+ | |||
+ | return playerState; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 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 | ||
+ | |||
+ | Just remember if you add new fields to '' | ||
+ | |||
+ | ====== 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 '' | ||
+ | |||
+ | Modify your class which '' | ||
+ | |||
+ | <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 = " | ||
+ | |||
+ | public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, | ||
+ | |||
+ | public static final Identifier INITIAL_SYNC = new Identifier(MOD_ID, | ||
+ | |||
+ | @Override | ||
+ | public void onInitialize() { | ||
+ | ServerPlayConnectionEvents.JOIN.register((handler, | ||
+ | PlayerData playerState = StateSaverAndLoader.getPlayerState(handler.getPlayer()); | ||
+ | PacketByteBuf data = PacketByteBufs.create(); | ||
+ | data.writeInt(playerState.dirtBlocksBroken); | ||
+ | server.execute(() -> { | ||
+ | ServerPlayNetworking.send(handler.getPlayer(), | ||
+ | }); | ||
+ | }); | ||
+ | |||
+ | PlayerBlockBreakEvents.AFTER.register((world, | ||
+ | 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, | ||
+ | }); | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Then modify your class which '' | ||
+ | |||
+ | <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, | ||
+ | int totalDirtBlocksBroken = buf.readInt(); | ||
+ | playerData.dirtBlocksBroken = buf.readInt(); | ||
+ | |||
+ | client.execute(() -> { | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | }); | ||
+ | }); | ||
+ | |||
+ | ClientPlayNetworking.registerGlobalReceiver(ExampleMod.INITIAL_SYNC, | ||
+ | playerData.dirtBlocksBroken = buf.readInt(); | ||
+ | |||
+ | client.execute(() -> { | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | }); | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | As soon as you join the world/ | ||
+ | |||
+ | * Note: The '' | ||
+ | |||
+ | ==== 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:// | ||
+ | |||
+ | ====== More Involved Player Data ====== | ||
+ | |||
+ | And just for good measure, let's see an example of how our '' | ||
+ | |||
+ | Let's say this is our '' | ||
+ | |||
+ | <code java> | ||
+ | import java.util.ArrayList; | ||
+ | import java.util.HashMap; | ||
+ | import java.util.List; | ||
+ | |||
+ | public class PlayerData { | ||
+ | public int dirtBlocksBroken = 0; | ||
+ | |||
+ | public HashMap< | ||
+ | |||
+ | public List< | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | This would be our '' | ||
+ | |||
+ | <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< | ||
+ | |||
+ | @Override | ||
+ | public NbtCompound writeNbt(NbtCompound nbt) { | ||
+ | nbt.putInt(" | ||
+ | |||
+ | NbtCompound playersNbt = new NbtCompound(); | ||
+ | players.forEach((uuid, | ||
+ | NbtCompound playerNbt = new NbtCompound(); | ||
+ | |||
+ | playerNbt.putInt(" | ||
+ | |||
+ | playerNbt.putIntArray(" | ||
+ | |||
+ | NbtCompound fatigueTag = new NbtCompound(); | ||
+ | playerData.fatigue.forEach((foodID, | ||
+ | playerNbt.put(" | ||
+ | |||
+ | playersNbt.put(uuid.toString(), | ||
+ | }); | ||
+ | nbt.put(" | ||
+ | |||
+ | return nbt; | ||
+ | } | ||
+ | |||
+ | public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = tag.getInt(" | ||
+ | |||
+ | NbtCompound playersNbt = tag.getCompound(" | ||
+ | playersNbt.getKeys().forEach(key -> { | ||
+ | PlayerData playerData = new PlayerData(); | ||
+ | |||
+ | playerData.dirtBlocksBroken = playersNbt.getCompound(key).getInt(" | ||
+ | |||
+ | NbtCompound fatigueCompound = playersNbt.getCompound(key).getCompound(" | ||
+ | fatigueCompound.getKeys().forEach(s -> { | ||
+ | Integer foodID = Integer.valueOf(s); | ||
+ | int fatigueAmount = fatigueCompound.getInt(s); | ||
+ | playerData.fatigue.put(foodID, | ||
+ | }); | ||
+ | |||
+ | for (int oldCravings : playersNbt.getCompound(key).getIntArray(" | ||
+ | playerData.oldCravings.add(oldCravings); | ||
+ | } | ||
+ | |||
+ | UUID uuid = UUID.fromString(key); | ||
+ | state.players.put(uuid, | ||
+ | }); | ||
+ | |||
+ | return state; | ||
+ | } | ||
+ | |||
+ | private static Type< | ||
+ | StateSaverAndLoader:: | ||
+ | StateSaverAndLoader:: | ||
+ | null // Supposed to be an ' | ||
+ | ); | ||
+ | |||
+ | public static StateSaverAndLoader getServerState(MinecraftServer server) { | ||
+ | // (Note: arbitrary choice to use ' | ||
+ | PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); | ||
+ | |||
+ | // The first time the following ' | ||
+ | // stores it inside the ' | ||
+ | // ' | ||
+ | StateSaverAndLoader state = persistentStateManager.getOrCreate(type, | ||
+ | |||
+ | // If state is not marked dirty, when Minecraft closes, ' | ||
+ | // Technically it's ' | ||
+ | // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to ' | ||
+ | // Besides, it's literally just setting a bool to true, and the only time there' | ||
+ | // 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(), | ||
+ | |||
+ | return playerState; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Our classes which implement '' | ||
+ | |||
+ | <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, | ||
+ | int totalDirtBlocksBroken = buf.readInt(); | ||
+ | playerData.dirtBlocksBroken = buf.readInt(); | ||
+ | |||
+ | client.execute(() -> { | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | }); | ||
+ | }); | ||
+ | |||
+ | ClientPlayNetworking.registerGlobalReceiver(ExampleMod.INITIAL_SYNC, | ||
+ | playerData.dirtBlocksBroken = buf.readInt(); | ||
+ | |||
+ | client.execute(() -> { | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | }); | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | <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 = " | ||
+ | |||
+ | public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, | ||
+ | |||
+ | public static final Identifier INITIAL_SYNC = new Identifier(MOD_ID, | ||
+ | |||
+ | @Override | ||
+ | public void onInitialize() { | ||
+ | ServerPlayConnectionEvents.JOIN.register((handler, | ||
+ | PlayerData playerState = StateSaverAndLoader.getPlayerState(handler.getPlayer()); | ||
+ | PacketByteBuf data = PacketByteBufs.create(); | ||
+ | data.writeInt(playerState.dirtBlocksBroken); | ||
+ | server.execute(() -> { | ||
+ | ServerPlayNetworking.send(handler.getPlayer(), | ||
+ | }); | ||
+ | }); | ||
+ | |||
+ | PlayerBlockBreakEvents.AFTER.register((world, | ||
+ | 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, | ||
+ | }); | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </ |
tutorial/persistent_states.txt · Last modified: 2024/04/25 14:06 by mayaqq