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 不只是保存整数值,它还保存着其他类型的数据,甚至是其他的 NbtCompound 数据或者任意的字节数据。
  • 因此,如果您希望存储一个自定义类,那么您应当新建一个 NbtCompound 子对象,然后将您自定义类的相应字段打包进这个 NbtCompound 子对象中,再传入其父级 NbtCompound 对象。(我们正在为此努力!)

接下来,将这个方法添加到同一个文件中:

public class StateSaverAndLoader extends PersistentState {
 
    // ... (先前写好的代码部分)
 
    public static StateSaverAndLoader createFromNbt(NbtCompound tag) {
        StateSaverAndLoader state = new StateSaverAndLoader();
        state.totalDirtBlocksBroken = tag.getInt("totalDirtBlocksBroken");
        return state;
    }
}

这个方法的作用与 writeNbt 相反,它需要传入一个 NbtCompound (在 writeNbt 时写入的同一个对象),之后新建一个 StateSaverAndLoader 以供我们传入在 NbtCompound 中的数据。

  • 注:与 getInt 读取整数的方式类似,writeNbt 传递字符串的方式是一样的。

现在我们再添加一个工具类方法,这个方法需要导入 MinecraftServer.PersistentStateManagerPersistentStateManager 有一个名为 getOrCreate 的方法,其中 MOD_ID 作为字段传递给 StateSaverAndLoader 以确定是否有其实例,若没有则创建。
当创建时,其调用我们刚刚写到的 createFromNbt 并将我们已经存储在本地的 NbtCompound 传入进去。最后,这个方法会向给定的 MinecraftServer 返回 StateSaverAndLoader

public class StateSaverAndLoader extends PersistentState {
 
    // ... (先前写好的代码部分)
 
    private static Type<StateSaverAndLoader> type = new Type<>(
            StateSaverAndLoader::new, // 若不存在 'StateSaverAndLoader' 则创建
            StateSaverAndLoader::createFromNbt, // 若存在 'StateSaverAndLoader' NBT, 则调用 'createFromNbt' 传入参数
            null // 此处理论上应为 'DataFixTypes' 的枚举,但我们直接传递为空(null)也可以
    );
 
    public static StateSaverAndLoader getServerState(MinecraftServer server) {
        // (注:如需在任意维度生效,请使用 'World.OVERWORLD' ,不要使用 'World.END' 或 'World.NETHER')
        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager();
 
        // 当第一次调用了方法 'getOrCreate' 后,它会创建新的 'StateSaverAndLoader' 并将其存储于  'PersistentStateManager' 中。
        //  'getOrCreate' 的后续调用将本地的 'StateSaverAndLoader' NBT 传递给 'StateSaverAndLoader::createFromNbt'。
        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID);
 
        // 若状态未标记为脏(dirty),当 Minecraft 关闭时, 'writeNbt' 不会被调用,相应地,没有数据会被保存。
        // 从技术上讲,只有在事实上发生数据变更时才应当将状态标记为脏(dirty)。
        // 但大多数开发者和模组作者会对他们的数据未能保存而感到困惑,所以不妨直接使用 'markDirty' 。
        // 另外,这只将对应的布尔值设定为 TRUE,代价是文件写入磁盘时模组的状态不会有任何改变。(这种情况非常少见)
        state.markDirty();
 
        return state;
    }
}

现在,您的 StateSaverAndLoader 文件应如下所示:

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, // 若不存在 'StateSaverAndLoader' 则创建
            StateSaverAndLoader::createFromNbt, // 若存在 'StateSaverAndLoader' NBT, 则调用 'createFromNbt' 传入参数
            null // 此处理论上应为 'DataFixTypes' 的枚举,但我们直接传递为空(null)也可以
    );
 
    public static StateSaverAndLoader getServerState(MinecraftServer server) {
        // (注:如需在任意维度生效,请使用 'World.OVERWORLD' ,不要使用 'World.END' 或 'World.NETHER')
        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager();
 
        // 当第一次调用了方法 'getOrCreate' 后,它会创建新的 'StateSaverAndLoader' 并将其存储于  'PersistentStateManager' 中。
        //  'getOrCreate' 的后续调用将本地的 'StateSaverAndLoader' NBT 传递给 'StateSaverAndLoader::createFromNbt'。
        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID);
 
        // 若状态未标记为脏(dirty),当 Minecraft 关闭时, 'writeNbt' 不会被调用,相应地,没有数据会被保存。
        // 从技术上讲,只有在事实上发生数据变更时才应当将状态标记为脏(dirty)。
        // 但大多数开发者和模组作者会对他们的数据未能保存而感到困惑,所以不妨直接使用 'markDirty' 。
        // 另外,这只将对应的布尔值设定为 TRUE,代价是文件写入磁盘时模组的状态不会有任何改变。(这种情况非常少见)
        state.markDirty();
 
        return state;
    }
}

