New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create CommandAPI plugin messaging system (Fix CommandAPIVelocity#updateRequirements
)
#467
base: dev/dev
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are still two files I have yet to stare at in full detail (CommandAPIProtocol
and CommandAPIMessengerTests
). I have yet to stare at packet ID usage and figuring out how the tests work (looks like you've made some very good use of Mockito using features I've never seen before!)
commandapi-core/src/main/java/dev/jorel/commandapi/network/CommandAPIMessenger.java
Outdated
Show resolved
Hide resolved
...andapi-bukkit-core/src/main/java/dev/jorel/commandapi/network/BukkitCommandAPIMessenger.java
Outdated
Show resolved
Hide resolved
...c/main/java/dev/jorel/commandapi/network/packets/ClientToServerUpdateRequirementsPacket.java
Outdated
Show resolved
Hide resolved
throw new IllegalStateException("Packet class \"" + clazz.getSimpleName() + "\" was already registered!"); | ||
} | ||
|
||
int id = idToPacket.size(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need this temporary variable, do we? This does the same thing:
packetToId.put(clazz, idToPacket.size());
idToPacket.add(deserializer);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still present in the code
CommandAPI/commandapi-core/src/main/java/dev/jorel/commandapi/network/CommandAPIProtocol.java
Lines 108 to 110 in a264fec
int id = idToPacket.size(); | |
idToPacket.add(deserializer); | |
packetToId.put(clazz, id); |
I think it might make it clearer that the id is the next one in the sequence, though it's probably not much different.
I'm not sure if either is 'best', I could go either way.
To answer some of the questions in the original post:
I think
It works, but it might be in our best interest to provide a version byte as well, so we'd have
I'll get round to this after I stare at how we do sequential packet IDs.
We should throw an exception if a new packet is sent to an old version of the plugin that doesn't know about that packet however I think I'm misunderstanding the question. How would the old plugin know the packet ID is invalid if the packet ID keeps incrementing?
I don't see any reason why id 0 is a bad thing, let's keep it for now. If we want to have "more important packets", we'd add a new channel (e.g. adding a
Consider adding a timeout feature. If we send |
Well, the system is meant to expand to other use cases, not just
I explained this a bit more here on discord. It sounds like when people first hear 'sequential packet ids`, there is a misconception that each packet gets a new id. So, like: INCORRECT
Instead, packet ids are assigned to each class of packet, and the same type of packet always gets the same id. So, like: CORRECT
'sequential packet ids' happens when the CommandAPIProtocol class is statically initialized. The id is used to tell what type of packet needs to be created from the network. Both Velocity and Bukkit would load So, if version 1 loaded
Yeah, a handshake system could be good for both the version problem and the no-response problem. A packet on |
a635bc8
to
4eb7bcb
Compare
dfb17b9
to
196fc7e
Compare
Okay, there has been a massive commit: 196fc7e. Most of the basic ideas are still there, but here's everything new to know about. Handling
|
Here are the answers to the questions in the original post that I've landed on so far.
Yeah, this seems to work.
Sure, this works. Note that each channel handles its ids separately. Currently,
The newer version should take responsibility and never send data that would cause an old version to throw an exception. If a packet is incompatible with an old version, its
This is fine. The specific ids for each packet don't really matter.
If there is not a communicating instance of the CommandAPI installed on a target, it is treated as having protocol version 0. I think all of these questions about the public-facing protocol are well answered now. Of course, if there's anything that seems weird about the protocol, ask and we can figure it out. The rest of the implementation details about how this works in the code should be flexible enough to change in the future if needed. So, the rest of my questions will be in code-review comments, which makes talking about the code easier. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really just two things, one just for me to understand something, the other one is the reason why this is requesting changes.
Anyway, this looks really good (although I might've missed a lot).
I also want to address another thing too: I think we shouldn't really release Velocity support in the next version since the snapshots have started in 9.0.4
's snapshot "stage".
I would suggest waiting and releasing Velocity support by the time 9.1.0
is released.
commandapi-core/src/main/java/dev/jorel/commandapi/network/CommandAPIProtocol.java
Show resolved
Hide resolved
...c/main/java/dev/jorel/commandapi/network/packets/ClientToServerUpdateRequirementsPacket.java
Outdated
Show resolved
Hide resolved
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
Yeah, Velocity should probably not be released in 9.0.4. Under semantic versioning rules, 9.1.0 is probably the lowest correct version since it's a major backward-compatible API change. So, we'll definitely need to change the messages here: Lines 50 to 59 in 3ec0874
and here: CommandAPI/commandapi-core/src/main/java/dev/jorel/commandapi/network/CommandAPIProtocol.java Lines 35 to 47 in 3ec0874
as the TODOs mention. We don't have to change these right now though. Perhaps Velocity will also be released in 10.0.0 along with annotations or whatever. We just need to make sure these messages agree with whatever version releases the networking system. Hopefully, the TODOs will serve as an effective reminder whenever that happens. |
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
a93621c
to
60321e7
Compare
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
500fbfe
to
701d881
Compare
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
701d881
to
b52ea6f
Compare
b52ea6f
to
9b25cef
Compare
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
9b25cef
to
1315b34
Compare
// TODO: If the first released version of the CommandAPI that can receive messages is not 9.0.4, change this description | ||
/** | ||
* The current version of the protocol. This should be incremented when the protocol is updated to indicate a large | ||
* change. A connection without a communicating instance of the CommandAPI (either no CommandAPI instance or a version | ||
* before 9.0.4) should be treated as protocol version 0. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a reminder to address the TODO and change this JavaDoc before merging
// TODO: If the first released version of the CommandAPI that can receive messages is not 9.0.4, change this message | ||
"CommandAPI version 9.0.4 or greater is required to receive UpdateRequirementsPacket" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a reminder to address the TODO and change this error message before merging
ProtocolVersionTooOldException.class, | ||
"Tried to send a packet to " + player + ", which is using protocol version 0. " + | ||
"This system is using version 1. That version is too old to receive the packet. " + | ||
"CommandAPI version 9.0.4 or greater is required to receive UpdateRequirementsPacket", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a reminder to change this error message before merging.
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
1315b34
to
6380f6d
Compare
@Timongcraft suggested on Discord to create a networking-only Bukkit plugin so users who only wanted to use the CommandAPI on Velocity didn't need to add the whole CommandAPI plugin to each backend server. I added a first draft of that with this commit: 3dee243. I haven't tested it yet, but the |
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
3dee243
to
477c737
Compare
...ty/commandapi-velocity-core/src/main/java/dev/jorel/commandapi/CommandAPIVelocityConfig.java
Outdated
Show resolved
Hide resolved
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
477c737
to
0e84afb
Compare
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
0e84afb
to
029f8a6
Compare
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
570ce40
to
d4c5274
Compare
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
68a960e
to
7556edc
Compare
Implement CommandAPIVelocity#updateRequirements by sending a ClientToServerUpdateRequirementsPacket to trigger CommandAPIBukkit#updateRequirements
…ntsPacket is attempted to be sent to target that won't do anything with it Adapt CommandAPIProtocol to support multiple channels - Add CommandAPIPacketHandlerProvider - Rename `commandapi:plugin` to `commandapi:play` - Add HANDSHAKE channel Add SetVersionPacket - Add (more) event listeners to BukkitCommandAPIMessenger and VelocityCommandAPIMessenger to handle sending the SetVersionPacket Add ProtocolVersionTooOldException and ProtocolVersionTooOldPacket Add FriendlyByteBuffer - Use it for fancy VarInt encoding (and more in the future?) - Avoid google/guava#937 silliness by making the method yourself! Tests! (in the dev.jorel.commandapi.test.network package :o) - FriendlyByteBufferTests - NetworkTestBase handles common utility and setup - CommandAPIMessengerTests reworked - Packet tests in separate classes
For some reason that stopped the entire GitHub actions build https://github.com/JorelAli/CommandAPI/actions/runs/5617486381/job/15221661407#step:4:1439 Whoops
…ementsPacket Credit #467 (comment) from @DerEchtePilz. I couldn't figure out how to directly include the suggestion and refactor everything else.
…dateCommands threw UnsupportedOperationException
…ement certain plugin messaging related methods
7556edc
to
58952e9
Compare
The main goal of this PR is to fix the method
CommandAPIVelocity#updateRequirements
, which currently does nothing.When
CommandAPI#updateRequirements
is called, the Commands packet should be resent. This packet encodes a BrigadierRootCommandNode
containing all the commands a player is allowed to run. If a player's requirements change, this packet should be resent to inform the player of those changes (see Updating requirements).Unfortunately, Velocity cannot generate the Commands packet by itself. It can only inject its own commands into a Commands packet sent by a backend server. There is also no clean built-in way to trigger the backend server to resend the Commands packet. So, the CommandAPI needs to set up its own cross-server communication.
That gets to the big change of this PR, a CommandAPI plugin messaging system. The Minecraft protocol includes a Plugin Message packet. Any plugin can use this packet to send whatever data they want, and any plugin at the destination can listen for the packet to receive that data.
While the
CommandAPIVelocity#updateRequirements
problem could have been easily solved with a quick and dirty solution, this PR instead creates a general packet-based communication protocol that should expand to other platforms and new packets. This system is heavily based on what I saw in Minecraft's code for handling packets. To show how this system works, I'll explain the process that fixesCommandAPIVelocity#updateRequirements
.BE WARNED: Lots of code reading ahead and explanations that might only make sense to me :P. If you can't understand what's happening, that's probably because I don't understand what's happening and the code is very silly. I didn't know about most of this stuff until about 2 weeks ago, so there is probably some incorrect information.
Plugin setup/shutdown
Click to expand
The central class of this PR is
CommandAPIMessenger
. It is the main interface for sending and receiving data using the Plugin Message packet. This code starts the process of setting up and shutting down this system:The different platforms have their own ways of hooking into this system, so
CommandAPIMessenger
is extended byBukkitCommandAPIMessenger
andVelocityCommandAPIMessenger
.onEnable
, the platforms set up their implementation and hand it toCommandAPIHandler
.onDisable
,CommandAPIHandler
callsCommandAPIMessenger#close
to disable the messenger.Here is how the messengers are constructed and closed:
CommandAPIMessenger
has two type parameters,InputChannel
andOutputChannel
. These types specify what classes on the platform may send messages to the plugin, and which classes the plugin can send messages to. On Bukkit, onlyPlayer
sends messages, and messages can only be sent toPlayer
. As a middle-man proxy, Velocity can send and receive messages from bothPlayer
andServerConnection
. Those two share the common interfacesChannelMessageSource
for receiving andChannelMessageSink
for sending.This code shows a new class
CommandAPIPacketHandler
, which is again implemented per platform asBukkitPacketHandler
andVelocityPacketHandler
. This class will be inspected later in Handling Packets. What's important is that packets should be handled differently on different platforms, and the messengers setCommandAPIMessenger
up with the correct implementation.BukkitCommandAPIMessenger
andVelocityCommandAPIMessenger
use the objects passed into their constructors to hook into the PluginMessage events on their platforms. So, when the Plugin Message packet is received, Bukkit will callonPluginMessageReceived(String channel, @NotNull Player player, byte[] message)
and Velocity will callonPluginMessageEvent(PluginMessageEvent event)
. The specifics of these methods will be inspected later in Receiving Packets.Finally, the
close
method is abstract inCommandAPIMessenger
, and implemented in the platform messengers. The messengers simply unregister their events and channels before the server shuts down.The CommandAPI protocol
Click to expand
The second and third most important classes are
CommandAPIProtocol
andCommandAPIPacket
. These classes define how data is encoded and decoded into a byte array. They were inspired bynet.minecraft.network.ConnectionProtocol
andnet.minecraft.network.protocol.Packet
respectively. We'll see how they come together in Sending Packets and Receiving Packets, but this is a brief overview of the methods they contain.First, the simpler
CommandAPIPacket
:This interface simply defines the
write
andhandle
method. However, implementations ofCommandAPIPacket
should also have two static methods,deserialize
andcreate
. For example, this is what a packet containing two ints might look like:Here is what these methods do:
void write(ByteArrayDataOutput buffer)
- Writes the data for the packet to the given byte output buffer.static [packet class] deserialize(ByteArrayDataInput buffer)
- Reads data from the given byte input buffer to create a new packet. This method can assume everything written by thewrite
method will be present for it to read.static [packet class] create([parameters for the packet])
- Creates a new packet with the given parameters<InputChannel> void handle(InputChannel sender, CommandAPIPacketHandler<InputChannel> packetHandler)
- Performs the appropriate action for receiving this packet. See more in Handling Packets.Next,
CommandAPIProtocol
:This class handles assigning 'ids' to the packets, which identifies packets in the byte input stream. Each packet class should be added to the protocol by putting it into the static initializer block at the top of the class. The
register
method registers a packet's class by giving it the next id, and linking that id to thedeserialize
method for that packet. ThecreatePacket
method can then be used to turn a byte input buffer into a packet, andgetId
will return the id for a given packet class. If the packet is not registered, these methods will returnnull
and-1
respectively.This class also holds the constant
CHANNEL_NAME
, which is set to"commandapi:plugin"
. This namespace identifier is included in the Plugin Message packet to show the packet is meant to be handled by the CommandAPI.Sending Packets
Click to expand
Sending packets is handled by the
CommandAPIMessenger#sendPacket
method, which looks like this:The
sendPacket
method usesCommandAPIProtocol#getId
to get the id for the packet. If -1 is given as the id, then this packet is not registered and cannot be sent. Otherwise, the id is written to a byte array, followed by the packet's data. These bytes are then sent to the given target using the abstractsendRawBytes
method. The platform implementation for each messenger takes care of getting the bytes sent.Receiving Packets
Click to expand
The platform messengers know when a Plugin Message packet has been received by hooking into the events provided by the server APIs, as discussed in Plugin setup/shutdown. Those methods are implemented as so:
These methods verify the Plugin Message packet is meant for us by checking the channel identifier, then send the event source and data into
CommandAPIMessenger#messageRecieved
. That method looks like this:It reads one byte to be the packet id, then uses
CommandAPIProtocol#createPacket
to deserialize the packet with that id. This method will throw an exception if:CommandAPIProtocol#createPacket
returnsnull
, indicating the given packet id is unknownread
method calls on theByteArrayDataInput
throws anIllegalStateException
, indicating that the packet tried reading past the available dataIf the packet is successfully deserialized it will be handled. This is described in the next section, Handling Packets.
Handling Packets
Click to expand
The main class for handling packets is
CommandAPIPacketHandler
, which currently looks like this:Each packet that needs a platform-specific implementation should have a corresponding method here. For example, the
ClientToServerUpdateRequirements
packet calls thehandleUpdateRequirementsPacket
method, which is implemented like so:Both of these methods handle the packet by updating requirements for the client that sent the packet, though Velocity also has to account for the fact that
ServerConnection
might send it packets.Fixing
CommandAPIVelocity#updateRequirements
Click to expand
So, finally,
CommandAPIVelocity
can use this messaging system to fix itsupdateRequirements
method. It does this using theClientToServerUpdateRequirementsPacket
, which looks like this:This packet is very simple, since it doesn't encode any data. It has nothing to read or write, and no parameters to create it. The server simply needs to receive this packet from the client, then should send the Commands packet back to that client.
CommandAPIVelocity
uses this like so:It creates a new
ClientToServerUpdateRequirementsPacket
and sends it to the server the player is connected to. The system discussed previously handles this like so:On Velocity
CommandAPIMessenger#sendPacket
is calledCommandAPIProtocol#getId
returns0
as the id for this packetClientToServerUpdateRequirementsPacket
doesn't have anything to write, so the data is simplynew byte[]{0}
VelocityCommandAPIMessenger#sendRawBytes
sends the data on thecommandapi:plugin
channelOn Bukkit
BukkitCommandAPIMessenger#onPluginMessageReceived
is called to handle the eventcommandapi:plugin
channelCommandAPIMessenger#messageReceivied
is called to handle the byte arraynew byte[]{0}
CommandAPIProtocol#createPacket
is called to deserialize the remaining dataCommandAPIProtocol
finds thedeserialize
method forClientToServerUpdateRequirementsPacket
ClientToServerUpdateRequirementsPacket#deserialize
has nothing to read, so it simply creates its new packetCommandAPIPacket#handle
is calledCommandAPIPacketHandler#handleUpdateRequirementsPacket
is called to handle this packetBukkitPacketHandler#handleUpdateRequirementsPacket
is called to handle the packet given the player who sent itCommandAPI#updateRequirements
is called for the player who sent the packetCommandAPIBukkit#updateRequirements
triggers the Bukkit server to resend its Commands packetOn Velocity
Other notes
CommandAPIVelocityConfig
now requires the plugin object when it is being constructed, sinceVelocityCommandAPIMessenger
needs it to register events.There are tests for
CommandAPIMessenger
and related features incommandapi-bukkit-test-tests
asCommandAPIMessengerTests
. Taking a look at those might help clarify what this system is intended to do.Todo
These are just some details to consider before releasing this PR. It would be annoying to change the protocol after releasing it while keeping backwards compatibility, so it would be good to find a solid system first.
Public protocol details:
commandapi:plugin
a good Plugin Message packet channel name? I'm not sure if there is a convention for channel names. Maybecommandapi
andplugin
are overly generic and could conflict with other channels.packet id, data...
system is used by Minecraft, but maybe there is a better way to encode the data.CommandAPIPacket
registered inCommandAPIProtocol
is given id 0, then id 1, etc. Again, this is how Minecraft does it, but maybe it would make sense to hand out ids in a way that is independent of the order they are registered.ClientToServerUpdateRequirementsPacket
be given id 0? Perhaps there is a more important packet to put 'first'.ClientToServerUpdateRequirementsPacket
? Currently, nothing happens and the requirements are not updated.And of course, code review! If you think there's something weird in the code, point it out and I'll try to fix it.