User Tools

Site Tools


zh_cn:tutorial:persistent_states

This is an old revision of the document!


状态持久化

通常情况下,我们的模组中带有与玩家相关的信息,或是在玩家死亡、服务器重启时我们希望保留的世界信息。
现在我们举个例子,我们的模组记录着一个玩家挖掘了多少泥土方块:

int dirtBlocksBroken = 0;

相应地,模组同时记录了服务器侧总计被挖掘了多少泥土方块:

int totalDirtBlocksBroken = 0;

那么,我们如何能让 Fabric 保存这些信息,或者其他我们想保存的模组或游戏数据,以供下次玩家加载世界或重新登录时能读取到这些信息?

简单的消息发送——数据包

首先,既然我们的游戏保存在逻辑服务器上,那么我们就让服务器在检测到玩家挖掘泥土方块时向玩家发送一个数据包,并将其显示在玩家的聊天区内。
(注:即便是单人游戏也有一个逻辑服务器在运行。关于逻辑服务器的术语细节,请参见服务器和客户端术语一节。)

将模组内实现了 ModInitializer 的类做如下改动:

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 = "您的MOD_ID";
 
    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken");
 
    private Integer totalDirtBlocksBroken = 0;
 
    @Override
    public void onInitialize() {
        // 注册一个方块挖掘事件监听器
        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> {
            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) {
                // 当泥土方块被挖掘时增加计数
                totalDirtBlocksBroken += 1;
 
                // 向客户端发送数据包
                MinecraftServer server = world.getServer();
 
                PacketByteBuf data = PacketByteBufs.create();
                data.writeInt(totalDirtBlocksBroken);
 
                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid());
                server.execute(() -> {
                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data);
                });
            }
        });
    }
}
  • 注意:MOD_ID 务必是唯一的。开发者通常情况下在这里填入模组名称。您可以自由改动,但请注意您只能使用小写字母(a-z)和下划线(_),否则将会报错。
    • 我们使用 ServerPlayNetworking.send() 方法来发送数据包。方法需要三个参数:
      • ServerPlayerEntity:指定要哪个玩家的游戏客户端接收数据包;
      • Identifer:这是你发送数据包要使用到的通道名称。
      • PacketByteBuf:这是你要发送的数据包内容,您可以在里面放入任意数据。在本例中,我们将服务器侧被挖掘的泥土方块总数放入数据包中。

接下来,将实现了 ClientModInitializer 的类做如下改动:

import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.text.Text; 
 
public class ExampleModClient implements ClientModInitializer {
 
    @Override
    public void onInitializeClient() {
        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (client, handler, buf, responseSender) -> {
            int totalDirtBlocksBroken = buf.readInt();
            client.execute(() -> {
                client.player.sendMessage(Text.literal("总计被挖掘的泥土方块数:" + totalDirtBlocksBroken));
            });
        });
    }
}
  • 显然地,这个过程中我们所使用的通道名 Identifier 与服务器侧是严格一致的。由于我们使用了 writeInt() 方法写入数据,相应地,我们就要使用 buf.readInt() 方法来读取我们在 PackedByteBuf 中存入的数据。

现在,如果您运行模组并创建世界,您在挖掘泥土或草方块时就会收到消息提示。您每挖掘一次,数值就应当加一。您甚至会注意到即使退出世界重新进入,计数也停在您退出的那一刻。
但这其实是一种误判。如果您完全退出游戏(终止 Minecraft 实例)并重新打开,您会发现计数归零了。
那为什么计数器没有保持在原本的数值呢?

这是因为数值只是暂存于系统内存(RAM)中,并没有写入到本地文件。
那么,如果我们把上次游戏结束时的计数写入到了本地(如硬盘某处),我们就可以在游戏启动时读取并加载它,作为计数器的初始值。这样一来我们就可以继续统计下去。
但是,我们必须要做到这一点:数值要在游戏关闭时保存,在游戏启动时读取并加载。
想要达到这一目的,方法千千万,不过我们在这里使用 Minecraft 提供给我们的方法:实现一个扩展了 'PersistentState' 的类。

状态持久化

首先,我们在项目目录新建一个名为 StateSaverAndLoader.java 的文件,它需要实现 ModInitializer 类:

import net.minecraft.nbt.NbtCompound;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.PersistentState;
import net.minecraft.world.PersistentStateManager;
import net.minecraft.world.World;
 