相应地,您也需要将实现了 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");
 
    @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());
                // 当泥土方块被挖掘时增加计数
                serverState.totalDirtBlocksBroken += 1;
 
                // 向客户端发送数据包
                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);
                });
            }
        });
    }
}

如果您现在运行游戏,您会发现计数器正常递增,但现在即使您关闭游戏并重新启动,计数器也会从退出时的位置继续递增。

有一点您有可能会忽略,totalDirtBlocksBroken 不是玩家限定的,这意味着若这个模组运行在服务器侧,并同时有好几个玩家挖掘泥土方块,他们都会收到相同的统计数字。
这对我们模组中的特定类型数据是很好的,表明它们能正常工作。但更多时候,我们期望数据是玩家限定(因人而异)的。 正如我们开篇所提,如果我们想要保留任意玩家特定方块的挖掘数据,这时我们要怎么做?

因玩家而异的持久化状态

我们可以将我们所写的代码延伸一下,这样就可以存储每个玩家的特定数据了。

首先,新建一个名为 PlayerData.java 的类(需要和实现了 ModInitializer 的类位于同一层次的软件包中).

  • 特别提示: 我们会创建 HashMap 用以存储 PlayerData 数据,, 当客户端侧希望获知 PlayerData 时非常有效, 但是若在服务器侧如法炮制,客户端将无法获知任何数据,因为这些数据只存在于服务器上

如果您期望在客户端侧获知部分或全部的 PlayerData 数据,则必须使用数据包进行传递。(稍后我们会创建一个内联同步(INITIAL_SYNC)数据包以作演示).

class PlayerData {
    public int dirtBlocksBroken = 0;
}

为尽可能简明地说明问题,在示例代码中我们依然使用这个简单的例子,但您在实际应用中可以向 PlayerData 内放入您所期望使用的任意字段。

接下来,将 StateSaverAndLoader.java 类的头部做如下改动:

// ... (其他的引用包)
import java.util.HashMap;
import java.util.UUID;
 
public class StateSaverAndLoader extends PersistentState {
 
    public Integer totalDirtBlocksBroken = 0;
 
    public HashMap<UUID, PlayerData> players = new HashMap<>();
 
    // ... (代码的剩余部分)
 
}

注:我们创建了一个关于 UUID 数据的 HashMap 并将其存储于 PlayereData 中。
Hashmap 即哈希表,简单而言,在本例中,您向表中给出一个特定的“键值”(key),表从 PlayerData 中返回对应“键值”的 UUID
我们使用 UUID进行记录的原因是每位连接到服务器的玩家的 UUID 必定是唯一的。这就使得我们得以区分不同玩家,并针对其返回相对应的数据。如对应玩家的数据不存在,则创建之。

接下来我们向 StateSaverAndLoader 类内添加一个方法,其接受一个传入参数 LivingEntity 并从我们的哈希表中返回对应的 PlayerData

public class StateSaverAndLoader extends PersistentState {
 
    // ... (先前写好的代码部分)
 
    public static PlayerData getPlayerState(LivingEntity player) {
        StateSaverAndLoader serverState = getServerState(player.getWorld().getServer());
 
        // 根据 UUID 获取对应玩家的状态,如果没有该玩家的数据,就创建一个新的玩家状态。
        PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData());
 
        return playerState;
    }
}
  • 若类 StateSaverAndLoader 从未处理过对应玩家的 UUID ,那么哈希表的返回值将会为空。这就是为何我们使用了 computeIfAbsent 方法,这样当该 UUID 未在表中存储时,模组会自动创建一个对应该玩家的新 PlayerData 并存入表中。我们使用 UUID 作为键值以便稍后从表中检索。

