Wowpedia

We have moved to Warcraft Wiki. Click here for information and the new URL.

READ MORE

Wowpedia
Advertisement

As the number of addons using raid or guild communications is increasing it's become aparent that we need a community protocol for addon developers to use to standardize these comm functions.

Proposed Features

  • Channels for Global, Guild and Group (Raid or party) communiction should be available
  • Channels are only joined if a mod needs to use it
  • Chat from the comm channels must be blocked from appearing in the default UI's chat channels
  • Protocol should not be tied to any one mod, but be implementable by anyone
  • Multiple addons implementing the protocol must not interfere with each other
  • A standard message format must be established, so addons do not conflict with each other's messages
  • A sobriety filter must be applied to outgoing messages to prevent modifications caused by drunk players. This involves replacing the chars 's' and 'S' with unique, rarely used chars. Also "...hic!" must be removed from the end of messages received.

Channel Management

There are two primary issues here, channel names and maintaining the channel connection, changing and chat supression.

Channel Names

Channel names have a 31 char limit and should be considered case-insensitive.

Global

A simple global channel should be available, something like CommGlobal

Group

A group channel should be provided, with a unique name derrived from the party or raid leader's name. If the player is in a raid a party channel will not exist, but party--specific communication can be maintained by the addon receiving the messages by checking the sender against the party roster.

Sample channel name with raid leader named Tekkub: CommRaTekkub

Player names are unique within a realm, cannot contain spaces, and the name limit is 12 chars, so there should never be conflicting channel names that would require the need for a checksum. "Special" chars are allowed however, and would have to be stripped out. The following function could be used:

function Strip(name)
  return string.gsub(name, "%W", "")
end

There is a possibility that a channel name could conflict due to this, so addons using this channel should probably verrify that the sender of a message is in the raid or party before the message is parsed.

The addon or library maintaining connection to this channel must handle leadership changes smoothly. It must close connection to the old group channel and open connection to the new channel. A means of notifying other addons when the channel has been changed is probably required.

Guild

A guild channel mechanism should be provided. This should not restrict the user to their current guild, as many guilds keep an "app guild" or "alt guild" wherein the players may need access to communication with the main guild's addon channel. The addon managing this channel should be able to give some sort of indication of the guild channel it is maintaining, so conflicts do not arrise but many guild channels could potentially be opened.

Guild channels present a unique issue because they can contain spaces and can be quite long. A checksum must be calculated from the raw guild name, then spaces and illegal chars stripped from the name and the string truncated to a specific length. The Checksum would then be appended to the end of the name. For example:
"Khaz Modan Brigade" would have the channel CommGuKhazModa22DE
"Khaz Modan Reserve" would have the channel CommGuKhazModa5215
"KhazModanBrigade" would have the channel CommGuKhazModa84E2
"KhazMødanBrigade" would have the channel CommGuKhazMdan3DD4

To get the guild name checksum, the following method can be used. It makes some concessions to prevent collisions in our specific case, but can be used as a general algorithm as well. In order to keep the channel names as clean as possible for the users, we've capped the checksums to four hex characters. Thanks to Cladhaire and ckknight for this girthy beast.

local SOME_PRIME = 16777213 local FFFFFF = tonumber("FFFFFF", 16)

function CheckSum(text)
	local counter = 1
	for i=1,string.len(text) do
		counter = counter * ((string.byte(text, i) + i) * 17)
    end
	counter = math.mod(counter, FFFFFF)
	return string.format("%06x", counter)
end


Using CheckSum and Strip from above, the following function could be used to get a full channel name from a string:

function GuildChannelName(name)
  local cs = CheckSum(name)
  name = Strip(name)
  name = string.sub(name, 1, 8)
  return "CommGu" .. name .. cs
end

Channel Management

There are a few things addons will need to do if they are using a channel:

  • Leadership changes must result in leaving the old channel and joining the new one.
  • Assure that unused channels are departed, some sort of global registrar table should be used to assure the channel is not closed when someone else is using it.
  • ChatFrames must have the comm channel messages supressed. To prevent duplicate processing a global registrar should be used to indicate if a channel is being filtered. Since one cannot effectivly unhook the filter can remain in place after established.

Global Registrar

A simple table could be used to maintain a registrar. Any addon using the comm protocol would look for this table when they need to register into a channel. If it doesn't exist they would create the basic empty table and add themselves in.

Proposed table structure

AddonCommRegistrar = {
  -- Stores a list of addons currently using a channel
  -- Note that the indexes here don't have to be strings,
  -- anything can be used as long as it's unique to the addon
  channels = {  
    CommGlobal = {  
      ["Sample addon 1"] = true,
      ["Sample addon 2"] = true,
    },
    CommRa = {
      ["Sample raid addon"] = true,       
    },
    CommGuKhazModa12345678 = {
      ["Sample guild addon"] = true,       
    },
  },

  -- Flag indicating that a ChatFrame fliter is in place
  filter = true,
  
  -- GetTime() of the last message that was sent.
  time = 453278.329,

  -- Name of the leader currently used for the group channel
  -- this is in place so that only one addon performs a channel switch on leadership change
  groupleader = "Joebob",
}