public class StateSaverAndLoader extends PersistentState {
 
    public Integer totalDirtBlocksBroken = 0;
 
    @Override
    public NbtCompound writeNbt(NbtCompound nbt) {
        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken);
        return nbt;
    }
}

注:在扩展 PersistentState 类时,必须实现 writeNbt。从功能上讲,我们通过 NbtCompound 将我们要存储到本地的数据进行打包。在本例中,我们将先前创建的 “public Integer totalDirtBlocksBroken” 移入了这个文件。

  • NbtCompound 不能直接保存 Integers. It has functions for strings, arrays, bools, floats, importantly other NbtCompound's as you'll see soon enough, and even arbitrary bytes. That means, if you want to store some SuperCustomClass, what you should do is create a new NbtCompound and pack that new NbtCompound with the fields of your SuperCustomClass and then store it in the main NbtCompound you get passed in. (We're about to do just that!)

Next add the following function to that same file:

public class StateSaverAndLoader extends PersistentState {
 
    // ... (Previously written code)
 
    public static StateSaverAndLoader createFromNbt(NbtCompound tag) {
        StateSaverAndLoader state = new StateSaverAndLoader();
        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken");
        return state;
    }
}

This function does the opposite of writeNbt. It takes in an NbtCompound (the same one we wrote in writeNbt), creates a brand new StateSaverAndLoader and stuffs it with the data inside the NbtCompound.

  • Note: how we pull out the int we stored earlier with getInt and how the string we pass in is the same one we used in writeNbt.

Now we just need to add one more utility function which hooks everything up together. This function will take a MinecraftServer and from it, get the PersistentStateManager. PersistentStateManager has a function getOrCreate which will use our MOD_ID as a key to see if it has an instance of our StateSaverAndLoader or if it needs to create one. If it needs to create one, it'll call the function we just wrote createFromNbt passing in the previously saved-to-disk NbtCompound. Ultimately the function returns the StateSaverAndLoader for the given MinecraftServer.

public class StateSaverAndLoader extends PersistentState {
 
    // ... (Previously written code)
 
    private static Type<StateSaverAndLoader> type = new Type<>(
            StateSaverAndLoader::new, // If there's no 'StateSaverAndLoader' yet create one
            StateSaverAndLoader::createFromNbt, // If there is a 'StateSaverAndLoader' NBT, parse it with 'createFromNbt'
            null // Supposed to be an 'DataFixTypes' enum, but we can just pass null
    );
 
    public static StateSaverAndLoader getServerState(MinecraftServer server) {
        // (Note: arbitrary choice to use 'World.OVERWORLD' instead of 'World.END' or 'World.NETHER'.  Any work)
        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager();
 
        // The first time the following 'getOrCreate' function is called, it creates a brand new 'StateSaverAndLoader' and
        // stores it inside the 'PersistentStateManager'. The subsequent calls to 'getOrCreate' pass in the saved
        // 'StateSaverAndLoader' NBT on disk to our function 'StateSaverAndLoader::createFromNbt'.
        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID);
 
        // If state is not marked dirty, when Minecraft closes, 'writeNbt' won't be called and therefore nothing will be saved.
        // Technically it's 'cleaner' if you only mark state as dirty when there was actually a change, but the vast majority
        // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to 'markDirty' for them.
        // Besides, it's literally just setting a bool to true, and the only time there's a 'cost' is when the file is written to disk when
        // there were no actual change to any of the mods state (INCREDIBLY RARE).
        state.markDirty();
 
        return state;
    }
}

Your StateSaverAndLoader file should look as follows:

import net.minecraft.nbt.NbtCompound;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.PersistentState;
import net.minecraft.world.PersistentStateManager;
import net.minecraft.world.World;
 
public class StateSaverAndLoader extends PersistentState {
 
    public Integer totalDirtBlocksBroken = 0;
 
    @Override
    public NbtCompound writeNbt(NbtCompound nbt) {
        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken);
        return nbt;
    }
 
    public static StateSaverAndLoader createFromNbt(NbtCompound tag) {
        StateSaverAndLoader state = new StateSaverAndLoader();
        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken");
        return state;
    }
 
    private static Type<StateSaverAndLoader> type = new Type<>(
            StateSaverAndLoader::new, // If there's no 'StateSaverAndLoader' yet create one
            StateSaverAndLoader::createFromNbt, // If there is a 'StateSaverAndLoader' NBT, parse it with 'createFromNbt'
            null // Supposed to be an 'DataFixTypes' enum, but we can just pass null
    );
 
    public static StateSaverAndLoader getServerState(MinecraftServer server) {
        // (Note: arbitrary choice to use 'World.OVERWORLD' instead of 'World.END' or 'World.NETHER'.  Any work)
        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager();
 
        // The first time the following 'getOrCreate' function is called, it creates a brand new 'StateSaverAndLoader' and
        // stores it inside the 'PersistentStateManager'. The subsequent calls to 'getOrCreate' pass in the saved
        // 'StateSaverAndLoader' NBT on disk to our function 'StateSaverAndLoader::createFromNbt'.
        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID);
 
        // If state is not marked dirty, when Minecraft closes, 'writeNbt' won't be called and therefore nothing will be saved.
        // Technically it's 'cleaner' if you only mark state as dirty when there was actually a change, but the vast majority
        // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to 'markDirty' for them.
        // Besides, it's literally just setting a bool to true, and the only time there's a 'cost' is when the file is written to disk when
        // there were no actual change to any of the mods state (INCREDIBLY RARE).
        state.markDirty();
 
        return state;
    }
}

You'll also have to update your class which implements ModInitializer so that it's as follows:

import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.block.Blocks;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
 
public class ExampleMod implements ModInitializer {
 
    public static final String MOD_ID = "your_unique_mod_id_change_me_please";
 
    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken");
 
    @Override
    public void onInitialize() {
        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> {
            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) {
                StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer());
                // Increment the amount of dirt blocks that have been broken
                serverState.totalDirtBlocksBroken += 1;
 
                // Send a packet to the client
                MinecraftServer server = world.getServer();
 
                PacketByteBuf data = PacketByteBufs.create();
                data.writeInt(serverState.totalDirtBlocksBroken);
 
                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid());
                server.execute(() -> {
                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data);
                });
            }
        });
    }
}

If you run your game now, you should see the counter going up, but now, if you fully close Minecraft, and open it again, you should see the number keeps increasing from where it left off.

What you might or might not expect is that totalDirtBlocksBroken is not specific to a player, that is, if you made a server with this mod, and a few people were running around breaking dirt blocks, all of them would increment that same number. This is fine for certain types of data we'd like to store for our mods, but most of the time, we want to store player-specific data. For instance, as we mentioned at the start of this article, what if we would like to store how many dirt blocks any specific player has broken?

Player Specific Persistent State

We can store player-specific data by extending what we already wrote.

First write a new class PlayerData.java (again placing it in the same folder as our class which implements ModInitializer).

  • Extremely important note: Since we'll be creating a HashMap which seemingly stores PlayerData's, it'll be tempting when you want to know some PlayerData client-side to use that hashmap, but, you won't be able to, because this data only exists on the server. If you want the client to see some or all the player's data. You'll have to send it through a packet. (We'll create an INITIAL_SYNC packet later which we will use to demonstrate this).
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 PlayerData (and it's expected that you will).

Next, we'll modify the top of our StateSaverAndLoader.java class as follows:

// ... (Previous imports)
import java.util.HashMap;
import java.util.UUID;
 
public class StateSaverAndLoader extends PersistentState {
 
    public Integer totalDirtBlocksBroken = 0;
 
    public HashMap<UUID, PlayerData> players = new HashMap<>();
 
    // ... (Rest of the code)
 
}

Note: We create a HashMap of UUID's to PlayereData's. If you don't know what a hashmap does: you give them a 'key', in our case a UUID and they give you back something, in our case PlayerData. The reason we use UUID's is because every player that connects to our 'server' has a unique UUID that only they are associated with. This lets us differentiate between different players and lets us 'pull' the right data for them. (Or create it if it doesn't exist yet).

Let's add a utility function to StateSaverAndLoader which will take a LivingEntity and return the associated PlayerData in our 'HashMap'.

public class StateSaverAndLoader extends PersistentState {
 
    // ... (Previously written code)
 
    public static PlayerData getPlayerState(LivingEntity player) {
        StateSaverAndLoader serverState = getServerState(player.getWorld().getServer());
 
        // Either get the player by the uuid, or we don't have data for him yet, make a new player state
        PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData());
 
        return playerState;
    }
}
  • If our StateSaverAndLoader has never seen the UUID before then it makes sense that our HashMap won't return anything. That's why we use the convenience function computeIfAbsent so that in the case where that UUID doesn't exist in the hashmap yet, it will automatically create a new PlayerData and puts it in our hashmap, using that players specific UUID as the key so that it can be retrieved later.

Now update the class which implements ModInitializer as follows:

import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.block.Blocks;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
 
public class ExampleMod implements ModInitializer {
 
    public static final String MOD_ID = "your_unique_mod_id_change_me_please";
 
    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken");
 
    @Override
    public void onInitialize() {
        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> {
            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) {
                StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer());
                // Increment the amount of dirt blocks that have been broken
                serverState.totalDirtBlocksBroken += 1;
 
                PlayerData playerState = StateSaverAndLoader.getPlayerState(player);
                playerState.dirtBlocksBroken += 1;
 
                // Send a packet to the client
                MinecraftServer server = world.getServer();
 
                PacketByteBuf data = PacketByteBufs.create();
                data.writeInt(serverState.totalDirtBlocksBroken);
                data.writeInt(playerState.dirtBlocksBroken);
 
                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid());
                server.execute(() -> {
                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data);
                });
            }
        });
    }
}