接下来将实现了 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");
 
    @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());
                // 当泥土方块被挖掘时增加计数
                serverState.totalDirtBlocksBroken += 1;
 
                PlayerData playerState = StateSaverAndLoader.getPlayerState(player);
                playerState.dirtBlocksBroken += 1;
 
                // 向客户端发送数据包
                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);
                });
            }
        });
    }
}

您同时需要将实现了 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();
            int playerSpecificDirtBlocksBroken = buf.readInt();
 
            client.execute(() -> {
                client.player.sendMessage(Text.literal("总计被挖掘的泥土方块数:" + totalDirtBlocksBroken));
                client.player.sendMessage(Text.literal("当前玩家挖掘的泥土方块数:" + playerSpecificDirtBlocksBroken));
            });
        });
    }
}

如果您现在运行客户端,看上去一切工作正常,但我们忘记了非常关键的一点:我们没有使用 writeNbtcreateFromNbt 方法来存储、加载我们的哈希表。

改动后的方法代码如下所示:

public class StateSaverAndLoader extends PersistentState {
 
    // ... (代码的剩余部分)
 
    @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;
    }
 
    // ... (代码的剩余部分)
 
}

最终的 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);
 
            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, // 若不存在 'StateSaverAndLoader' 则创建
            StateSaverAndLoader::createFromNbt, // 若存在 'StateSaverAndLoader' NBT, 则调用 'createFromNbt' 传入参数
            null // 此处理论上应为 'DataFixTypes' 的枚举,但我们直接传递为空(null)也可以
    );
 
    public static StateSaverAndLoader getServerState(MinecraftServer server) {
        // (注:如需在任意维度生效,请使用 'World.OVERWORLD' ,不要使用 'World.END' 或 'World.NETHER')
        PersistentStateManager persistentStateManager = server.getWorld(World.OVERWORLD).getPersistentStateManager();
 
        // 当第一次调用了方法 'getOrCreate' 后,它会创建新的 'StateSaverAndLoader' 并将其存储于  'PersistentStateManager' 中。
        //  'getOrCreate' 的后续调用将本地的 'StateSaverAndLoader' NBT 传递给 'StateSaverAndLoader::createFromNbt'。
        StateSaverAndLoader state = persistentStateManager.getOrCreate(type, ExampleMod.MOD_ID);
 
        // 若状态未标记为脏(dirty),当 Minecraft 关闭时, 'writeNbt' 不会被调用,相应地,没有数据会被保存。
        // 从技术上讲,只有在事实上发生数据变更时才应当将状态标记为脏(dirty)。
        // 但大多数开发者和模组作者会对他们的数据未能保存而感到困惑,所以不妨直接使用 'markDirty' 。
        // 另外,这只将对应的布尔值设定为 TRUE,代价是文件写入磁盘时模组的状态不会有任何改变。(这种情况非常少见)
        state.markDirty();
 
        return state;
    }
 
    public static PlayerData getPlayerState(LivingEntity player) {
        StateSaverAndLoader serverState = getServerState(player.getWorld().getServer());
 
        // 根据 UUID 获取对应玩家的状态,如果没有该玩家的数据,就创建一个新的玩家状态。
        PlayerData playerState = serverState.players.computeIfAbsent(player.getUuid(), uuid -> new PlayerData());
 
        return playerState;
    }
}

接下来运行客户端,玩家特定的数据现在都可以被正确地保存了。

  • 注:每次您使用 Fabric 调试启动客户端时,您所分配到的 UUID 都是不一样的,所以您可能会认为代码没有正确工作,但这只是因为您使用的开发环境使然。(如果您在生产环境运行一个 Minecraft 服务器并使用 Authme,您就可以确认模组在按照我们的预期目的工作了。)

请谨记,只要您向 PlayerDataStateSaveAndLoader 添加了新的字段,您就需要将对应的字段调用 writeNbtcreateFromNbt 方法。如果您忘记了这一步,那么数据就无法被正确地保存到磁盘或从磁盘读取。

内联同步

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.)

更复杂的玩家数据

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.1703145092.txt.gz · Last modified: 2023/12/21 07:51 by dreamuniverse