furrycat furrycat

Implementing /me chat support


Many games now feature the IRC standard /me command. If you don't know what this is ... where have you been? No seriously, if you don't know what it is, it enables you do "say" stuff in the third person. So if I typed "/me faints with shock" into an IRC program, the server would parse this and display it as "* furrycat faints with shock *"

The Quake3 engine doesn't support this but it is very easy to hack in. Here's how to do it:

All the work is done on the server side. We intercept a client's chat message in the G_Say() function (in g_cmds.c) and look for the magic "/me" string. If we find it, we pass control to a new function G_SlashMe() (which I'll show you later) and return. Otherwise we continue to the end of G_Say() as usual.

void G_Say( gentity_t *ent, gentity_t *target, int mode, const char *chatText ) {
  int j;
  gentity_t *other;
  int color;
  char name[64];
  // don't let text be too long for malicious reasons
  char text[MAX_SAY_TEXT];
  char location[64];

  if ( g_gametype.integer < GT_TEAM && mode == SAY_TEAM ) {
    mode = SAY_ALL;
  }

  /* check for /me */
  if (*(chatText + 0) == '/') {
    if (*(chatText + 1) == 'm' || *(chatText + 1) == 'M') {
      if (*(chatText + 2) == 'e' || *(chatText + 2) == 'E') {
        if (*(chatText + 3) == ' ' && *(chatText + 4)) {
          G_SlashMe(ent, target, mode, chatText + 4);
          return;
        }
      }
    }
  }


/* rest of function */

C++ programmers look away now! The above code is quick and dirty but fast. First of all we check that the first four characters of the client's chat text are <slash> m e <space>. Because of the way C's shortcut operators work, we'll drop out of the test if any one of the characters isn't what we expected. After that we want to make sure that there is actually some more text otherwise we'd end up printing "* furrycat <nothing> *" which would look a bit silly.

G_SlashMe()

Now let's write the G_SlashMe() function that we'll call. In fact we can pretty much copy and paste G_Say() but we'll do a slightly different formatting of the chat message. Recall that the chatText argument to G_SlashMe() is the client's original message minus the /me  at the start.

Please note that since we've already called this function, we either need to prototype it or place it before G_Say() in g_cmds.c or the compiler will get upset.

void G_SlashMe( gentity_t *ent, gentity_t *target, int mode, const char *chatText ) {
  int j;
  gentity_t *other;
  // don't let text be too long for malicious reasons
  char text[MAX_SAY_TEXT];

  switch ( mode ) {
  default:
  case SAY_ALL:
    Com_sprintf(text, MAX_SAY_TEXT, S_COLOR_RED "* %s %s *", ent->client->pers.netname, chatText);
    G_LogPrintf( "%s\n", text);
    break;
  case SAY_TEAM:
  case SAY_TELL:
    Com_sprintf(text, MAX_SAY_TEXT, S_COLOR_MAGENTA "* %s %s *", ent->client->pers.netname, chatText);
    G_LogPrintf( "%s\n", text);
    break;
  }

  if ( target ) {
    G_SlashMeTo(ent, target, mode, text);
    return;
  }

  // echo the text to the console
  if ( g_dedicated.integer ) {
    G_Printf( "%s\n", text);
  }

  // send it to all the apropriate clients
  for (j = 0; j < level.maxclients; j++) {
    other = &g_entities[j];
    G_SlashMeTo(ent, other, mode, text);
  }
}

The differences (highlighted in red) between this and G_Say() are that instead of printing "furrycat: bla bla bla" we'll print "* furrycat bla bla bla *" and also log it with G_LogPrintf(). Team chats and tell chats will be highlighted in purple to distinguish them from public messages.

G_SlashMeTo()

Finally, here's the G_SlashMeTo() command which, as you probably guessed, replaces G_SayTo() when we are using /me. Again, we need to place this function before any other functions which call it or prototype it somewhere.

static void G_SlashMeTo( gentity_t *ent, gentity_t *other, int mode, const char *message ) {
  if (!other) {
    return;
  }
  if (!other->inuse) {
    return;
  }
  if (!other->client) {
    return;
  }
  if ( other->client->pers.connected != CON_CONNECTED ) {
    return;
  }
  if ( mode == SAY_TEAM && !OnSameTeam(ent, other) ) {
    return;
  }
  // no chatting to players in tournements
  if ( (g_gametype.integer == GT_TOURNAMENT )
    && other-&gt;client-&gt;sess.sessionTeam == TEAM_FREE
    && ent-&gt;client-&gt;sess.sessionTeam != TEAM_FREE ) {
    //Hmm, maybe some option to do so if allowed? Or at least in developer mode...
    return;
  }

  trap_SendServerCommand( other-g_entities, va("%s \"%s\"",
    mode == SAY_TEAM ? "tchat" : "chat", message));
}

As you can see, it's almost exactly the same as G_SayTo() with the exception that the final server command is send without modifying the chat text to include the sender's name.

Download

You can download the modified g_cmds.c here: slashme.zip

Feedback

Send any comments to jk2@furrycat.net.

Please note I have configured ICQ to ignore messages from individuals not on my contact list.