You'll also have to modify the class which implements ClientModInitializer as follows:

import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.text.Text;
 
public class ExampleModClient implements ClientModInitializer {
 
    @Override
    public void onInitializeClient() {
        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (client, handler, buf, responseSender) -> {
            int totalDirtBlocksBroken = buf.readInt();
            int playerSpecificDirtBlocksBroken = buf.readInt();
 
            client.execute(() -> {
                client.player.sendMessage(Text.literal("Total dirt blocks broken: " + totalDirtBlocksBroken));
                client.player.sendMessage(Text.literal("Player specific dirt blocks broken: " + playerSpecificDirtBlocksBroken));
            });
        });
    }
}

If you ran the client now, it would seem as if everything is working, but we are forgetting a crucial step: We haven't updated our writeNbt and createFromNbt to save and load our hashmap.

The updated functions are as follows:

public class StateSaverAndLoader extends PersistentState {
 
    // ... (Rest of code)
 
    @Override
    public NbtCompound writeNbt(NbtCompound nbt) {
        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken);
 
        NbtCompound playersNbt = new NbtCompound();
        players.forEach((uuid, playerData) -> {
            NbtCompound playerNbt = new NbtCompound();
 
            playerNbt.putInt("dirtBlocksBroken", playerData.dirtBlocksBroken);
 
            playersNbt.put(uuid.toString(), playerNbt);
        });
        nbt.put("players", playersNbt);
 
        return nbt;
    }
 
    public static StateSaverAndLoader createFromNbt(NbtCompound tag) {
        StateSaverAndLoader state = new StateSaverAndLoader();
        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken");
 
        NbtCompound playersNbt = tag.getCompound("players");
        playersNbt.getKeys().forEach(key -> {
            PlayerData playerData = new PlayerData();
 
            playerData.dirtBlocksBroken = playersNbt.getCompound(key).getInt("dirtBlocksBroken");
 
            UUID uuid = UUID.fromString(key);
            state.players.put(uuid, playerData);
        });
 
        return state;
    }
 
    // ... (Rest of code)
 
}

The final StateSaverAndLoader should be as follows:

import net.minecraft.entity.LivingEntity;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.PersistentState;
import net.minecraft.world.PersistentStateManager;
import net.minecraft.world.World;
 
import java.util.HashMap;
import java.util.UUID;
 
public class StateSaverAndLoader extends PersistentState {
 
    public Integer totalDirtBlocksBroken = 0;
 
    public HashMap<UUID, PlayerData> players = new HashMap<>();
 
    @Override
    public NbtCompound writeNbt(NbtCompound nbt) {
        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken);
 
        NbtCompound playersNbt = new NbtCompound();
        players.forEach((uuid, playerData) -> {
            NbtCompound playerNbt = new NbtCompound();
 
            playerNbt.putInt("dirtBlocksBroken", playerData.dirtBlocksBroken);
 
            playersNbt.put(uuid.toString(), playerNbt);
        });
        nbt.put("players", playersNbt);
 
