tutorial:persistent_states
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
tutorial:persistent_states [2023/04/10 11:42] – Removed more markDirty() calls lunathelemon | tutorial:persistent_states [2025/03/13 14:52] (current) – world.getServer() only once, harmonized MOD_ID lakazatong | ||
---|---|---|---|
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 as an example, here is a thought experiment that shows you what you should be thinking about when deciding: | + | For that, we need to define |
- | 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.minecraft.network.PacketByteBuf; | ||
+ | import net.minecraft.network.codec.PacketCodec; | ||
+ | import net.minecraft.network.codec.PacketCodecs; | ||
+ | import net.minecraft.network.packet.CustomPayload; | ||
+ | import net.minecraft.util.Identifier; | ||
- | - Client sends a request to the server for another players information. | + | public record DirtBrokenPayload(Integer totalDirtBlocksBroken) implements CustomPayload { |
- | - Server receives request and sends its own request to the player we want the information from. | + | public static final Identifier DIRT_BROKEN_ID = Identifier.of(MOD_ID, " |
- | - Client replies with requested data. | + | |
- | - Server receives it, trusting the the right data was sent, and replies back to the original client. | + | |
+ | | ||
+ | DirtBrokenPayload:: | ||
- | Had the server been keeping track of the furnaces crafted on the players behalf instead, when it received the request for that information, | + | @Override |
+ | public Id<? extends CustomPayload> | ||
+ | return 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. | + | Next, modify your class which '' |
- | Side-Note: | + | <code java> |
+ | import net.fabricmc.api.ModInitializer; | ||
+ | import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; | ||
+ | import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; | ||
+ | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; | ||
+ | import net.minecraft.block.Blocks; | ||
+ | import net.minecraft.server.MinecraftServer; | ||
+ | import net.minecraft.server.network.ServerPlayerEntity; | ||
- | * Generally data the client is keeping track of should be transient (disappears when the player logs off), and only when they log in (to the server or world), do we pass them that information back. If they need it, that is; it could be the case that they have no use for the data themselves and is just something the server needs to know for your mod to work. | + | public class ExampleMod implements ModInitializer { |
- | 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. | + | public static final String MOD_ID = " |
- | ===== Long Term Storage (Server) ===== | + | private Integer 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 '' | + | public void onInitialize() { |
+ | PayloadTypeRegistry.playS2C().register(DirtBrokenPayload.ID, | ||
- | We'll start by making a new class '' | + | PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> { |
+ | if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { | ||
+ | // Send a packet | ||
+ | MinecraftServer server = world.getServer(); | ||
+ | assert server != null; | ||
- | <code java> | + | // Increment the amount of dirt blocks that have been broken |
- | public class ServerState extends PersistentState { | + | |
- | int totalFurnacesCrafted | + | ServerPlayerEntity playerEntity |
- | + | | |
- | @Override | + | |
- | public NbtCompound writeNbt(NbtCompound nbt) { | + | ServerPlayNetworking.send(playerEntity, |
- | nbt.putInt(" | + | }); |
- | | + | } |
- | } | + | |
- | + | ||
- | public static ServerState createFromNbt(NbtCompound tag) { | + | |
- | | + | |
- | | + | |
- | | + | |
} | } | ||
} | } | ||
</ | </ | ||
+ | |||
+ | * The '' | ||
+ | * To send a packet we use: '' | ||
- | You'll notice the class implements the two methods | + | Finally, modify your class which '' |
<code java> | <code java> | ||
- | boolean keepingTrack | + | import net.fabricmc.api.ClientModInitializer; |
+ | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; | ||
+ | import net.minecraft.client.network.ClientPlayerEntity; | ||
+ | import net.minecraft.text.Text; | ||
+ | |||
+ | public class ExampleModClient implements ClientModInitializer { | ||
+ | private static void handleDirtBrokenPayload(DirtBrokenPayload payload, ClientPlayNetworking.Context context) { | ||
+ | ClientPlayerEntity player | ||
+ | assert player != null; | ||
+ | player.sendMessage(Text.literal(" | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void onInitializeClient() { | ||
+ | ClientPlayNetworking.registerGlobalReceiver(DirtBrokenPayload.ID, | ||
+ | } | ||
+ | } | ||
</ | </ | ||
- | You have to make sure to update those two functions | + | * 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 | ||
+ | |||
+ | ====== Persistent State ====== | ||
+ | |||
+ | First make a new file '' | ||
<code java> | <code java> | ||
- | public class ServerState | + | import net.minecraft.nbt.NbtCompound; |
+ | import net.minecraft.registry.RegistryWrapper; | ||
+ | import net.minecraft.server.MinecraftServer; | ||
+ | import net.minecraft.server.world.ServerWorld; | ||
+ | import net.minecraft.world.PersistentState; | ||
+ | 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, RegistryWrapper.WrapperLookup registries) { |
- | 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 '' | + | * '' |
+ | |||
+ | Next add the following functions to that same file: | ||
<code java> | <code java> | ||
- | public class ServerState | + | public class StateSaverAndLoader |
- | // ... (What we' | + | // ... (Previously |
- | public static | + | public static |
- | | + | |
- | | + | |
- | | + | 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 key. You should already have a MODID variable defined by you somewhere in your code. Use that. | + | |
- | | + | |
- | ServerState:: | + | |
- | ServerState:: | + | return |
- | " | + | |
- | + | ||
- | return | + | |
} | } | ||
} | } | ||
</ | </ | ||
- | In order to signal that the server should save the state of the object at the next write cycle you must call '' | + | First function does the opposite |
- | Failing | + | |
+ | * Note: how we pull out the int we stored earlier with '' | ||
+ | |||
+ | Second function refreshing | ||
+ | |||
+ | Now we just need to add one more utility function which hooks everything up together. This function | ||
<code java> | <code java> | ||
- | ServerState serverState = ServerState.getServerState(server); | + | public class StateSaverAndLoader extends PersistentState { |
- | System.out.println("The server has seen this many furnaces crafted: " + serverState.totalFurnacesCrafted); | + | |
- | serverState.markDirty(); | + | // ... (Previously written code) |
+ | |||
+ | private static final Type< | ||
+ | StateSaverAndLoader:: | ||
+ | StateSaverAndLoader:: | ||
+ | null // Supposed to be an ' | ||
+ | ); | ||
+ | |||
+ | public static StateSaverAndLoader | ||
+ | // (Note: arbitrary choice to use 'World.OVERWORLD' | ||
+ | ServerWorld serverWorld = server.getWorld(World.OVERWORLD); | ||
+ | assert serverWorld != null; | ||
+ | |||
+ | // The first time the following ' | ||
+ | // stores it inside the ' | ||
+ | // ' | ||
+ | StateSaverAndLoader state = serverWorld.getPersistentStateManager().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 notice we need to pass the '' | + | Your '' |
<code java> | <code java> | ||
- | public class OurMod implements ModInitializer | + | import net.minecraft.nbt.NbtCompound; |
- | public | + | import net.minecraft.registry.RegistryWrapper; |
+ | import net.minecraft.server.MinecraftServer; | ||
+ | import net.minecraft.server.world.ServerWorld; | ||
+ | import net.minecraft.world.PersistentState; | ||
+ | import net.minecraft.world.World; | ||
+ | |||
+ | public class StateSaverAndLoader extends PersistentState | ||
+ | |||
+ | public | ||
@Override | @Override | ||
+ | public NbtCompound writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registries) { | ||
+ | nbt.putInt(" | ||
+ | return nbt; | ||
+ | } | ||
+ | |||
+ | public static StateSaverAndLoader createFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = tag.getInt(" | ||
+ | return state; | ||
+ | } | ||
+ | |||
+ | public static StateSaverAndLoader createNew() { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = 0; | ||
+ | return state; | ||
+ | } | ||
+ | |||
+ | private static final Type< | ||
+ | StateSaverAndLoader:: | ||
+ | StateSaverAndLoader:: | ||
+ | null // Supposed to be an ' | ||
+ | ); | ||
+ | |||
+ | public static StateSaverAndLoader getServerState(MinecraftServer server) { | ||
+ | // (Note: arbitrary choice to use ' | ||
+ | ServerWorld serverWorld = server.getWorld(World.OVERWORLD); | ||
+ | assert serverWorld != null; | ||
+ | |||
+ | // The first time the following ' | ||
+ | // stores it inside the ' | ||
+ | // ' | ||
+ | StateSaverAndLoader state = serverWorld.getPersistentStateManager().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.PayloadTypeRegistry; | ||
+ | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; | ||
+ | import net.minecraft.block.Blocks; | ||
+ | import net.minecraft.server.MinecraftServer; | ||
+ | import net.minecraft.server.network.ServerPlayerEntity; | ||
+ | |||
+ | public class ExampleMod implements ModInitializer { | ||
+ | |||
+ | public static final String MOD_ID = " | ||
+ | |||
public void onInitialize() { | public void onInitialize() { | ||
- | | + | |
- | // You can see we use the function getServer() that's on the player. | + | |
- | ServerState serverState = ServerState.getServerState(handler.player.world.getServer()); | + | |
- | | + | PlayerBlockBreakEvents.AFTER.register((world, |
- | | + | if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) { |
- | data.writeInt(serverState.totalFurnacesCrafted); | + | |
- | ServerPlayNetworking.send(handler.player, NetworkingMessages.CRAFTED_FURNACES, | + | MinecraftServer server = world.getServer(); |
+ | | ||
+ | |||
+ | // Increment the amount of dirt blocks that have been broken | ||
+ | StateSaverAndLoader serverState | ||
+ | | ||
+ | |||
+ | ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); | ||
+ | | ||
+ | assert playerEntity != null; | ||
+ | | ||
+ | }); | ||
+ | } | ||
}); | }); | ||
} | } | ||
Line 150: | Line 302: | ||
</ | </ | ||
- | --------- | + | 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 '' | + | To simplify, |
- | <code java [highlight_lines_extra=" | + | Next, we'll modify the top of our '' |
- | public class ServerState | + | |
- | | + | <code java> |
+ | // ... (Previous imports) | ||
+ | import java.util.HashMap; | ||
+ | import java.util.UUID; | ||
+ | |||
+ | public class StateSaverAndLoader | ||
+ | |||
+ | | ||
| | ||
- | | + | public HashMap< |
- | + | ||
- | | + | // ... (Rest of the code) |
+ | |||
+ | } | ||
</ | </ | ||
- | Every player has a unique | + | Note: We create |
- | Next is to update the '' | + | Let's add a utility function |
- | The '' | + | <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, | ||
- | <code java [highlight_lines_extra=" | ||
@Override | @Override | ||
- | public | + | public |
- | | + | |
- | NbtCompound playersNbtCompound = new NbtCompound(); | + | |
- | players.forEach((UUID, playerSate) -> { | + | // Send a packet to the client |
- | | + | MinecraftServer server = world.getServer(); |
+ | assert server != null; | ||
+ | |||
+ | StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(server); | ||
+ | // 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); | + | 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 '' | + | 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 '' |
- | <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 '' | + | <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, RegistryWrapper.WrapperLookup registryLookup) { | ||
+ | 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 |
} | } | ||
+ | | ||
+ | public static StateSaverAndLoader createNew() { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = 0; | ||
+ | state.players = new HashMap<> | ||
+ | return state; | ||
+ | } | ||
+ | |||
+ | // ... (Rest of code) | ||
+ | |||
+ | } | ||
</ | </ | ||
- | Finally we add a utility function which takes a player, looks through the server state '' | + | 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, RegistryWrapper.WrapperLookup registryLookup) { | ||
+ | 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; | ||
+ | } | ||
+ | | ||
+ | public static | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken = 0; | ||
+ | state.players = new HashMap<> | ||
+ | return state; | ||
+ | } | ||
+ | |||
+ | private static Type< | ||
+ | StateSaverAndLoader:: | ||
+ | StateSaverAndLoader:: | ||
+ | null // Supposed to be an ' | ||
+ | ); | ||
+ | |||
+ | public static StateSaverAndLoader | ||
+ | // (Note: arbitrary choice to use ' | ||
PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); | PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager(); | ||
- | | + | |
- | | + | // stores it inside the ' |
- | | + | // ' |
- | " | + | StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID); |
- | + | ||
- | return | + | // 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 | ||
} | } | ||
- | 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 253: | Line 586: | ||
</ | </ | ||
- | 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="8,13"]> | + | ==== Important Caveat ==== |
- | public class OurMod | + | |
- | public static final String | + | * 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 | ||
+ | |||
+ | 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) { | ||
+ | // Send a packet to the client | ||
+ | MinecraftServer server = world.getServer(); | ||
+ | assert server != null; | ||
+ | |||
+ | StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(server); | ||
+ | // Increment the amount of dirt blocks that have been broken | ||
+ | serverState.totalDirtBlocksBroken += 1; | ||
+ | |||
+ | PlayerData playerState = StateSaverAndLoader.getPlayerState(player); | ||
+ | playerState.dirtBlocksBroken += 1; | ||
+ | |||
+ | PacketByteBuf data = PacketByteBufs.create(); | ||
+ | data.writeInt(serverState.totalDirtBlocksBroken); | ||
+ | | ||
+ | |||
+ | ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid()); | ||
+ | server.execute(() -> { | ||
+ | ServerPlayNetworking.send(playerEntity, | ||
+ | }); | ||
+ | } | ||
}); | }); | ||
} | } | ||
Line 275: | Line 658: | ||
</ | </ | ||
- | ----- | + | 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 '' | + | 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 '' | + | ==== Important Caveat ==== |
- | Let's say this is our '' | + | * 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> | <code java> | ||
Line 311: | Line 711: | ||
import java.util.List; | import java.util.List; | ||
- | public class PlayerState | + | public class PlayerData |
- | int furnacesCrafted | + | |
public HashMap< | public HashMap< | ||
+ | |||
public List< | public List< | ||
} | } | ||
</ | </ | ||
- | This is how the '' | + | This would be our '' |
<code java> | <code java> | ||
Line 333: | Line 733: | ||
import java.util.UUID; | import java.util.UUID; | ||
- | public class ServerState | + | public class StateSaverAndLoader |
- | int totalFurnacesCrafted = 0; | + | |
- | | + | |
- | public HashMap< | + | public HashMap< |
- | public | + | |
- | | + | |
+ | | ||
- | NbtCompound | + | NbtCompound |
- | | + | |
- | | + | |
- | | + | |
- | NbtCompound fatigueCompound = playersTag.getCompound(key).getCompound(" | + | |
+ | |||
+ | 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(" | ||
+ | |||
+ | | ||
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); | ||
- | | + | |
}); | }); | ||
- | | + | |
- | | + | } |
+ | |||
+ | public static StateSaverAndLoader createNew() { | ||
+ | StateSaverAndLoader state = new StateSaverAndLoader(); | ||
+ | state.totalDirtBlocksBroken | ||
+ | state.players = new HashMap<> | ||
+ | | ||
+ | } | ||
+ | |||
+ | private static Type< | ||
+ | StateSaverAndLoader:: | ||
+ | StateSaverAndLoader:: | ||
+ | null // Supposed to be an ' | ||
+ | ); | ||
+ | |||
+ | public static StateSaverAndLoader getServerState(MinecraftServer server) { | ||
+ | // (Note: arbitrary choice to use 'World.OVERWORLD' | ||
+ | PersistentStateManager persistentStateManager | ||
+ | |||
+ | // 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 | + | return |
} | } | ||
- | | + | 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; | ||
- | return serverState; | + | public class ExampleMod implements ModInitializer { |
- | } | + | |
- | 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 DIRT_BROKEN = new Identifier(MOD_ID, " |
- | PlayerState playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerState()); | + | |
- | return | + | public static final Identifier INITIAL_SYNC = new Identifier(MOD_ID, |
+ | |||
+ | @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) { | ||
+ | // Send a packet to the client | ||
+ | MinecraftServer server = world.getServer(); | ||
+ | assert server != null; | ||
+ | |||
+ | StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(server); | ||
+ | // Increment the amount of dirt blocks that have been broken | ||
+ | serverState.totalDirtBlocksBroken += 1; | ||
+ | |||
+ | PlayerData playerState = StateSaverAndLoader.getPlayerState(player); | ||
+ | playerState.dirtBlocksBroken += 1; | ||
+ | |||
+ | 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.1681126924.txt.gz · Last modified: 2023/04/10 11:42 by lunathelemon