Registering a channel

When registering into a channel, an addon should execute this block of code:

-- Establish the registrar
if not AddonCommRegistrar then 
  AddonCommRegistrar = {
    channels = {}, 
  }
end

-- Establish the channel registrar
if not AddonCommRegistrar.channels[channelname] then 
  AddonCommRegistrar.channels[channelname] = {}
end

-- Implement a ChatFrame filter for our channel if one does not exist
if not AddonCommRegistrar.filter then
  -- Hook ChatFrame_OnEvent here
  AddonCommRegistrar.filter = true
end

-- Join the channel
if GetChannelName(channelname) == 0 then
  JoinChannelByName(channename)
end

Unregistering a channel

When an addon is done with a channel it needs to check if anyone else is using it, if noone is it should make sure the channel is departed.

AddonCommRegistrar.channels[channelname]["Sample addon"] = nil
if not next(AddonCommRegistrar.channels[channelname]) then
  -- Leave the channel
  LeaveChannelByName(channelname)
end

Leadership Changes

When the raid or party leader changes, each addon that uses the CommRa* channel should check to see if the switch has been made yet, if not it should perform the switch. Also it should be noted that leaving a channel has a slight delay, the best solution appears to be to delay 1 second between leaving and joining.

-- To be performed when the raid leader changes
if AddonCommRegistrar.groupleader ~= newleadername then
  AddonCommRegistrar.groupleader = newleadername
  LeaveChannelByName("CommRa".. oldleadername)
  -- Delay 1 second, by some means...
  JoinChannelByName("CommRa".. newleadername)
end

Message throttling

The total outgoing can be sustained at about 500 bytes per second, so having a throttle rate of one message every 0.4 seconds is a good comprimise. Make sure to set it right before or right after you send your message.

if AddonCommRegistrar.time <= GetTime() + 0.4 then
  AddonCommRegistrar.time = GetTime()
  SendChatMessage("message", "CHANNEL", nil, "CommRa" .. leaderName)
else
  -- put in a queue
end

Chatframe Filtering

Here is a proposed hook for the ChatFrame_OnEvent to block all Comm text from ever appearing in the default UI's chatframes. Note that only the first addon to register should implement this (see sample in Registering a channel)

local filters = {"^commglobal$", "^commgu", "^commra"}
local cf_oe = ChatFrame_OnEvent
ChatFrame_OnEvent = function(event)
  if event == "CHAT_MSG_CHANNEL" then
    local chan = string.lower(arg9)
    for _,str in pairs(filters) do
      if string.find(chan, str) then return end
    end
  end
  cf_oe(event)
end

Message Protocol

A standard protocol for messages sent should be used to assure addons do not unintentionally receive other addon's messages. The proposed standard is: "<Namespace>: <message>" <Namespace> is some unique token that a develop's addons use. Some devs may choose to share a token across their addons, like "Tekkub" or they may make it addon-specific like "CTRA". Handling of the <message> portion of the communication is entirely placed upon the receiveing addon. This will allow for simple filtering of messages intended for other addons.

Sobriety filter

Special care must be taken so that drunken player don't slur messages.

When you are drunk, your text is slurred in two possible ways: "s" is changed to "sh" and " ...hic!" (localized) is added to the end of your message.
The internal Blizzard function can be represented as such:

function Drunkify(text)
    -- turns regular text into drunken, slurred text.
    return string.gsub(text, "([Ss])", "%1h") .. " ...hic!"
end

assert(Drunkify("I drank seven beers") == "I drank sheven beersh ...hic!")

In order to prevent this, special care must be taken to make sure that "sh"s are turned back into "s"s, but without overkill. Also, the " ...hic!" must be stripped from the end, paying careful attention to localization.

Here are two functions to handle this on send and receive:

-- Package a message for transmission
function Encode(text)
    text = string.gsub(text, "([h°])", "°%1") -- encode a hidden character in front of all the "h"s and the same hidden character
    return text.."°" -- add the hidden character to the very end.
end
-- Clean a received message
function Decode(text)
    text = string.gsub(text, "([Ss])h", "%1")       -- find "h"s added to any "s", remove.
    text = string.gsub(text, "°([h°])", "%1")       -- remove the hidden character.
    text = string.gsub(text, "^(.*)°.-$", "%1") -- make sure there hasn't been further tampering with the msg.
    return text
end

By definition, Decode(Encode(text)) == text, but also Decode(Drunkify(Encode(text))) == text, where Drunkify is Blizzard's internal function.

-- some tests:
assert(Decode(Encode("It's 58° out!")) == "It's 58° out!")
assert(Decode(Encode("She sells sea shells by the sea shore")) == "She sells sea shells by the sea shore")

assert(Decode(Drunkify(Encode("She sells sea shells by the sea shore"))) == "She sells sea shells by the sea shore")
Advertisement