        return nbt;
    }
 
    public static StateSaverAndLoader createFromNbt(NbtCompound tag) {
        StateSaverAndLoader state = new StateSaverAndLoader();
        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken");
 
        NbtCompound playersNbt = tag.getCompound("players");
        playersNbt.getKeys().forEach(key -> {
            PlayerData playerData = new PlayerData();
 
            playerData.dirtBlocksBroken = playersNbt.getCompound(key).getInt("dirtBlocksBroken");
 
            UUID uuid = UUID.fromString(key);
            state.players.put(uuid, playerData);
        });
 
        return state;
    }
 
    private static Type<StateSaverAndLoader> type = new Type<>(
            StateSaverAndLoader::new, // If there's no 'StateSaverAndLoader' yet create one
            StateSaverAndLoader::createFromNbt, // If there is a 'StateSaverAndLoader' NBT, parse it with 'createFromNbt'
            null // Supposed to be an 'DataFixTypes' enum, but we can just pass null
    );
 
    public static StateSaverAndLoader getServerState(MinecraftServer server) {
        // (Note: arbitrary choice to use 'World.OVERWORLD' instead of 'World.END' or 'World.NETHER'.  Any work)
        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager();
 
        // The first time the following 'getOrCreate' function is called, it creates a brand new 'StateSaverAndLoader' and
        // stores it inside the 'PersistentStateManager'. The subsequent calls to 'getOrCreate' pass in the saved
        // 'StateSaverAndLoader' NBT on disk to our function 'StateSaverAndLoader::createFromNbt'.
        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID);
 
        // If state is not marked dirty, when Minecraft closes, 'writeNbt' won't be called and therefore nothing will be saved.
        // Technically it's 'cleaner' if you only mark state as dirty when there was actually a change, but the vast majority
        // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to 'markDirty' for them.
        // Besides, it's literally just setting a bool to true, and the only time there's a 'cost' is when the file is written to disk when
        // there were no actual change to any of the mods state (INCREDIBLY RARE).
        state.markDirty();
 
        return state;
    }
 
    public static PlayerData getPlayerState(LivingEntity player) {
        StateSaverAndLoader serverState = getServerState(player.getWorld().getServer());
 
        // Either get the player by the uuid, or we don't have data for him yet, make a new player state
        PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData());
 
        return playerState;
    }
}

Running the client now, all our player-specific data is correctly saved.

  • 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, 'Minecraft Server' and use authme, you could verify it does indeed work as it's supposed to.)

Just remember if you add new fields to PlayerData or StateSaveAndLoader you need to correctly do the work of writing and loading those fields in the writeNbt and createFromNbt functions always. If you forget this step, your data won't be properly saved or loaded from disk.

Initial Sync

What if it's important for our mod that as soon as a player joins they receive some or all the PlayerData associated with them? For this, we will crate a new packet INITIAL_SYNC which will send the player, their specific player data as soon as they join the world.

Modify your class which implements ModInitializer as follows:

import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.block.Blocks;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
 
public class ExampleMod implements ModInitializer {
 
    public static final String MOD_ID = "your_unique_mod_id_change_me_please";
 
    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken");
 
    public static final Identifier INITIAL_SYNC = new Identifier(MOD_ID, "initial_sync");
 
    @Override
    public void onInitialize() {
        ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
            PlayerData playerState = StateSaverAndLoader.getPlayerState(handler.getPlayer());
            PacketByteBuf data = PacketByteBufs.create();
            data.writeInt(playerState.dirtBlocksBroken);
            server.execute(() -> {
                ServerPlayNetworking.send(handler.getPlayer(), INITIAL_SYNC, data);
            });
        });
 
        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> {
            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) {
                StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer());
                // Increment the amount of dirt blocks that have been broken
                serverState.totalDirtBlocksBroken += 1;
 
                PlayerData playerState = StateSaverAndLoader.getPlayerState(player);
                playerState.dirtBlocksBroken += 1;
 
                // Send a packet to the client
                MinecraftServer server = world.getServer();
 
                PacketByteBuf data = PacketByteBufs.create();
                data.writeInt(serverState.totalDirtBlocksBroken);
                data.writeInt(playerState.dirtBlocksBroken);
 
                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid());
                server.execute(() -> {
                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data);
                });
            }
        });
    }
}

Then modify your class which implements ClientModInitializer as follows:

import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.text.Text;
 
public class ExampleModClient implements ClientModInitializer {
 
    public static PlayerData playerData = new PlayerData();
 
