Minecraft: Displaying Entity Outlines on Specific Players with ProtocolLib

Abby Beizer
AI and Games — By Regression Games
6 min readDec 2, 2022

--

Minecraft’s extensive modding community offers free plugins for almost any feature you could need to personalize your server. However, creating features for your own plugin can be deceptively difficult at times — especially when you’re working with less intuitive components like metadata packets. Being able to limit who can see another player’s outline from the “glowing” effect is a great example of this. The ability to customize glow effects is a pretty common ask on forums like Spigot’s, but only a handful of threads lead readers in a helpful direction, and even those conclude with incomplete/buggy code samples. There are a small number of plugins that can handle this task using in-game commands, but each one is either poorly documented for the purpose of development or is no longer receiving updates for current Minecraft versions.

This feature was coincidentally something we needed to implement for Regression Games’ Ultimate Collector Challenge. The challenge pits two teams against each other in a race to collect specific resources, where teams consist of human players and bots they’ve created using Regression Games’ AI engine. Due to the game mode’s competitive nature, we needed an easy way for players to track their own bots’ locations without giving them away to opponents.

My solution uses the ProtocolLib plugin to monitor outgoing packets from the server and modify the “glowing” attribute for specific packets. We inform the client to render an outline around a bot whenever its owner receives a packet containing its metadata. By default, outlines adopt the color of the player’s current team, or are white if the player isn’t on a team.

Note that following code samples will be written in Kotlin.

Dependencies

To start, we’ll need to add ProtocolLib as a dependency for our project and our plugin’s plugin.yml. Make sure that the version you’re using is compatible with the version of Minecraft running on your server. It’s also important to note that any server using your plugin will now also need to include the ProtocolLib plugin in its /plugins directory.

name: RegressionGames
version: 1.x
author: Regression Games
main: gg.regression.rgspigot.RegressionGamesPlugin
api-version: 1.x
depend: ["ProtocolLib"]

Listen for Packets

Instantiate a ProtocolManager in the plugin’s onEnable method. This will allow us to start interacting with the ProtocolLib library. We’ll be setting up packet listeners in the following steps, so we’ll remove those listeners in the plugin’s onDisable method.

class RegressionGamesPlugin : JavaPlugin() {

lateinit var protocolManager: ProtocolManager

override fun onEnable() {
protocolManager = ProtocolLibrary.getProtocolManager()
}

override fun onDisable() {
this.protocolManager.removePacketListeners(this)
}
}

Next, we’ll need to listen for appropriate packets. NAMED_ENTITY_SPAWN packets contain metadata for an Entity the server is spawning for the first time or is respawning upon death. ENTITY_METADATA packets contain metadata about an Entity whenever that metadata is modified by the server. We’ll listen for both of these to ensure that outlines are visible as soon as bots spawn and that they persist after death.

private fun enableBotOutlines() {
plugin.protocolManager.addPacketListener(object : PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.NAMED_ENTITY_SPAWN, PacketType.Play.Server.ENTITY_METADATA) {
// Override `onPacketSending` to modify a packet before it's sent to the client
override fun onPacketSending(event: PacketEvent) {
}
}
}

Determine Which Packets to Modify

For this example, we only want to modify packets which are being sent to a human player and contain metadata for one of the recipient’s bots. Regression Games closely monitors the state of each player and records data on top of Minecraft’s Player model so this sample won’t work as a direct copy-and-paste, but it serves to convey our basic idea.

event.player represents the Player that will receive the packet, and event.packet contains updated metadata for a Player (do note that event.player and event.packet may represent the same Player). While we are given the name, id, etc. of the recipient up-front, the packet contains only an entityId — in this example, we’ll need to match that id to a list of known Players to determine whether this Entity is one of the recipient’s bots.

override fun onPacketSending(event: PacketEvent) {

var applyOutline = false
val recipient: Player = event.player
val packet: PacketContainer = event.packet

if (rgUtils.isPlayerHuman(recipient)) {
val entityId: Int? = packet.integers?.readSafely(0)
if (entityId != null && rgUtils.isEntityOwnedBy(entityId, recipient)) {
applyOutline = true
}
}
}

Note on Player Respawns

