tutorial:persistent_states
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revisionNext revisionBoth sides next revision | ||
tutorial:persistent_states [2022/12/21 04:44] – created jmanc3 | tutorial:persistent_states [2023/09/28 05:52] – Dropped un-needed DataFixType parameter and made type singleton jmanc3 | ||
---|---|---|---|
Line 1: | Line 1: | ||
====== Persistent State ====== | ====== Persistent State ====== | ||
- | Frequently we have information about the state of the world, or about the player, | + | Frequently |
<code java> | <code java> | ||
- | int furnacesCrafted | + | int dirtBlocksBroken |
</ | </ | ||
- | And it keeps track of how many times furnaces | + | And that the mod also keeps track of how many dirt blocks have been broken, |
<code java> | <code java> | ||
- | int totalFurnacesCrafted | + | int totalDirtBlocksBroken |
</ | </ | ||
- | 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' | + | How do we get fabric to save that information, or any other information that we want to keep (booleans, lists, strings) |
- | ===== Server-Side or Client-Side | + | ====== Simple Message ====== |
- | Before that, we have to decide who is keeping track of the data. The server or the client? In most cases, data should | + | First, since the data will be saved on the 'server', |
- | Using the furnaces crafted variable from before | + | Modify your class which '' |
- | If we were on a server and another | + | <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, | + | public static final String MOD_ID = " |
- | 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, |
- | Side-Note: | + | private Integer totalDirtBlocksBroken = 0; |
- | * Generally data the client is keeping track of should be transient | + | @Override |
+ | public void onInitialize() { | ||
+ | PlayerBlockBreakEvents.AFTER.register((world, | ||
+ | if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { | ||
+ | // Increment | ||
+ | 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 |
+ | MinecraftServer server = world.getServer(); | ||
- | ===== Long Term Storage | + | PacketByteBuf data = PacketByteBufs.create(); |
+ | data.writeInt(totalDirtBlocksBroken); | ||
- | So we've determined that the server | + | ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); |
+ | server.execute(() -> { | ||
+ | ServerPlayNetworking.send(playerEntity, | ||
+ | }); | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | * The '' | ||
+ | * To send a packet we use: '' | ||
- | We'll start by making a new class **ServerState** | + | Next modify your class which '' |
<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 | + | public |
- | | + | |
- | return nbt; | + | int totalDirtBlocksBroken |
- | } | + | |
- | + | client.player.sendMessage(Text.literal("Total dirt blocks broken: | |
- | public static ServerState createFromNbt(NbtCompound tag) { | + | }); |
- | | + | |
- | | + | |
- | | + | |
} | } | ||
} | } | ||
</ | </ | ||
- | You'll notice | + | * You can see the '' |
- | <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' |
- | boolean keepingTrack = true; | + | |
- | </code> | + | |
- | You have to make sure to update those two functions | + | The reason is that the number only ever lives in memory. It isn't saved anywhere. If it where, for instance |
+ | |||
+ | ====== Persistent State ====== | ||
+ | |||
+ | First make a new file '' | ||
<code java> | <code java> | ||
- | public class ServerState | + | 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 | ||
+ | |||
+ | public Integer totalDirtBlocksBroken = 0; | ||
- | int totalFurnacesCrafted = 0; | ||
- | | ||
- | boolean keepingTrack = true; | ||
- | | ||
@Override | @Override | ||
public NbtCompound writeNbt(NbtCompound nbt) { | public NbtCompound writeNbt(NbtCompound nbt) { | ||
- | nbt.putInt(" | + | nbt.putInt(" |
- | nbt.putBoolean(" | + | |
return nbt; | return nbt; | ||
- | } | ||
- | | ||
- | public static ServerState createFromNbt(NbtCompound tag) { | ||
- | ServerState serverState = new ServerState(); | ||
- | serverState.totalFurnacesCrafted = tag.getInt(" | ||
- | serverState.keepingTrack = tag.getBoolean(" | ||
- | return serverState; | ||
} | } | ||
} | } | ||
</ | </ | ||
- | | + | Note: '' |
- | Now we need to register it to the **PersistentStateManager** which will be in charge of reading | + | |
+ | |||
+ | Next add the following | ||
<code java> | <code java> | ||
- | public class ServerState | + | public class StateSaverAndLoader |
- | // ... (What we' | + | // ... (Previously |
- | public static | + | public static |
- | | + | |
- | PersistentStateManager persistentStateManager | + | |
- | .getWorld(World.OVERWORLD).getPersistentStateManager(); | + | return state; |
- | | + | } |
- | | + | } |
- | // You need to use a unique string as the key. You should already have a MODID variable defined by you somewhere in your code. Use that. | + | </ |
- | ServerState serverState | + | |
- | ServerState:: | + | |
- | ServerState:: | + | |
- | | + | |
- | serverState.markDirty(); // YOU MUST DO THIS!!!! Or data wont be saved correctly. | + | This function does the opposite of '' |
- | + | ||
- | return | + | * 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) { | ||
+ | | ||
+ | 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 | ||
} | } | ||
} | } | ||
</ | </ | ||
- | One thing to note is the line **serverState.markDirty(); | + | Your '' |
<code java> | <code java> | ||
- | ServerState serverState | + | import net.minecraft.nbt.NbtCompound; |
- | System.out.println("The server | + | 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 | ||
+ | |||
+ | @Override | ||
+ | public NbtCompound writeNbt(NbtCompound nbt) { | ||
+ | nbt.putInt(" | ||
+ | | ||
+ | } | ||
+ | |||
+ | public static StateSaverAndLoader createFromNbt(NbtCompound tag) { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken" | ||
+ | return state; | ||
+ | } | ||
+ | |||
+ | private static Type< | ||
+ | StateSaverAndLoader:: | ||
+ | StateSaverAndLoader:: | ||
+ | null // Supposed to be an ' | ||
+ | ); | ||
+ | |||
+ | public static StateSaverAndLoader getServerState(MinecraftServer | ||
+ | // (Note: arbitrary choice to use 'World.OVERWORLD' | ||
+ | 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' | + | You' |
<code java> | <code java> | ||
- | public class OurMod | + | import net.fabricmc.api.ModInitializer; |
- | public static final String | + | 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 | ||
+ | |||
+ | public static final String | ||
+ | |||
+ | public static final Identifier DIRT_BROKEN | ||
@Override | @Override | ||
public void onInitialize() { | public void onInitialize() { | ||
- | | + | |
- | | + | |
- | | + | |
+ | // Increment the amount of dirt blocks that have been broken | ||
+ | serverState.totalDirtBlocksBroken += 1; | ||
- | | + | |
- | PacketByteBuf data = PacketByteBufs.create(); | + | MinecraftServer server = world.getServer(); |
- | data.writeInt(serverState.totalFurnacesCrafted); | + | |
- | | + | |
+ | data.writeInt(serverState.totalDirtBlocksBroken); | ||
+ | |||
+ | ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); | ||
+ | server.execute(() -> { | ||
+ | ServerPlayNetworking.send(playerEntity, | ||
+ | }); | ||
+ | } | ||
}); | }); | ||
} | } | ||
Line 150: | Line 270: | ||
</ | </ | ||
- | --------- | + | 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 '' |
+ | ====== Player | ||
- | Let's say our example mod we've been making so far also wants to keep track of how many furnaces have been crafted by each user individually. That type of information should be associated to the player. How do we do that? | + | We can store player-specific data by extending what we already wrote. |
- | First let's create | + | First write a new class '' |
+ | |||
+ | * Extremely important note: Since we'll be creating a HashMap which seemingly stores '' | ||
<code java> | <code java> | ||
- | public | + | class PlayerData |
- | int furnacesCrafted | + | |
} | } | ||
</ | </ | ||
- | 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, |
+ | |||
+ | Next, we'll modify | ||
+ | |||
+ | <code java> | ||
+ | // ... (Previous imports) | ||
+ | import java.util.HashMap; | ||
+ | import java.util.UUID; | ||
+ | |||
+ | public class StateSaverAndLoader extends PersistentState { | ||
- | <code java [highlight_lines_extra=" | + | |
- | public | + | |
- | int totalFurnacesCrafted | + | |
| | ||
- | | + | public HashMap< |
- | + | ||
- | | + | // ... (Rest of the code) |
+ | |||
+ | } | ||
</ | </ | ||
- | Every player has a unique | + | Note: We create |
- | 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 |
- | 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(), | ||
+ | |||
+ | return playerState; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | | ||
+ | |||
+ | 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, | ||
- | <code java [highlight_lines_extra=" | ||
@Override | @Override | ||
- | public | + | public |
- | | + | |
- | NbtCompound playersNbtCompound = new NbtCompound(); | + | |
- | players.forEach((UUID, playerSate) -> { | + | StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer()); |
- | | + | // 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(" | + | 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(playerEntity, DIRT_BROKEN, | ||
+ | }); | ||
+ | } | ||
}); | }); | ||
- | nbt.put(" | + | } |
+ | } | ||
+ | </ | ||
- | // Putting | + | You'll also have to modify |
- | nbt.putInt(" | + | |
- | nbt.putBoolean(" | + | |
- | 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, | ||
+ | int totalDirtBlocksBroken = buf.readInt(); | ||
+ | int playerSpecificDirtBlocksBroken = buf.readInt(); | ||
+ | |||
+ | client.execute(() -> { | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | }); | ||
+ | }); | ||
} | } | ||
+ | } | ||
</ | </ | ||
- | The **createFromNbt** is where the program passes you an NbtCompound | + | If you ran the client now, it would seem as if everything is working, but we are forgetting a crucial step: We haven' |
- | <code java [highlight_lines_extra=" | + | 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> |
- | | + | public class StateSaverAndLoader extends PersistentState |
- | playersTag.getKeys().forEach(key -> { | + | |
- | PlayerState playerState = new PlayerState(); | + | |
- | playerState.furnacesCrafted = playersTag.getCompound(key).getInt(" | + | // ... (Rest of code) |
- | UUID uuid = UUID.fromString(key); | + | @Override |
- | | + | public NbtCompound writeNbt(NbtCompound nbt) { |
+ | nbt.putInt(" | ||
+ | |||
+ | NbtCompound playersNbt | ||
+ | players.forEach((uuid, | ||
+ | NbtCompound playerNbt = new NbtCompound(); | ||
+ | |||
+ | | ||
+ | |||
+ | playersNbt.put(uuid.toString(), playerNbt); | ||
}); | }); | ||
+ | nbt.put(" | ||
- | | + | |
- | | + | } |
+ | |||
+ | public static StateSaverAndLoader createFromNbt(NbtCompound tag) { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken | ||
+ | |||
+ | | ||
+ | playersNbt.getKeys().forEach(key -> { | ||
+ | PlayerData playerData = new PlayerData(); | ||
+ | |||
+ | playerData.dirtBlocksBroken = playersNbt.getCompound(key).getInt(" | ||
+ | |||
+ | UUID uuid = UUID.fromString(key); | ||
+ | state.players.put(uuid, | ||
+ | }); | ||
- | return | + | return |
} | } | ||
+ | |||
+ | // ... (Rest of code) | ||
+ | |||
+ | } | ||
</ | </ | ||
- | Finally we add a utility function which takes a player, looks through the server state 'players' | + | The final '' |
- | <code java [highlight_lines_extra="14,15,16,17,18,19,20,21"]> | + | <code java> |
- | public static | + | 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 | ||
+ | |||
+ | public HashMap< | ||
+ | |||
+ | @Override | ||
+ | public NbtCompound writeNbt(NbtCompound nbt) { | ||
+ | nbt.putInt(" | ||
+ | |||
+ | NbtCompound playersNbt = new NbtCompound(); | ||
+ | players.forEach((uuid, playerData) -> { | ||
+ | NbtCompound playerNbt = new NbtCompound(); | ||
+ | |||
+ | playerNbt.putInt(" | ||
+ | |||
+ | playersNbt.put(uuid.toString(), playerNbt); | ||
+ | }); | ||
+ | nbt.put(" | ||
+ | |||
+ | return nbt; | ||
+ | } | ||
+ | |||
+ | public static StateSaverAndLoader createFromNbt(NbtCompound tag) { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken" | ||
+ | |||
+ | 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 | ||
+ | // (Note: arbitrary choice to use ' | ||
PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); | PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); | ||
- | | + | |
- | | + | // stores it inside the ' |
- | | + | // ' |
- | " | + | |
- | | + | |
+ | | ||
+ | // 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 | + | return |
} | } | ||
- | public static | + | public static |
- | | + | |
// 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 | ||
- | | + | |
return playerState; | return playerState; | ||
Line 255: | Line 539: | ||
</ | </ | ||
- | How do I use this? Using our earlier example where we passed each player | + | Running the client now, all our player-specific data is correctly saved. |
- | <code java [highlight_lines_extra=" | + | * Note: each time you restart the minecraft client with fabric, you're assigned a new UUID, so it may seem like it's not working, but that's just because of the developer environment. (If you run the fabric multiplayer server, ' |
- | public class OurMod | + | |
- | public static final String | + | 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 | ||
+ | |||
+ | public static final String | ||
+ | |||
+ | public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, | ||
+ | |||
+ | public static final Identifier INITIAL_SYNC = new Identifier(MOD_ID, | ||
@Override | @Override | ||
public void onInitialize() { | public void onInitialize() { | ||
ServerPlayConnectionEvents.JOIN.register((handler, | ServerPlayConnectionEvents.JOIN.register((handler, | ||
- | | + | |
- | 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); | + | |
- | | + | 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); | ||
+ | | ||
+ | |||
+ | ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); | ||
+ | server.execute(() -> { | ||
+ | ServerPlayNetworking.send(playerEntity, | ||
+ | }); | ||
+ | } | ||
}); | }); | ||
} | } | ||
Line 277: | Line 608: | ||
</ | </ | ||
- | ----- | + | Then modify your class which '' |
- | ===== 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/ | + | public |
- | * 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 | + | public |
- | | + | |
- | + | int totalDirtBlocksBroken | |
- | PersistentStateManager persistentStateManager | + | |
- | .getWorld(World.OVERWORLD).getPersistentStateManager(); | + | |
- | ClientState clientState = persistentStateManager.getOrCreate( | + | client.execute(() -> { |
- | | + | |
- | | + | |
- | | + | }); |
+ | }); | ||
- | return clientState; | + | ClientPlayNetworking.registerGlobalReceiver(ExampleMod.INITIAL_SYNC, |
+ | playerData.dirtBlocksBroken = buf.readInt(); | ||
+ | |||
+ | client.execute(() -> { | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | }); | ||
+ | }); | ||
+ | } | ||
} | } | ||
</ | </ | ||
+ | As soon as you join the world/ | ||
- | ===== More Involved Player State ===== | + | * Note: The '' |
- | 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? | + | * Note: each time you restart the minecraft client with fabric, you're assigned a new UUID, so it may seem like it's not working, but that's just because of the developer environment. (If you run the fabric multiplayer server, ' |
- | Let's say this is our **PlayerState**: | + | ====== 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> | <code java> | ||
- | public class PlayerState { | + | import java.util.ArrayList; |
- | int furnacesCrafted = 0; | + | import java.util.HashMap; |
+ | import java.util.List; | ||
- | | + | public |
+ | public int dirtBlocksBroken | ||
+ | public HashMap< | ||
+ | |||
public List< | public List< | ||
} | } | ||
</ | </ | ||
- | This is how the **ServerState** should look. | + | This would be our '' |
<code java> | <code java> | ||
- | public class ServerState extends | + | 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 |
- | public | + | public |
- | ServerState serverState | + | |
- | NbtCompound playersTag = tag.getCompound(" | + | public HashMap< |
- | playersTag.getKeys().forEach(key -> { | + | |
- | PlayerState playerState | + | |
- | playerState.previousFoodAteID | + | @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) { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = tag.getInt(" | ||
+ | |||
+ | NbtCompound playersNbt = tag.getCompound(" | ||
+ | playersNbt.getKeys().forEach(key -> { | ||
+ | PlayerData playerData = new PlayerData(); | ||
+ | |||
+ | playerData.dirtBlocksBroken | ||
- | NbtCompound fatigueCompound = playersTag.getCompound(key).getCompound(" | + | NbtCompound fatigueCompound = playersNbt.getCompound(key).getCompound(" |
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); | ||
- | | + | |
}); | }); | ||
- | for (int oldCravings : playersTag.getCompound(key).getIntArray(" | + | for (int oldCravings : playersNbt.getCompound(key).getIntArray(" |
- | | + | |
} | } | ||
UUID uuid = UUID.fromString(key); | UUID uuid = UUID.fromString(key); | ||
- | | + | |
}); | }); | ||
- | | + | |
- | | + | } |
- | | + | 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(); | ||
+ | |||
+ | | ||
} | } | ||
- | | + | public |
- | | + | |
- | | + | |
- | players.forEach((UUID, playerData) -> { | + | |
- | NbtCompound playerDataAsNbt = new NbtCompound(); | + | PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData()); |
- | playerDataAsNbt.putInt(" | + | return playerState; |
+ | } | ||
+ | } | ||
+ | </ | ||
- | playerDataAsNbt.putIntArray(" | + | Our classes which implement '' |
- | NbtCompound fatigueTag = new NbtCompound(); | + | <code java> |
- | | + | import net.fabricmc.api.ClientModInitializer; |
- | | + | 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, | ||
+ | int totalDirtBlocksBroken = buf.readInt(); | ||
+ | playerData.dirtBlocksBroken = buf.readInt(); | ||
+ | |||
+ | client.execute(() -> { | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | client.player.sendMessage(Text.literal(" | ||
+ | }); | ||
}); | }); | ||
- | nbt.put(" | ||
- | | + | |
- | nbt.putBoolean(" | + | |
- | return nbt; | + | client.execute(() -> { |
+ | client.player.sendMessage(Text.literal(" | ||
+ | }); | ||
+ | }); | ||
} | } | ||
+ | } | ||
+ | </ | ||
- | public static ServerState getServerState(MinecraftServer server) { | ||
- | PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); | ||
- | ServerState serverState = persistentStateManager.getOrCreate( | + | <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; | ||
- | serverState.markDirty(); | + | public class ExampleMod implements ModInitializer { |
- | return serverState; | + | public static final String MOD_ID = " |
- | } | + | |
- | public static | + | public static |
- | ServerState serverState | + | |
- | // 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, " |
- | PlayerState playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerState()); | + | |
- | return | + | @Override |
+ | public void onInitialize() { | ||
+ | ServerPlayConnectionEvents.JOIN.register((handler, | ||
+ | PlayerData | ||
+ | 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