    @Override
    public void onInitializeClient() {
        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (client, handler, buf, responseSender) -> {
            int totalDirtBlocksBroken = buf.readInt();
            playerData.dirtBlocksBroken = buf.readInt();
 
            client.execute(() -> {
                client.player.sendMessage(Text.literal("Total dirt blocks broken: " + totalDirtBlocksBroken));
                client.player.sendMessage(Text.literal("Player specific dirt blocks broken: " + playerData.dirtBlocksBroken));
            });
        });
 
        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.INITIAL_SYNC, (client, handler, buf, responseSender) -> {
            playerData.dirtBlocksBroken = buf.readInt();
 
            client.execute(() -> {
                client.player.sendMessage(Text.literal("Initial specific dirt blocks broken: " + playerData.dirtBlocksBroken));
            });
        });
    }
}

As soon as you join the world/server you should see a message popup telling you the amount of dirt blocks you've specifically broken.

  • Note: The playerData we created isn't the up-to-date one that lives on the server. We simply create our own copy of PlayerData client-side and update it as we receive packets. Since it's public static that means you can access it from anywhere on the client.
  • 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, 'Minecraft Server' and use authme, you could verify it does indeed work as it's supposed to.)

More Involved Player Data

And just for good measure, let's see an example of how our StateSaverAndLoader class would look if our PlayerData has more than primitives, like lists, and even its own hashmap. How would that look?

Let's say this is our PlayerData':

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
 
public class PlayerData {
    public int dirtBlocksBroken = 0;
 
    public HashMap<Integer, Integer> fatigue = new HashMap<>();
 
    public List<Integer> oldCravings = new ArrayList<>();
}

This would be our StateSaverAndLoader:

import net.minecraft.entity.LivingEntity;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.PersistentState;
import net.minecraft.world.PersistentStateManager;
import net.minecraft.world.World;
 
import java.util.HashMap;
import java.util.UUID;
 
public class StateSaverAndLoader extends PersistentState {
 
    public Integer totalDirtBlocksBroken = 0;
 
    public HashMap<UUID, PlayerData> players = new HashMap<>();
 
    @Override
    public NbtCompound writeNbt(NbtCompound nbt) {
        nbt.putInt("totalDirtBlocksBroken", totalDirtBlocksBroken);
 
        NbtCompound playersNbt = new NbtCompound();
        players.forEach((uuid, playerData) -> {
            NbtCompound playerNbt = new NbtCompound();
 
            playerNbt.putInt("dirtBlocksBroken", playerData.dirtBlocksBroken);
 
            playerNbt.putIntArray("oldCravings", playerData.oldCravings);
 
            NbtCompound fatigueTag = new NbtCompound();
            playerData.fatigue.forEach((foodID, fatigueAmount) -> fatigueTag.putInt(String.valueOf(foodID), fatigueAmount));
            playerNbt.put("fatigue", fatigueTag);
 
            playersNbt.put(uuid.toString(), playerNbt);
        });
        nbt.put("players", playersNbt);
 
        return nbt;
    }
 
    public static StateSaverAndLoader createFromNbt(NbtCompound tag) {
        StateSaverAndLoader state = new StateSaverAndLoader();
        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken");
 
        NbtCompound playersNbt = tag.getCompound("players");
        playersNbt.getKeys().forEach(key -> {
            PlayerData playerData = new PlayerData();
 
            playerData.dirtBlocksBroken = playersNbt.getCompound(key).getInt("dirtBlocksBroken");
 
            NbtCompound fatigueCompound = playersNbt.getCompound(key).getCompound("fatigue");
            fatigueCompound.getKeys().forEach(s -> {
                Integer foodID = Integer.valueOf(s);
                int fatigueAmount = fatigueCompound.getInt(s);
                playerData.fatigue.put(foodID, fatigueAmount);
            });
 
            for (int oldCravings : playersNbt.getCompound(key).getIntArray("oldCravings")) {
                playerData.oldCravings.add(oldCravings);
            }
 
            UUID uuid = UUID.fromString(key);
            state.players.put(uuid, playerData);
        });
 
        return state;
    }
 
    private static Type<StateSaverAndLoader> type = new Type<>(
            StateSaverAndLoader::new, // If there's no 'StateSaverAndLoader' yet create one
            StateSaverAndLoader::createFromNbt, // If there is a 'StateSaverAndLoader' NBT, parse it with 'createFromNbt'
            null // Supposed to be an 'DataFixTypes' enum, but we can just pass null
    );
 
