After releasing version 3.0.0 of the mod, I got a bug report in very quickly. Butterflies were no longer spawning in server games. I hadn’t really been testing servers up to that point, but as it turns out that didn’t mean people weren’t using the mod on servers.
Server Testing
The first step in developing any fix is to reproduce the original bug. In order to do this, I needed to run a local server and attach a client to it. The gradle project provides a runServer
command that lets you do this. Before this will work, however, you need to go into your project’s root folder and find /run/eula.txt
. Change the last line in that file to “eula=TRUE
“, and you will be able to start a server through your IDE.
There is also a server.properties
file in the same folder that will allow you to change things like the game mode and the difficulty. You will need to set online-mode=false
in this file to allow players to connect without being logged in to Minecraft properly. This is because a test client usually isn’t logged in, and by default servers will prevent players from connecting if they aren’t. This can be important for security when running a real server, but for testing a server locally it’s safe to disable.
After doing this we can run a test server. Then, when we run a client and select Multiplayer, we will see the local server ready to connect.
After I did this, I confirmed that indeed butterflies were invisible to clients. After checking the log output it was obvious why. I had a fundamental misunderstanding of how data works in Minecraft.
Data Only Loads on Servers
Clients load assets, and servers load data. By default, everything under the /resources/data
folder isn’t directly available to clients. It has to be synced from the server. This isn’t a problem in single player games – the game will load both assets and data since it is operating as a listen-server.
However, in multiplayer games clients only know about the data the server tells it about. On releasing v3.0.0 I had added data to help define butterfly attributes, but I had neglected to write any code to synchronise that data to the clients. So in multiplayer games, clients had no butterfly data.
Thankfully, fixing this wasn’t too difficult. We start by defining a data packet that can store all the butterfly data.
/** * A network packet used to send butterfly data to the clients. * @param data A collection of all the butterfly data the server has. */ public record ClientboundButterflyDataPacket(Collection<ButterflyData> data) implements CustomPacketPayload { // The ID of this packet. public static final ResourceLocation ID = new ResourceLocation(ButterfliesMod.MODID, "butterfly_data"); /** * Write the data to a network buffer. * @param buffer The buffer to write to. */ @Override public void write(@NotNull FriendlyByteBuf buffer) { buffer.writeCollection(data, (collectionBuffer, i) -> { collectionBuffer.writeInt(i.butterflyIndex); collectionBuffer.writeUtf(i.entityId); collectionBuffer.writeEnum(i.size); collectionBuffer.writeEnum(i.speed); collectionBuffer.writeEnum(i.rarity); collectionBuffer.writeEnum(i.habitat); collectionBuffer.writeInt(i.caterpillarLifespan); collectionBuffer.writeInt(i.chrysalisLifespan); collectionBuffer.writeInt(i.butterflyLifespan); }); } /** * Get the ID of this buffer. * @return ResourceLocation containing the buffer ID. */ @Override @NotNull public ResourceLocation id() { return ID; } }
Minecraft’s FriendlyByteBuf
makes this easy with several methods to write pretty much anything we would need to write. writeCollection()
is especially useful here, as we are sending an array of butterfly data.
Next we create a NetworkEventListener
that listens to events on the FORGE
bus. This allows us to handle any networking events in the game.
/** * Listens for network-based events. */ @Mod.EventBusSubscriber(modid = ButterfliesMod.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE) public class NetworkEventListener { /** * Called when there is a datapack sync requested. Used to send butterfly * data to the clients. * @param event The sync event. */ @SubscribeEvent public static void onDatapackSync(OnDatapackSyncEvent event) { // Handle events here } }
Within this class we define two methods. One to send, the other to receive. The event we need to respond to send is OnDatapackSyncEvent
. This event is sent to a server when a client requests a datapack sync. The server will then send its datapack to the client. This ensures that things like recipes and loot tables are the same as the server on every client. We use this to also send our butterfly data to the client.
/** * Called when there is a datapack sync requested. Used to send butterfly * data to the clients. * @param event The sync event. */ @SubscribeEvent public static void onDatapackSync(OnDatapackSyncEvent event) { // Get the butterfly data collection. Collection<ButterflyData> butterflyDataCollection = ButterflyData.getButterflyDataCollection(); // Create our packet. ClientboundButterflyDataPacket packet = new ClientboundButterflyDataPacket(butterflyDataCollection); // Create the payload. Packet<?> payload = new ClientboundCustomPayloadPacket(packet); // Handle a single player. if (event.getPlayer() != null) { event.getPlayer().connection.send(payload); } // Handle multiple players. if (event.getPlayerList() != null) { for (ServerPlayer i : event.getPlayerList().getPlayers()) { i.connection.send(payload); } } }
getButterflyDataCollection()
is a new accessor we added that returns an array of all the butterfly data.
We now send the data to clients, but the clients need to know what to do with the data once they receive it. To do this, we add another method that responds to a CustomPayloadEvent
.
/** * Called when a custom payload is received. * @param event The payload event. */ @SubscribeEvent public static void onCustomPayload(CustomPayloadEvent event) { // Handle a butterfly data collection. if (event.getChannel().compareTo(ClientboundButterflyDataPacket.ID) == 0) { // Extract the data from the payload. List<ButterflyData> butterflyData = event.getPayload().readCollection(ArrayList::new, (buffer) -> { return new ButterflyData(buffer.readInt(), buffer.readUtf(), buffer.readEnum(ButterflyData.Size.class), buffer.readEnum(ButterflyData.Speed.class), buffer.readEnum(ButterflyData.Rarity.class), buffer.readEnum(ButterflyData.Habitat.class), buffer.readInt(), buffer.readInt(), buffer.readInt()); }); // Register the new data. for (ButterflyData butterfly : butterflyData) { ButterflyData.addButterfly(butterfly); } } }
We check that the packet ID matches our butterfly data, then we read the data from the packet. It essentially does the opposite of our custom data packets write()
method. Once we have read the data we use addButterfly()
to finish the whole process.
Now we have the data synchronised between client and server, and it works. After connecting a client to a local server, you can now see butterflies in the world!
Except, it turns out it’s not quite that easy.
Backporting Woes
The current flow I have for developing the mod is to implement for the latest version first (i.e. 1.20.2), then backport to earlier versions. It turns out that 1.20.2 introduced some changes to the network code, so I still wasn’t done. I needed to change a few things to get synchronisation working in earlier versions of the game.
CustomPacketPayload
doesn’t exist in 1.20.1 and earlier versions. When you send a message, you send a FriendlyByteBuf
directly. It also doesn’t send the packet ID for you, so you need to make sure you write it to the buffer yourself. To support this, I removed ClientboundButterflyDataPacket
‘s inheritance from CustomPacketPayload
and added a getBuffer()
method.
/** * Get the buffer to send to a client. * @return The buffer to send. */ public FriendlyByteBuf getBuffer() { FriendlyByteBuf result = new FriendlyByteBuf(Unpooled.buffer()); result.writeResourceLocation(ID); write(result); return result; }
This means that in our NetworkEventListener
we can create a payload using:
Packet<?> payload = new ClientboundCustomPayloadPacket(packet.getBuffer());
Everything else in the method remains the same.
When responding to a packet being received, the game uses network channels. We need to create a new one that is tied to our packet ID.
public static final EventNetworkChannel BUTTERFLY_NETWORK_CHANNEL = NetworkRegistry.ChannelBuilder. named(ClientboundButterflyDataPacket.ID). clientAcceptedVersions(a -> true). serverAcceptedVersions(a -> true). networkProtocolVersion(() -> NetworkConstants.NETVERSION). eventNetworkChannel();
To tell the game what method to call when this packet is received, we need to register it during the game’s setup.
/** * Listens for mod lifecycle events. */ @Mod.EventBusSubscriber(modid = ButterfliesMod.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) public class LifecycleEventListener { /** * Common setup - initialise some network event handlers. * @param event The event and its context. */ @SubscribeEvent public static void onCommonSetup(FMLCommonSetupEvent event) { NetworkEventListener.BUTTERFLY_NETWORK_CHANNEL.addListener(NetworkEventListener::onButterflyCollectionPayload); } }
Since this method is only called for this packet ID, we don’t need to check it within the event response itself. We also need to respond to NetworkEvent.ServerCustomPayloadEvent
instead of CustomPayloadEvent
. Other than that our client method remains the same.
/** * Called when a custom payload is received. * @param event The payload event. */ @SubscribeEvent public static void onCustomPayload(NetworkEvent.ServerCustomPayloadEventevent) { // Extract the data from the payload. List<ButterflyData> butterflyData = event.getPayload().readCollection(ArrayList::new, (buffer) -> { return new ButterflyData(buffer.readInt(), buffer.readUtf(), buffer.readEnum(ButterflyData.Size.class), buffer.readEnum(ButterflyData.Speed.class), buffer.readEnum(ButterflyData.Rarity.class), buffer.readEnum(ButterflyData.Habitat.class), buffer.readInt(), buffer.readInt(), buffer.readInt()); }); // Register the new data. for (ButterflyData butterfly : butterflyData) { ButterflyData.addButterfly(butterfly); } }
Thankfully, this code also works in 1.19.2, so we don’t need to make any further changes to get things working there.
Coda
So this was an interesting and unexpected bug to work on. I got things working again and released a new version with the fixes. And I learned a lot about Minecraft’s networking code, and a bit more about backporting. I may not have any new features this week, but I have a better, more stable version of the mod.
I want to give a shoutout to user Rat – they’ve been reporting and investigating several bugs in the mod this week, which has helped improve butterflies for all. I always appreciate people reporting issues, even if they are annoying to hear about and fix. At the end of the day simply knowing about them helps to improve the mod and allow more people to play and enjoy it.