Many of the code samples I’ve come across attempt to match the entityId in the packet to one of the active players by iterating over Bukkit.getOnlinePlayers(). This won’t work for certain packet types, such as NAMED_ENTITY_SPAWN because a player is removed from the list of online players when they die, but the packet is sent before the player is added back to the list during respawn. You can maintain your own list of active players, like Regression Games does, or you can delay evaluation by 1 tick using Bukkit’s task scheduler.

Enabling the Glowing Effect

Now that we’ve confirmed the recipient should see an outline around the target entity, we need to modify the packet. This is where ProtocolLib gives us an easy way to access and set values in the Entity’s metadata. We need to look at the metadata format to know that the first byte can represent one of several conditions such as the Entity being on fire, crouching, being invisible, etc. From this reference, we can see that 0x40 represents the glowing effect. Our implementation will be a bit different depending on which packet type we’re handling, but ultimately we are reading the first byte, checking its value, and setting it to 0x40 if it doesn’t represent another active condition.

override fun onPacketSending(event: PacketEvent) {

// ...

if (applyOutline) {
if (packet.type.equals(PacketType.Play.Server.NAMED_ENTITY_SPAWN)) {
val dataWatcher: WrappedDataWatcher? = packet.dataWatcherModifier.readSafely(0)
if (dataWatcher != null && dataWatcher.hasIndex(0)) {
var value = dataWatcher.getByte(0)
value = (value.toInt() or 64).toByte()
dataWatcher.setObject(0, value)
}
} else {
val watchableObjects: List<WrappedWatchableObject>? = packet.watchableCollectionModifier.readSafely(0)
val metadata: WrappedWatchableObject? = watchableObjects?.get(0)
if (metadata != null) {
try {
var value = metadata.value as Byte
value = (value.toInt() or 0x40).toByte()
metadata.value = value
} catch (e: ClassCastException) {
// do nothing
}
}
}
}
}
Selective outlines in a Regression Games “Ultimate Collector” match

The Complete Solution

/**
* Uses ProtocolLib to modify outgoing packets to apply a glowing effect to bots that only their owners can see.
* The in-game outline automatically adopts the color of bot team.
*/
private fun enableBotOutlines() {
plugin.protocolManager.addPacketListener(object : PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.NAMED_ENTITY_SPAWN, PacketType.Play.Server.ENTITY_METADATA) {
override fun onPacketSending(event: PacketEvent) {

var applyOutline = false
val recipient: Player = event.player
val packet: PacketContainer = event.packet

if (rgUtils.isPlayerHuman(recipient)) {
val entityId: Int? = packet.integers?.readSafely(0)
if (entityId != null && rgUtils.isEntityOwnedBy(entityId, recipient)) {
applyOutline = true
}
}

if (applyOutline) {
if (packet.type.equals(PacketType.Play.Server.NAMED_ENTITY_SPAWN)) {
val dataWatcher: WrappedDataWatcher? = packet.dataWatcherModifier.readSafely(0)
if (dataWatcher != null && dataWatcher.hasIndex(0)) {
var value = dataWatcher.getByte(0)
value = (value.toInt() or 64).toByte()
dataWatcher.setObject(0, value)
}
} else {
val watchableObjects: List<WrappedWatchableObject>? = packet.watchableCollectionModifier.readSafely(0)
val metadata: WrappedWatchableObject? = watchableObjects?.get(0)
if (metadata != null) {
try {
var value = metadata.value as Byte
value = (value.toInt() or 0x40).toByte()
metadata.value = value
} catch (e: ClassCastException) {
// do nothing
}
}
}
}
}
})
}

At Regression Games, we build the platform and ecosystem to make competitive gaming and esports with artificial intelligence accessible and enjoyable for everyone. Players will write code and AIs to control characters, debug strategies in real time, compete for prizes in tournaments and top spots on the leaderboards, and collaborate with friends to build the best bots possible. You can sign up for our first season at https://regression.gg, and play in our Minecraft Ultimate Collector challenge!

Twitter: https://twitter.com/RegressionGG

Discord: https://discord.com/invite/w7W8dYCH

Instagram: https://www.instagram.com/regressiongames/

TikTok: https://www.tiktok.com/@regressiongg

Excited about working on AI, gaming, and esports full time? Come work with us! We are remote-first! Feel free to sign up here or send us a DM through our socials.

--

--