    public static StateSaverAndLoader getServerState(MinecraftServer server) {
        // (Note: arbitrary choice to use 'World.OVERWORLD' instead of 'World.END' or 'World.NETHER'.  Any work)
        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager();
 
        // The first time the following 'getOrCreate' function is called, it creates a brand new 'StateSaverAndLoader' and
        // stores it inside the 'PersistentStateManager'. The subsequent calls to 'getOrCreate' pass in the saved
        // 'StateSaverAndLoader' NBT on disk to our function 'StateSaverAndLoader::createFromNbt'.
        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID);
 
        // If state is not marked dirty, when Minecraft closes, 'writeNbt' won't be called and therefore nothing will be saved.
        // Technically it's 'cleaner' if you only mark state as dirty when there was actually a change, but the vast majority
        // of mod writers are just going to be confused when their data isn't being saved, and so it's best just to 'markDirty' for them.
        // Besides, it's literally just setting a bool to true, and the only time there's a 'cost' is when the file is written to disk when
        // there were no actual change to any of the mods state (INCREDIBLY RARE).
        state.markDirty();
 
        return state;
    }
 
    public static PlayerData getPlayerState(LivingEntity player) {
        StateSaverAndLoader serverState = getServerState(player.getWorld().getServer());
 
        // Either get the player by the uuid, or we don't have data for him yet, make a new player state
        PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData());
 
        return playerState;
    }
}

Our classes which implement ClientModInitializer and ModInitializer would be the same as before, but we include them here so that this section of the article is easy to copy and paste into you program and experiment with persistent state.

import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.text.Text;
 
public class ExampleModClient implements ClientModInitializer {
 
    public static PlayerData playerData = new PlayerData();
 
    @Override
    public void onInitializeClient() {
        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.DIRT_BROKEN, (client, handler, buf, responseSender) -> {
            int totalDirtBlocksBroken = buf.readInt();
            playerData.dirtBlocksBroken = buf.readInt();
 
            client.execute(() -> {
                client.player.sendMessage(Text.literal("Total dirt blocks broken: " + totalDirtBlocksBroken));
                client.player.sendMessage(Text.literal("Player specific dirt blocks broken: " + playerData.dirtBlocksBroken));
            });
        });
 
        ClientPlayNetworking.registerGlobalReceiver(ExampleMod.INITIAL_SYNC, (client, handler, buf, responseSender) -> {
            playerData.dirtBlocksBroken = buf.readInt();
 
            client.execute(() -> {
                client.player.sendMessage(Text.literal("Initial specific dirt blocks broken: " + playerData.dirtBlocksBroken));
            });
        });
    }
}
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.block.Blocks;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
 
public class ExampleMod implements ModInitializer {
 
    public static final String MOD_ID = "your_unique_mod_id_change_me_please";
 
    public static final Identifier DIRT_BROKEN = new Identifier(MOD_ID, "dirt_broken");
 
    public static final Identifier INITIAL_SYNC = new Identifier(MOD_ID, "initial_sync");
 
    @Override
    public void onInitialize() {
        ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
            PlayerData playerState = StateSaverAndLoader.getPlayerState(handler.getPlayer());
            PacketByteBuf data = PacketByteBufs.create();
            data.writeInt(playerState.dirtBlocksBroken);
            server.execute(() -> {
                ServerPlayNetworking.send(handler.getPlayer(), INITIAL_SYNC, data);
            });
        });
 
        PlayerBlockBreakEvents.AFTER.register((world, player, pos, state, entity) -> {
            if (state.getBlock() == Blocks.GRASS_BLOCK || state.getBlock() == Blocks.DIRT) {
                StateSaverAndLoader serverState = StateSaverAndLoader.getServerState(world.getServer());
                // Increment the amount of dirt blocks that have been broken
                serverState.totalDirtBlocksBroken += 1;
 
                PlayerData playerState = StateSaverAndLoader.getPlayerState(player);
                playerState.dirtBlocksBroken += 1;
 
                // Send a packet to the client
                MinecraftServer server = world.getServer();
 
                PacketByteBuf data = PacketByteBufs.create();
                data.writeInt(serverState.totalDirtBlocksBroken);
                data.writeInt(playerState.dirtBlocksBroken);
 
                ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(player.getUuid());
                server.execute(() -> {
                    ServerPlayNetworking.send(playerEntity, DIRT_BROKEN, data);
                });
            }
        });
    }
}
zh_cn/tutorial/persistent_states.1702627824.txt.gz · Last modified: 2023/12/15 08:10 by dreamuniverse