Skip to content

Instantly share code, notes, and snippets.

@apple502j
Created April 28, 2022 10:22
Show Gist options
  • Save apple502j/d19177fadc04ef90b22868668ee42f1e to your computer and use it in GitHub Desktop.
Save apple502j/d19177fadc04ef90b22868668ee42f1e to your computer and use it in GitHub Desktop.
Fabric chat API proposal (22w17a)

Fabric Chat API Proposal

Now that Mojang has decided to refactor chat, it'll be a good time to add an API for that. I had some designs (including one made in 1.18 that I put on GitHub), however I think there are better changes.

Note on Compatibility

I expect the new API to be 1.19+ exclusive, however it's not impossible to backport most (if not all). One of the biggest 17a changes was that chat messages are no longer "wrapped" in chat.type.text and sender/message info were completely split. In 1.18 depending on the injection point it required us to "parse" the text to get the original message, however with this change it is no longer necessary.

Design Idea (Server Side)

Basic concept

This API should handle the following messages:

  • "chat messages" (messages sent using ChatMessageC2SPacket) supports message conversion, blocking, and transformation
  • "game messages" (everything else) supports blocking and transformation
  • /me, /msg, etc: Unsure. Maybe handle as part of game messages?

Top priority is obviously chat messages.

Message conversion refers to a process where the message is converted without styling changes - such as custom text filtering or translation. This process is designed to be async. Transformation is a process where Text gets transformed into another Text. This split allows mods to modify messages without other mods interfering due to them using custom Text.

One thing to keep in mind is that we should make it hard to screw up (AKA block server thread). Currently chat messages are processed in IO thread or worker thread depending on injection point. Game messages are always processed in the server thread. One notable exception is /me which, if invoked by player from chat commands, is processed in worker threads. This (+ game messages having no string content in many cases) is why message conversion is limited to chat only.

Another thing to consider is callback order. Normally things in Fabric API are order-independent; however, given how this API is designed to modify one state, consistent ordering is important here. API Base currenly has "phase"; however phases are defined by us, not them, and it has no guarantee on in-phase ordering. This could potentially be done by adding a variant in API Base that uses TreeMultimap<Integer, Callback> or something similar. (Tree map for sorting by priority, and multimap to allow conflict of priority.) Given how chat isn't usually performance critical and the amount of callbacks being limited I think this is fine. APIs that use this type of events should provide a pre-defined constants for priorities. (See below for examples)

ChatMessageProcessEvent

This event should be used for async stuff requiring message content string.

interface ChatMessageProcessEvent {
  // Should this return CompletableFuture<Void> instead?
  // Note that this is called inside worker thread, so blocking should be fine
  void process(ChatMessageContext context);
  // Priorities
  // Process the original message.
  static int PROCESS_ORIGINAL_MESSAGE = 0;
  // Convert the message (to one that is different) - such as translation
  static int CONVERT = 1000;
  // Process the converted message.
  static int PROCESS_CONVERTED_MESSAGE = 2000;
}

interface ChatMessageContext {
  String getMessage();
  // Should not be null
  ServerPlayerEntity getSender();
  // This allows setting the content/blocked status per-player.
  // Ones without player sets the default.
  // For example, setBlocked(true) + setBlocked(player, false) = DM
  // Question: should per-player setMessage imply setBlocked(player, false)?
  void setMessage(String message);
  void setMessage(ServerPlayerEntity player, String message);
  // Calling setBlocked(true) then setBlocked(false) should be no-op, this should NOT be implemented using 1 map
  void setBlocked(boolean blocked);
  void setBlocked(ServerPlayerEntity player, boolean blocked);
}

ChatTextTransformationEvent

This event should be used to style the text.

interface ChatTextTransformationEvent {
  void transform(ChatTextContext context);
}

interface ChatTextContext {
  Text getText();
  ServerPlayerEntity getSender();
  // Note: if getString returns a different value this will fail the signature validation, which is inevitable.
  void setText(Text text);
  // Used if the message should not be displayed using chat.type.text message. Users should wrap getText()
  // with appropriate text that contains the sender information if needed.
  void setSendAsGameMessage(boolean sendAsGameMessage);
}

Design Idea (Client Side)

Not sure if this should be an event or an API to add vanilla ClientChatListener.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment