· 7 years ago · Jan 05, 2019, 01:04 AM
1-------------------------------------------------------------------------------
2-- Cross RP by Tammya-MoonGuard (2019)
3--
4-- The Alliance Protocol.
5-------------------------------------------------------------------------------
6local _, Me = ...
7local LibRealmInfo = LibStub( "LibRealmInfo" )
8local Comm = Me.Comm
9-------------------------------------------------------------------------------
10-- Imports. We don't really want to clutter up our code with a bunch of
11-- boilerplate, so leave out rarely used globals.
12local min, max, ipairs, pairs, tonumber, tostring, UnitGUID =
13 math.min, math.max, ipairs, pairs, tonumber, tostring, UnitGUID
14local strbyte, strmatch, strsub, strlower, strupper =
15 string.byte, string.match, string.sub, string.lower, string.upper
16local gsub, UnitIsPlayer, UnitFactionGroup, UnitGUID, type =
17 string.gsub, UnitIsPlayer, UnitFactionGroup, UnitGUID, type
18local DebugLog, DebugLog2, tblconcat, next =
19 Me.DebugLog, Me.DebugLog2, table.concat, next
20-------------------------------------------------------------------------------
21-- Some terminology:
22-- FULLNAME: A player name and realm in the normal game format,
23-- e.g. Tammya-MoonGuard.
24-- BAND: A set of people that have the same faction and realm.
25-- DEST/DESTINATION: A player name and a band.
26-- LINK: A link between two Battle.net friends. Both sides of a link must
27-- have Cross RP and be "hosting" for a link to be formed.
28-- BRIDGE: A player local to you (on the same realm and faction) that has one
29-- or more links to other realms or factions. The player themselves
30-- are also a valid bridge if they have links and are hosting.
31-- Bands are formatted as <number><A/H>, meaning the realm ID and Alliance
32-- or Horde. e.g. "1H" is Moon Guard Horde. Some realms are selected as
33-- "primo", which means they have a single digit realm ID.
34-- Destinations are formatted as <player><band>, <player> being their toon name
35-- without their realm attached (as that's part of the band). e.g. "Tammya1A"
36-- for Tammya-MoonGuard Alliance.
37-- For connected realms, there may be multiple bands that are linked, and the
38-- one with the lowest realm ID will be used as the "base band", which is
39-- basically for routing purposes. The links and bridge structs are indexed
40-- by "base" bands where applicable.
41-------------------------------------------------------------------------------
42-- Our module object to export variables too. Rule of thumb is to export as
43-- much as possible (especially tables, as those can share references with
44-- locals).
45local Proto = {} Me.Proto = Proto
46-------------------------------------------------------------------------------
47-- When the user touches someone (mouseover), the band that someone belongs
48-- to is set to 'active'. This table contains band names mapped to time
49-- the user touched them, and the band is active for BAND_TOUCH_ACTIVE
50-- seconds.
51local m_active_bands = {} Proto.active_bands = m_active_bands
52-------------------------------------------------------------------------------
53-- Hosting is when we are offering a connection to the opposite faction or
54-- other realms through our Bnet friends. This may be true anyway even if we
55-- have no links, which just means that we are open to hosting but aren't
56-- really able to at that moment. `hosting_time` is just a number updated
57-- with the time so long as we are hosting (and it pauses when we stop
58-- hosting).
59local m_hosting = false
60local m_hosting_time = 0
61-------------------------------------------------------------------------------
62-- We have some procedures in place to make sure that data is transferred
63-- 'securely'. This is used for connecting raid groups, where we use a
64-- a different addon channel (that only people in the linked groups know), and
65-- for Bnet transfers (which ignore the addon-channel filter) use a sort of
66-- authentication method.
67-------------------------------------------------------------------------------
68-- This is the password the linked groups are using:
69local m_secure_code = nil
70-------------------------------------------------------------------------------
71-- `channel` is the addon prefix that we use, generated by taking the hash
72-- of the secure code against a constant. Currently, it's base32 and suitable
73-- for a suffix for an ingame channel (which earlier designs called for).
74local m_secure_channel = nil
75-------------------------------------------------------------------------------
76-- `secure_hash` is the sha256 hash of our secure code. When broadcasting
77-- status and such, we send a piece of this as a method of authentication.
78-- Basically, this is just a way to let people know what password we're using,
79-- without actually sharing the password.
80local m_secure_hash = nil
81-------------------------------------------------------------------------------
82-- `secure_myhash` is the sha256 hash of our secure code mixed with our
83-- fullname. This is a way to let people know that we actually have the
84-- password, and we aren't just copying the hash from someone else - because
85-- it's salted with our fullname, a constant unique to us.
86local m_secure_myhash = nil
87-------------------------------------------------------------------------------
88-- This table is to keep track of what "secure codes" we receive from a player.
89-- It's indexed by both fullnames and gameids, as we can receive that sort of
90-- data over the broadcasted status (ST), and the Battle.net status (HI).
91-- [<fullname> or <bnet account id> or game id] = {
92-- code: The secure code that we used to compute their state. This is not
93-- what code they're using. It's our code we used when checking if
94-- they're secure.
95-- h1: The public hash of the user's secure code.
96-- h2: The personal public hash of the user's secure code
97-- (secure code + their name)
98-- secure: True if we can contact this user over our secure channel, or if
99-- they're safe to be contacted with secure data over Bnet (which
100-- doesn't have addon prefix filtering).
101-- }
102local m_node_secure_data = {} Proto.node_secure_data = m_node_secure_data
103-------------------------------------------------------------------------------
104-- The time we last broadcast our status to the local channel. We do it around
105-- every two minutes, or STATUS_BROADCAST_INTERVAL seconds. If someone doesn't
106-- broadcast their status for a while, their bridge is considered invalidated
107-- and we will remove it next update.
108local m_status_broadcast_time = 0
109-------------------------------------------------------------------------------
110-- This is a flag to make our next status broadcast use a higher priority, so
111-- that during heavy-load situations, the status message is prioritized.
112-- This is set when we lose a link to someone - we broadcast that state as soon
113-- as possible, to deter people from trying to use a route that is no longer
114-- valid.
115local m_status_broadcast_fast = false
116-------------------------------------------------------------------------------
117-- True if the last status we broadcast had an empty band list. Basically stops
118-- us from rebroadcasting that, as we don't need to periodically tell people
119-- that we can't host for them.
120local m_status_last_sent_empty = false
121-------------------------------------------------------------------------------
122-- These are just cached values of our destination and band. Should look
123-- something like "Tammya1A", and "1A".
124local m_my_dest = nil
125local m_my_band = nil
126-------------------------------------------------------------------------------
127-- Indexed by bands, this is a list of NodeSets that contain our links to other
128-- realms and factions (the user's Battle.net friends that are using
129-- Cross RP). Typically these sets and lists will be very small, as the user
130-- probably won't have that many friends across the faction wall (unless they
131-- play Moon Guard Horde), but we use the same storage as bridges (which
132-- should be a lot more in number) for elegance and convenience.
133-- Basically what I mean is that NodeSets are optimized to work with a lot of
134-- users, and it's kind of overkill for small sets.
135local m_links = {} Proto.links = m_links
136-------------------------------------------------------------------------------
137-- Indexed by bands, this is our list of NodeSets that contain bridges.
138-- The NodeSet class handles managing what bridge to use. We may also have
139-- ourself in the nodeset for our local band, as we can be a valid bridge for
140-- ourself.
141local m_bridges = {} Proto.bridges = m_bridges
142-------------------------------------------------------------------------------
143-- This is a list of links that are actively hosting for us, redundant data
144-- that complements the m_links sets so we can scan them quickly and weed out
145-- things like offline users.
146local m_link_ids = {} Proto.link_ids = m_link_ids
147-------------------------------------------------------------------------------
148-- This is a list of Game Account IDs of Battle.net friends that have Cross RP
149-- installed. The difference between this and above is that this contains
150-- all users, and not just the ones that are "hosting", the reason being that
151-- we still send our period status updates to users that aren't hosting. If
152-- they do start hosting, they do so in a good state already.
153local m_crossrp_gameids = {} Proto.crossrp_gameids = m_crossrp_gameids
154-------------------------------------------------------------------------------
155-- Just a list of what we haved called C_ChatInfo.RegisterAddonMessagePrefix
156-- with, so we don't make duplicate calls.
157local m_registered_addon_prefixes = {} Proto.registered_addon_prefixes =
158 m_registered_addon_prefixes
159-------------------------------------------------------------------------------
160-- A list of realms that are connected to us, formatted as the realm name
161-- without spaces or dashes (e.g. "MoonGuard"). Example use is when we
162-- broadcast our status to our friends list - we don't want to send it to
163-- anyone that is linked to our band.
164local m_linked_realms = {} Proto.linked_realms = m_linked_realms
165-------------------------------------------------------------------------------
166-- This is for the network status display. Basically, just shortens realm names
167-- for common RP realms. Make it pretty for them. Other realms will show up
168-- as a truncated string.
169local m_realm_names = {
170 ArgentDawn = "AD";
171 EmeraldDream = "ED";
172 MoonGuard = "MG";
173 WyrmrestAccord = "WRA";
174} Proto.m_realm_names = m_realm_names
175-------------------------------------------------------------------------------
176-- Indexed by "command" which is the first word found in a received message,
177-- this is a callback list for when we receive a routed message. Registered
178-- with `Proto.SetMessageHandler( command, handler )`.
179local m_message_handlers = {} Proto.message_handlers = m_message_handlers
180-------------------------------------------------------------------------------
181-- Our list of handlers we want to register with the Comm module. BROADCAST is
182-- for (addon) messages received in the broadcast channel. BNET is for
183-- whispered GameData messages over Battle.net, and WHISPER is local addon
184-- whisper messages. Basically, our protocol is broken up into different
185-- transfer mediums like that. I /could/ have designed the system so that
186-- all commands share a single list, but I think there's a little bit of
187-- network security to be had by forcing the proper/expected channels for
188-- different message types, like the routing commands begin with R1 over
189-- WHISPER, then R2 over BNET, and then R3 over BROADCAST or WHISPER.
190Proto.handlers = {
191 BROADCAST = {};
192 BNET = {};
193 WHISPER = {};
194}
195-------------------------------------------------------------------------------
196-- Simple statistic of GetTime() when we started, for uptime or whatever.
197Proto.start_time = nil
198-------------------------------------------------------------------------------
199-- UMIDs are Unique Message IDs. Basically a way to prevent a receiving side
200-- from reading the same message twice. Each message we route has an ID
201-- attached. This prefix is a randomly generated code, to help ensure that
202-- when the user /reloads or relogs, they will generate a new unique ID when
203-- sending another message. A bit overkill, but I hate depending on luck, even
204-- if the chance to fail is super, super slim.
205local m_umid_prefix = ""
206-------------------------------------------------------------------------------
207-- The prefix is mixed with this incrementing number to create a new UMID.
208local m_next_umid_serial = 1
209-------------------------------------------------------------------------------
210-- A list of UMIDs that we have seen already, so if we get another message with
211-- a UMID in this list, we ignore it. The values are time, and they expire
212-- after five minutes (which /should/ be plenty of time to transfer anything).
213local m_seen_umids = {} Proto.seen_umids = m_seen_umids
214-------------------------------------------------------------------------------
215-- A `sender` is an active task to send data from our end. They hold what data
216-- we're sending, what UMIDs are used, what destinations to send it to, and
217-- then we do some management on top of them to make sure they get sent.
218-- Especially important for "guaranteed" messages, where the sender stays
219-- active until an ACK is received (used mainly for RP chat).
220local m_senders = {} Proto.senders = m_senders
221-------------------------------------------------------------------------------
222-- This is what step of our initialization process is done.
223-- 0 = Joining game channel.
224-- 1 = Waiting for Bnet (link) status messages.
225-- 2 = Waiting for local (bridge) status messages.
226-- 3 = Fully initialized.
227Proto.init_state = -1
228-------------------------------------------------------------------------------
229-- These servers get a special single digit realm identifier because they're
230-- very popular. This may change if we decide to support non RP servers
231-- (these IDs are overwriting PvE servers).
232local PRIMO_RP_SERVERS = {
233 [1] = 1365; -- Moon Guard US
234 [2] = 536; -- Argent Dawn EU
235 [3] = 1369; -- Wyrmrest Accord US
236}
237local PRIMO_RP_SERVERS_R = {}
238for k, v in pairs( PRIMO_RP_SERVERS ) do
239 PRIMO_RP_SERVERS_R[v] = k
240end
241
242-------------------------------------------------------------------------------
243-- TIMING
244-------------------------------------------------------------------------------
245-- The time we wait before starting up the system, to give the game and other
246-- addons a bit of time to get settled in. After this expires, we try to join
247-- the game channel and then start our post initialization.
248local START_DELAY = 1.0
249-------------------------------------------------------------------------------
250-- When we stop hosting, we still will accept messages as a bridge/router for
251-- this many seconds. This is so that if we stop hosting, and other clients
252-- haven't registered that yet, we're not going to throw away their messages.
253-- After the time is up, then any new (routing) messages will be ignored.
254local HOSTING_GRACE_PERIOD = 60 * 5
255-------------------------------------------------------------------------------
256-- When touching a player of a foreign band, that band will become "active" for
257-- this many seconds. I don't believe this is being used right now, but it
258-- should trigger an event for future purposes.
259local BAND_TOUCH_ACTIVE = 60 * 15
260-------------------------------------------------------------------------------
261-- The period that we broadcast our status; this includes both the channel
262-- broadcast, and the whisper broadcast to Bnet friends. Every two minutes.
263local STATUS_BROADCAST_INTERVAL = 120
264
265local ACK_TIMEOUT = 10.0
266local ACK_TIMEOUT_PER_BYTE = 2.0/250
267local ONLINE_TIMEOUT = 3.0
268
269
270-------------------------------------------------------------------------------
271-- DESTINATION UTILITY FUNCTIONS
272-------------------------------------------------------------------------------
273-- Returns the band for a valid game unit. A "band" is a short identifier for
274-- realm ID and faction.
275local function GetBandFromUnit( unit )
276 if not UnitIsPlayer( unit ) then return end
277 local guid = UnitGUID( unit )
278 if not guid then return end
279 local realm = LibRealmInfo:GetRealmInfoByGUID( guid )
280 local faction = UnitFactionGroup( unit )
281 if realm <= #PRIMO_RP_SERVERS then
282 realm = "0" .. realm
283 else
284 realm = PRIMO_RP_SERVERS_R[realm] or realm
285 end
286 return realm .. strsub( faction, 1, 1 )
287end Proto.GetBandFromUnit = GetBandFromUnit
288
289-------------------------------------------------------------------------------
290-- Parses the band part of a destination. "Destination" values are player names
291-- and bands put together, e.g. "Tammya1A".
292local function GetBandFromDest( destination )
293 return strmatch( destination, "%d+[AH]" )
294end Proto.GetBandFromDest = GetBandFromDest
295
296-------------------------------------------------------------------------------
297-- A "fullname" is a player name with realm. Destinations contain that and
298-- faction, and this packs them together. Destinations have the player name
299-- realm ID, and faction ID.
300-- Returns a destination for fullname and faction given.
301local function DestFromFullname( fullname, faction )
302 local realm = LibRealmInfo:GetRealmInfo( strmatch( fullname, "%-(.+)" ) or Me.realm )
303 if realm <= #PRIMO_RP_SERVERS then
304 realm = "0" .. realm
305 else
306 realm = PRIMO_RP_SERVERS_R[realm] or realm
307 end
308 return strmatch( fullname, "^[^-]*" ) .. realm .. faction
309end Proto.DestFromFullname = DestFromFullname
310
311-------------------------------------------------------------------------------
312-- Returns a fullname from the destination given.
313-- e.g. Tammya1A -> Tammya-MoonGuard.
314-- Faction bit is discarded.
315local function DestToFullname( dest )
316 local name, realm = strmatch( dest, "(%a+)(%d+)" )
317
318 -- The destination doesn't necessarily have to be properly capitalized, but
319 -- a lot of other things depend on particular capitalization.
320 -- Format so first (UTF-8) character is capitalized only.
321 name = strlower( name )
322 name = gsub( name, "^[%z\1-\127\194-\244][\128-\191]*", strupper )
323
324 -- If the first character in a string is "0" then it bypasses the "primo"
325 -- server list. In other words, if we want to use realm ID 2 directly
326 -- without getting remapped by our primo list, we have to format it as
327 -- "02".
328 local primo = strbyte( realm, 1 ) ~= 48
329 realm = tonumber( realm )
330 realm = primo and PRIMO_RP_SERVERS[realm] or realm
331 local _, _, realm_apiname = LibRealmInfo:GetRealmInfoByID( realm )
332 return name .. "-" .. realm_apiname
333end Proto.DestToFullname = DestToFullname
334
335-------------------------------------------------------------------------------
336-- Parses a destination and returns the "base" band for it. For realms that are
337-- connected to other realms, this scans the connected list and returns the
338-- band with the lowest realm ID. For example, if these three bands are
339-- part of the same connected realm group, "23A" "333A" and "1235A", then it
340-- returns "23A" for any of those destinations. Used mainly for indexing our
341-- bridge and link list (which are meant to point to unique bands).
342local function GetLinkedBand( dest1 )
343 local realm, faction = strmatch( dest1, "(%d+)([AH])" )
344 local primo = strbyte( realm, 1 ) ~= 48
345 realm = tonumber( realm )
346 if primo and PRIMO_RP_SERVERS[realm] then
347 -- primo servers aren't linked.
348 return realm .. faction
349 end
350 local realmid, _,_,_,_,_,_,_,
351 connections = LibRealmInfo:GetRealmInfoByID( realm )
352 for _, v in pairs( connections ) do
353 realm = min( realm, v )
354 end
355 return realm .. faction
356end Proto.GetLinkedBand = GetLinkedBand
357
358-------------------------------------------------------------------------------
359-- Returns true if these two destinations are part of the same band, or if
360-- they're on linked bands.
361local function IsDestLinked( dest1, dest2 )
362 if strbyte( dest1, #dest1 ) ~= strbyte( dest2, #dest2 ) then
363 -- Faction mismatch is always false. The two might be on the same
364 -- connected realm group, but they have to be on the same faction to
365 -- contact each other.
366 return false
367 end
368 local band1, band2 = strmatch( dest1, "(%d+)[AH]" ),
369 strmatch( dest2, "(%d+)[AH]" )
370 if not band1 or not band2 then return end
371 if band1 == band2 then
372 -- A nice shortcut.
373 return true
374 end
375
376 if GetLinkedBand(dest1) == GetLinkedBand(dest2) then
377 return true
378 end
379end Proto.IsDestLinked = IsDestLinked
380
381-------------------------------------------------------------------------------
382-- Returns true if the destination given is on our band, or if we're connected
383-- to that band. In other words, if the dest is "local" then we can send them
384-- a normal addon whisper.
385local function IsDestLocal( dest )
386 return IsDestLinked( m_my_dest, dest )
387end Proto.IsDestLocal = IsDestLocal
388
389-------------------------------------------------------------------------------
390-- Returns a band name in a readable format. Not /too/ readable, but meant for
391-- the network monitor. e.g. "MG-H" for "1H".
392local function GetBandName( band )
393 local realm, faction = strmatch( band, "(%d+)([AH])" )
394 if not realm then return UNKNOWN end
395 local primo = strbyte( realm, 1 ) ~= 48
396 realm = tonumber( realm )
397 realm = primo and PRIMO_RP_SERVERS[realm] or realm
398 local realmid,_,apiname = LibRealmInfo:GetRealmInfoByID( realm )
399 -- Look for a fancy name we have for the realm, or just use the first 5
400 -- characters.
401 apiname = m_realm_names[apiname] or strsub( apiname, 1, 5 )
402 return strupper(apiname .. "-" .. faction)
403end Proto.GetBandName = GetBandName
404
405
406-------------------------------------------------------------------------------
407-- MISC UTILITY
408-------------------------------------------------------------------------------
409-- This function returns information abouto our bridges, what bands we can
410-- access. Used for the minimap tooltip display.
411-- Returns list of {
412-- band Band index this entry is for.
413-- quota Quots this band has. May be 0, meaning the band has become
414-- unavailable.
415-- secure If we can access this band using our secure channel.
416-- direct If we own a link to this band.
417-- active If we have seen any users on this band recently (by mouseover).
418-- }
419-- This is meant to be diagnostic data only, and shouldn't be considered a
420-- super accurate API (updated only once per second, and may briefly contain
421-- outdated information).
422local m_network_status = {}
423local m_last_network_status_time = 0
424local function GetNetworkStatus()
425 -- This function is kind of heavy, and it doesn't need to be cutting edge
426 -- data, so we cache it every second.
427 local time = GetTime()
428 if time < m_last_network_status_time + 1.0 then
429 return m_network_status
430 end
431 m_last_network_status_time = time
432 wipe( m_network_status )
433
434 for band, set in pairs( m_bridges ) do
435 -- Quota internally is calculated as 1000/load, but in here we convert
436 -- it to 10/load, so a healthy user will have 10 points, double load
437 -- will be 5 points, etc.
438 local qsum = math.ceil(set.quota_sums.all * 10 / 1000)
439 local secure = set.node_counts.secure > 0
440 local includes_self = set.nodes[Me.fullname]
441 table.insert( m_network_status, {
442 band = band;
443 quota = qsum;
444 secure = secure;
445 direct = includes_self;
446 active = GetTime() < (m_active_bands[band] or 0) + BAND_TOUCH_ACTIVE
447 })
448 end
449
450 -- Sort alphabetically by bands.
451 table.sort( m_network_status, function( a, b )
452 return a.band < b.band
453 end)
454
455 return m_network_status
456end Proto.GetNetworkStatus = GetNetworkStatus
457
458-------------------------------------------------------------------------------
459-- Returns true if the user (as reported by the client) is experiencing high
460-- latency, and they shouldn't be hosting.
461local function HasUnsuitableLag()
462 local _,_, home_lag, world_lag = GetNetStats()
463 return max( home_lag, world_lag ) > 500
464end Proto.HasUnsuitableLag = HasUnsuitableLag
465
466-------------------------------------------------------------------------------
467-- Returns an iterator for a player's game accounts. Usually a WoW player is
468-- only logged into one game account, but some people can have two, or even
469-- three. The quicker/more efficient methods can only read from the first
470-- game account, and aren't very useful for our case, where any of those game
471-- accounts can receive Bnet data and act as links.
472-- Iterator returns `fullname`, `faction` (single char), `gameid`
473local BNGetFriendGameAccountInfo = BNGetFriendGameAccountInfo
474local BNET_CLIENT_WOW = BNET_CLIENT_WOW -- (Inner function
475local BNGetFriendIndex = BNGetFriendIndex -- optimizations.)
476local BNGetNumFriendGameAccounts = BNGetNumFriendGameAccounts
477local function GameAccounts( bnet_account_id )
478 local account = 1
479 local friend_index = BNGetFriendIndex( bnet_account_id )
480 local num_accounts = BNGetNumFriendGameAccounts( friend_index )
481 return function()
482 while account <= num_accounts do
483 local _, char_name, client, realm, _, faction,
484 _,_,_,_,_,_,_,_,_, game_account_id
485 = BNGetFriendGameAccountInfo( friend_index, account )
486 account = account + 1
487
488 if client == BNET_CLIENT_WOW then
489 realm = realm:gsub( "[ -]", "" )
490 return char_name .. "-" .. realm, strsub( faction, 1, 1 ),
491 game_account_id
492 end
493 end
494 end
495end Proto.GameAccounts = GameAccounts
496
497-------------------------------------------------------------------------------
498-- This returns a megaiterator for going over all WoW game accounts that are
499-- logged in in your friends list.
500-- Iterator returns `fullname`, `faction` (single char), `gameid`
501local function FriendsGameAccounts()
502 local friend = 1
503 local friends_online = select( 2, BNGetNumFriends() )
504 local account_iterator = nil
505
506 return function()
507 while friend <= friends_online do
508 if not account_iterator then
509 local id, _,_,_,_,_,_, is_online = BNGetFriendInfo( friend )
510 if is_online then
511 account_iterator = GameAccounts( id )
512 end
513 end
514
515 if account_iterator then
516 local name, faction, id = account_iterator()
517 if not name then
518 account_iterator = nil
519 friend = friend + 1
520 else
521 return name, faction, id
522 end
523 else
524 friend = friend + 1
525 end
526 end
527 end
528end Proto.FriendsGameAccounts = FriendsGameAccounts
529
530-------------------------------------------------------------------------------
531-- Reads Battle.net game account info data for the gameid and turns it into a
532-- fullname.
533local function GetFullnameFromGameID( gameid )
534 local _, charname, _, realm, _, faction = BNGetGameAccountInfo( gameid )
535 -- The realm name is a human readable string, but the standard for
536 -- pasting it onto a player name is to remove spaces and dashes.
537 realm = realm:gsub( "[ -]", "" )
538 charname = charname .. "-" .. realm
539 return charname, faction
540end Proto.GetFullnameFromGameID = GetFullnameFromGameID
541
542-------------------------------------------------------------------------------
543-- Shortcut for sending a simple Bnet addon message. `gameid` is the recipient
544-- game account ID, `msg` is the text we're sending. If `secure` is set, then
545-- the message will be sent over the secure channel. Keep in mind that ANY
546-- user can read a "securely" transmitted message over Bnet, even if they
547-- don't have the prefix registered (there are extra authentication steps for
548-- Bnet transfers). `priority` is the message priority, which may be "LOW",
549-- "NORMAL", "FAST", or "URGENT". Defaults to "NORMAL" (in the Comm sublayer).
550local function SendBnetMessage( gameid, msg, secure, priority )
551 if secure and not m_secure_code then
552 DebugLog( "No secure channel for secure BNET message." )
553 return
554 end
555
556 if type( msg ) == "table" then
557 msg = tblconcat( msg, " " )
558 end
559
560 return Comm.SendBnetPacket( gameid, nil, true, msg,
561 secure and m_secure_channel, priority )
562
563end Proto.SendBnetMessage = SendBnetMessage
564
565-------------------------------------------------------------------------------
566-- Shortcut for sending a local addon message. Same-ish arguments as above.
567-- `target` may be a player fullname or "P" to send to party/raid or "*" to
568-- send to the local broadcast channel.
569-- Renamed to SendProtoAddonMessage rather than SendAddonMessage, because the
570-- latter might be confusing (conflicts with the old WoW API).
571local function SendProtoAddonMessage( target, msg, secure, priority )
572 if secure and not m_secure_code then
573 DebugLog( "Tried to send secure ADDON message with no secure channel.")
574 return
575 end
576
577 if type( msg ) == "table" then
578 msg = tblconcat( msg, " " )
579 end
580
581 return Comm.SendAddonPacket( target, nil, true, msg,
582 secure and m_secure_channel, priority )
583
584end Proto.SendProtoAddonMessage = SendProtoAddonMessage
585
586-------------------------------------------------------------------------------
587-- GAME CHANNEL UTILITY FUNCTIONS
588-------------------------------------------------------------------------------
589-- Returns true if the user is currently joined with the channel of `name`.
590--
591local function GameChannelExists( name )
592 return GetChannelName( name ) ~= 0
593end Proto.GameChannelExists = GameChannelExists
594
595-------------------------------------------------------------------------------
596-- Addons joining channels is pretty sketchy, because unless you wait an
597-- indecent amount of time to join the channel, you could be bumping down
598-- channel numbers that the user expects to be maintained. With more recent
599-- version of the WoW client, you can swap channels, so this function moves
600-- the channel specified to the bottom of the list.
601local function MoveGameChannelToBottom( name )
602
603 local index = GetChannelName( name )
604 local last_channel = index
605 if index == 0 then return end
606
607 -- There might be gaps in the channel list for whatever reason, so we scan
608 -- the entire channel list for the last channel.
609 for i = last_channel, MAX_WOW_CHAT_CHANNELS do
610 if GetChannelName( i ) ~= 0 then
611 last_channel = i
612 end
613 end
614
615 for i = index, last_channel-1 do
616 C_ChatInfo.SwapChatChannelsByChannelIndex( i, i + 1 )
617 end
618end Proto.MoveGameChannelToBottom = MoveGameChannelToBottom
619
620-------------------------------------------------------------------------------
621-- Tries to join a game channel, and usually succeeds unless something crazy
622-- is wrong, like the user being in 20 channels already, or the channel being
623-- password protected. `name` is the name of the channel, i.e. /join name.
624-- `onjoin` is a callback function that will be triggered when the channel is
625-- successfully joined. `retries` is an internal (recursion) value.
626local function JoinGameChannel( name, onjoin, retries )
627 if GameChannelExists( name ) then
628 -- Channel exists, our job is done. Move it to the bottom of the
629 -- channel list and pass execution to callback.
630 MoveGameChannelToBottom( name )
631 if onjoin then
632 return onjoin( name )
633 end
634 else
635 if retries and retries <= 0 then
636 if name == Me.data_channel then
637 Me.Print( "Error: couldn't join broadcast channel." )
638 else
639 Me.Print( "Error: couldn't join channel '%s'.", name )
640 end
641 return
642 end
643 JoinTemporaryChannel( name )
644
645 -- Delay one second and continue with recursion, up to 10 retries.
646 -- (or 9???)
647 -- `onjoin or false` - We don't pass nil arguments to the timer
648 -- function as it might confuse the table unpacking when passing the
649 -- args to the callback.
650 Me.Timer_Start( "joinchannel_" .. name, "push", 1.0,
651 JoinGameChannel, name, onjoin or false, (retries or 10) - 1 )
652 end
653end Proto.JoinGameChannel = JoinGameChannel
654
655-------------------------------------------------------------------------------
656-- Leave a game channel. This isn't used - from older designs that used
657-- different broadcast channels for security. Just a complement to our
658-- channel joining function (most importantly, it cancels the joining timer).
659local function LeaveGameChannel( name )
660 Me.Timer_Cancel( "joinchannel_" .. name )
661 if GameChannelExists( name ) then
662 LeaveChannelByName( name )
663 end
664end Proto.LeaveGameChannel = LeaveGameChannel
665
666
667-------------------------------------------------------------------------------
668-- BRIDGING/LINKING
669-------------------------------------------------------------------------------
670-- Returns true if the user specified can take us to `destination`. Used to
671-- validate a bridge when we get a status message from them (a status response
672-- may mean that we sent our message to an invalid route).
673-- For example, after selecting a bridge and sending a routed message to it,
674-- the opposite end might refuse the message and instead send us their status,
675-- which means that they can't send to the destination. When we receive a
676-- status message, we check over our senders and then validate the bridges
677-- used (after our bridge list is updated with the status message).
678-- Senders that aren't guarantee stay alive for a few seconds to wait for
679-- things like this; another thing that can trigger a re-send for non
680-- "guaranteed" messages is sending to an offline player (and seeing the
681-- system message response).
682local function BridgeValid( destination, secure, fullname )
683 -- For most utility functions, we avoid using things like string methods,
684 -- for speed, as utility functions are often found in inner loops.
685 local band = strmatch( destination, "%a*(%d+[AH])" )
686 band = GetLinkedBand( band )
687 local bridge = m_bridges[band]
688 if not bridge then return end
689 return bridge:KeyExists( fullname, secure )
690end Proto.BridgeValid = BridgeValid
691
692-------------------------------------------------------------------------------
693-- Selects a bridge that can reach `destination`. If `secure` is set, then it
694-- only selects secure bridges - users that we have verified to be in our
695-- secure group.
696local function SelectBridge( destination, secure )
697 local band = strmatch( destination, "%a*(%d+[AH])" )
698 band = GetLinkedBand( band )
699 local bridge = m_bridges[band]
700 if not bridge then return end
701 return bridge:Select( secure and "secure" )
702end Proto.SelectBridge = SelectBridge
703
704-------------------------------------------------------------------------------
705-- Selects a link that can reach `destination`. Normally this is called from
706-- the R1 routing message, to find a link to continue to destination, but it
707-- can also be called from Proto.Send, when we select ourselves as a bridge
708-- (and we just bypass the R1 message).
709-- Sometimes this might fail if our link to the destination goes offline or
710-- something, and unfortunately the message likely needs to be just dropped,
711-- as we don't have a direct connection, and in that case we reply to the
712-- user that we're routing for with our status to let them update us and then
713-- pick another bridge.
714-- If `secure` is set, this only selects links that we have verified to be in
715-- our secure group.
716local function SelectLink( destination, secure )
717 local user, band = strmatch( destination, "(%a*)(%d+[AH])" )
718 band = GetLinkedBand( band )
719 local link = m_links[band]
720 if not link then return end
721
722 if user ~= "" then
723 -- This is a routing optimization. If the destination has a user set,
724 -- then we can try targeting them for the R2 message, and then they
725 -- don't need to send an R3 message, as they're the final endpoint.
726 local direct = link:HasBnetLink( destination )
727 if direct then return direct end
728 end
729
730 return link:Select( secure and "secure" )
731end Proto.SelectLink = SelectLink
732
733-------------------------------------------------------------------------------
734-- The self bridge is our name added into the bridge lists. We update it
735-- whenever we gain or lose a link.
736local function UpdateSelfBridge()
737 local loads = {}
738
739 -- Similar operation to us generating a status message and then handling
740 -- it.
741 for band, set in pairs( m_links ) do
742 local avg = set:GetLoadAverage()
743 if avg then
744 if m_secure_code then
745 if set:SubsetCount( "secure" ) > 0 then
746 loads["#"..band] = "secure"
747 end
748 end
749 loads[band] = avg
750 if not m_bridges[band] then
751 m_bridges[band] = Me.NodeSet.Create( {"secure"} )
752 end
753 end
754 end
755
756
757 -- And then update them.
758 for band, bridge in pairs( m_bridges ) do
759 local load = loads[band]
760 if load then
761 bridge:Add( Me.fullname, load, loads["#"..band] )
762 else
763 bridge:Remove( Me.fullname )
764 end
765 end
766end Proto.UpdateSelfBridge = UpdateSelfBridge
767
768-------------------------------------------------------------------------------
769-- `gameid` is Bnet game account ID, `load` is how many users the link is
770-- connected to. Triggered by the `HI` BNET message.
771local function AddLink( gameid, load )
772 load = load or 99
773 local _, charname, _, realm, _, faction = BNGetGameAccountInfo( gameid )
774
775 DebugLog2( "Adding link.", charname, realm, gameid )
776
777 if not charname then
778 -- Sometimes we might not be able to pull their data. This is a fairly
779 -- slim corner case, where we get their message after they log out
780 -- or something similar.
781 return
782 end
783
784 -- Normalize realm and append to character name.
785 realm = gsub( realm, "[ -]", "" )
786 charname = charname .. "-" .. realm
787
788 local band = DestFromFullname( "-" .. realm, faction )
789 if IsDestLinked( band, m_my_band ) then
790 -- We're not supposed to be sent HI messages from the same band.
791 -- Likely will never happen, but it might due to some weird network
792 -- hiccup (or malicious data).
793 return
794 end
795
796 band = GetLinkedBand( band )
797
798 if not m_links[band] then
799 m_links[band] = Me.NodeSet.Create( { "secure" } )
800 end
801
802 local subset
803 -- `m_node_secure_data` records whatever secure data we see, and it's easy
804 -- access and optimized to let us know if any player (fullname) or
805 -- game account is on a secure channel.
806 if m_node_secure_data[gameid] and m_node_secure_data[gameid].secure then
807 subset = "secure"
808 end
809
810 -- `link_ids` is links that are actively hosting. `crossrp_gameids` are
811 -- game IDs that have Cross RP installed.
812 m_link_ids[gameid] = true
813 m_crossrp_gameids[gameid] = true
814
815 m_links[band]:Add( gameid, load, subset )
816
817 UpdateSelfBridge()
818end Proto.AddLink = AddLink
819
820-------------------------------------------------------------------------------
821-- Removes a player link from our link sets. Examples of how this is called are
822-- when a link goes offline, when a link sends us a BYE message (when they
823-- /reload), or when they stop hosting (sending a HI message with 0 load).
824-- `unset_crossrp` clears the Cross RP user tag for their game ID. Essentially,
825-- if this is triggered from a normal status message, they still have Cross RP
826-- and should be treated as such; but if they go offline or send us a BYE
827-- message, then they might not have Cross RP after, so we clear the flag.
828local function RemoveLink( gameid, unset_crossrp )
829 DebugLog2( "Removing link.", gameid )
830 for k, v in pairs( m_links ) do
831 if v:Remove( gameid ) then
832 m_status_broadcast_time = 0
833 m_status_broadcast_fast = true
834 end
835 end
836 m_link_ids[gameid] = nil
837
838 if unset_crossrp then
839 m_crossrp_gameids[gameid] = nil
840 else
841 m_crossrp_gameids[gameid] = true
842 end
843
844 UpdateSelfBridge()
845end Proto.RemoveLink = RemoveLink
846
847-------------------------------------------------------------------------------
848-- Update a bridge with data we've received from an ST message. `sender` is
849-- a fullname. `bands` is a list of bands that they can handle for us, in the
850-- status format, e.g. "1A1 #1H2", the hash means they can contact that
851-- destination securely (with their secure key).
852-- We don't pass secure data into here, because it's updated in another method
853-- before this call. We just read m_node_secure_data for the sender.
854local function UpdateBridge( sender, bands )
855 -- All of the bands in here should be LINKED bands. If they aren't, then it
856 -- won't really cause /much/ trouble (because we will never select bad
857 -- bands).
858 local loads = {}
859 local secure_bridge = m_node_secure_data[sender] and
860 m_node_secure_data[sender].secure
861 for secure, band, load in bands:gmatch( "(#?)(%d+[AH])([0-9]+)" ) do
862 -- Each entry is <securemark><realmid><faction><load>.
863 load = tonumber( load )
864
865 if load < 1 or load > 99 then
866 -- Basically, the user gave us some invalid data, so just treat
867 -- them as an invalid/empty bridge.
868 wipe( loads )
869 break
870 end
871
872 loads[band] = tonumber( load )
873 if secure_bridge and secure ~= "" then
874 loads["#" .. band] = "secure"
875 end
876
877 -- Create any bridge sets that don't exist.
878 if not m_bridges[band] then
879 m_bridges[band] = Me.NodeSet.Create( {"secure"} )
880 end
881 end
882
883 for band, bridge in pairs( m_bridges ) do
884 local load = loads[band]
885 if load then
886 bridge:Add( sender, load, loads["#" .. band] )
887 else
888 bridge:Remove( sender )
889 end
890 end
891end Proto.UpdateBridge = UpdateBridge
892
893-------------------------------------------------------------------------------
894-- Remove a bridge from our registry. Examples of sources calling this are when
895-- a client times out, when a client stops hosting, when a client loses all of
896-- their links, or when a client failed a message on us.
897local function RemoveBridge( sender )
898 for band, bridge in pairs( m_bridges ) do
899 bridge:Remove( sender )
900 end
901end Proto.RemoveBridge = RemoveBridge
902
903
904-------------------------------------------------------------------------------
905-- SENDING
906-------------------------------------------------------------------------------
907-- This is the main function to process a sender's UMID (and send it). Senders
908-- usually only have one UMID, but for some messages there can be UMIDs for
909-- all bands, such as RP chat which is broadcast to everywhere.
910local function ProcessSenderUMID( sender, umid )
911
912 -- Local caches for speed. Accessing local variables is like 5 times
913 -- faster.
914 local data = sender.umids[umid]
915 local datatime, sendermsg, guarantee, datatries, time, datadest =
916 data.time, sender.msg, sender.guarantee, data.tries, GetTime(),
917 data.dest
918
919 -- If `time` isn't set, then we need to send this message. `time` can be
920 -- reset if the message needs to be resent due to failures.
921 -- If this is a "guaranteed" message, then we also pass this condition to
922 -- "re-send" if the ack times out. The timeout for that depends on how
923 -- long the message being sent was (because longer messages can take quite
924 -- a longer time to transfer).
925 if not datatime or (guarantee and time > datatime +
926 ACK_TIMEOUT + ACK_TIMEOUT_PER_BYTE * #sendermsg) then
927 if datatries >= 5 then
928 -- We've already tried to send this message 5 times, so we give up.
929 -- Something is wrong.
930 if sender.callback then
931 sender.callback( sender, "TIMEOUT", data )
932 end
933 -- Resetting the `umid` entry basically cancels this sending task.
934 sender.umids[umid] = nil
935 return
936 end
937
938 -- I'm not actually entirely sure why we would have "local" data being
939 -- sent from one of these. Completion sake, I guess?
940 if IsDestLocal( datadest ) then
941 if sender.ack then
942 -- Ack messages shouldn't be sent locally.
943 error( "Internal error." )
944 end
945 local localplayer = strmatch( datadest, "(%a*)%d+[AH]" )
946 local target
947
948 if localplayer ~= "" then
949 target = DestToFullname( datadest )
950 else
951 target = "*"
952 end
953
954 -- R0 is basically a simple wrapper. Doesn't need any routing info
955 -- added because the source is the sender and the destination is
956 -- the receiver, just like a normal addon message.
957 SendProtoAddonMessage( target, {"R0", sender.msg}, sender.secure,
958 sender.priority )
959 if sender.callback then
960 sender.callback( sender, "LOCAL_SENT", data )
961 end
962 sender.umids[umid] = nil
963 return
964 end
965
966 -- If `r1_bridge` is set, then we're re-sending this message after a
967 -- failure. We disable that bridge until they send us another status
968 -- message. (If it's not the bridge's fault, then unset `r1_bridge`
969 -- to preserve it.)
970 local r1_bridge = data.r1_bridge
971 if r1_bridge and r1_bridge ~= Me.fullname then
972 -- This bridge failed us...
973 RemoveBridge( r1_bridge )
974 data.r1_bridge = nil
975 end
976
977 -- Sending or re-sending message.
978
979 -- Find a bridge.
980 local bridge = SelectBridge( datadest, sender.secure )
981 if not bridge then
982 -- No available route, cancel this message with an error callback.
983 if sender.callback then
984 sender.callback( sender, "NOBRIDGE", data )
985 end
986 sender.umids[umid] = nil
987 return
988 end
989
990 -- Flags are a string of characters attached to a message. Currently
991 -- includes these:
992 -- 'G' Guarantee - An ack is requested from the last router.
993 --
994 local flags = guarantee and "G" or "-"
995
996 if bridge == Me.fullname then
997 local link = SelectLink( datadest, sender.secure )
998 if not link then
999 if sender.callback then
1000 sender.callback( sender, "NOBRIDGE", data )
1001 end
1002
1003 -- This should be a logical error, as we SHOULD have link data
1004 -- if we are a valid bridge selected above. Something wasn't
1005 -- updated.
1006 Me.DebugLog( "Should have link here. Investigate." )
1007 sender.umids[umid] = nil
1008 return
1009 end
1010
1011 if sender.ack then
1012 -- A2 (acknowledgement checkpoint 2) format is:
1013 -- "A2 [UMID] [DESTINATION]"
1014 SendBnetMessage( link, {"A2", umid, datadest},
1015 sender.secure, sender.priority )
1016 else
1017 -- R2 (routing checkpoint 2) format is:
1018 -- "R2 [UMID] [FLAGS] [SOURCE] [DESTINATION] [DATA..."
1019 SendBnetMessage( link,
1020 {"R2", umid, flags, m_my_dest, datadest, sendermsg},
1021 sender.secure, sender.priority )
1022 end
1023
1024 else
1025 -- R1 (routing checkpoint 1) format is:
1026 -- "R1 [UMID] [FLAGS] [DESTINATION] [DATA..."
1027 -- R1 doesn't have a source field, because the source is always
1028 -- the sender.
1029 -- A1 (acknowledgement checkpoint 1) format is:
1030 -- "A1 [UMID] [DESTINATION]"
1031 -- Basically, ack messages are routing messages but stripped of any
1032 -- message overhead.
1033 if sender.ack then
1034 SendProtoAddonMessage( bridge, {"A1", umid, datadest},
1035 sender.secure, sender.priority )
1036 else
1037 SendProtoAddonMessage( bridge,
1038 {"R1", umid, flags, datadest, sendermsg},
1039 sender.secure, sender.priority )
1040 end
1041
1042 -- Save the bridge used, so that if there's a failure, we can
1043 -- disable them.
1044 data.r1_bridge = bridge
1045 end
1046
1047 data.tries = datatries + 1
1048 data.time = time
1049
1050 -- The "SENT" event does not guarantee delivery. For guaranteed
1051 -- messages, the "CONFIRMED" event is after the acknowledgement
1052 -- is received.
1053 if sender.callback then
1054 sender.callback( sender, "SENT", data )
1055 end
1056 elseif datatime and
1057 (not guarantee and time >= datatime + ONLINE_TIMEOUT) then
1058 -- This is for non guaranteed messages (which are most common),
1059 -- otherwise this bit is done after the ACK is received.
1060 -- The "DONE" event is triggered after a sender is completely
1061 -- done, and there will be no further callbacks for that sender.
1062
1063 -- What's happening here, is we waited a short amount of time after
1064 -- sending. This wait is just to catch if we're sending to an offline
1065 -- player or an invalid route. In the first case, we intercept the
1066 -- system message, remove the bridge, and then retry (by scanning our
1067 -- sender list and resetting any that were using that bridge). In the
1068 -- second case, the R1 checkpoint will reply to us a ST message, and
1069 -- if we see that they no longer have the link required in their ST
1070 -- message, then we reset the sender and try again.
1071
1072 -- Anyway, if the short wait expires, then we assume that the message
1073 -- went through (it may fail later down the line though).
1074 sender.umids[umid] = nil
1075 if not next( sender.umids ) then
1076 m_senders[sender] = nil
1077 if sender.callback then
1078 sender.callback( sender, "DONE" )
1079 end
1080 end
1081 end
1082end Proto.ProcessSenderUMID = ProcessSenderUMID
1083
1084-------------------------------------------------------------------------------
1085-- Update function for senders. Called periodically by Proto.Update, and also
1086-- called when a sender is changed by other methods and events related to its
1087-- operation.
1088local function ProcessSender( sender )
1089 -- Don't process this sender if it was removed from the sender table
1090 -- already.
1091 if not m_senders[sender] then return end
1092
1093 for umid, _ in pairs( sender.umids ) do
1094 ProcessSenderUMID( sender, umid )
1095 end
1096
1097 -- Check if m_senders[sender] is unset already before triggering this, as
1098 -- that can be done in the child function. Right now we don't really need
1099 -- this bit, and it's more of just a safety net.
1100 if not next(sender.umids) and m_senders[sender] then
1101 m_senders[sender] = nil
1102 if sender.callback then
1103 sender.callback( sender, "DONE" )
1104 end
1105 end
1106end Proto.ProcessSender = ProcessSender
1107
1108-------------------------------------------------------------------------------
1109-- This is called when we receive an ST message from someone. We check over our
1110-- senders, and any that were using that someone verifies if the bridge is
1111-- still valid for the destination they want to reach. Routers can especially
1112-- respond with an ST message when they can't reach the destination we want.
1113-- This is rare, because ST is also broadcast fairly instantly when a bridge
1114-- loses a destination band entirely. `route_fullname` is the fullname of the
1115-- sender of the ST message.
1116local function CheckSenderRoutes( route_fullname )
1117 -- It'll probably be pretty common that we aren't sending anything, because
1118 -- there can be a lot of status messages that trigger in a high population
1119 -- setting.
1120 if not next(m_senders) then return end
1121
1122 -- Copy our sender table, because the ProcessSender function might leave it
1123 -- in a bad state for iterating.
1124 local senders_copy = {}
1125 for _, sender in pairs( m_senders ) do
1126 senders_copy[ #senders_copy ] = sender
1127 end
1128
1129 for _, sender in pairs( senders_copy ) do
1130 local process = false
1131 for umid, data in pairs( sender.umids ) do
1132 -- `r1_bridge` is the fullname of the bridge we chose to start
1133 -- routing.
1134 if data.r1_bridge == route_fullname then
1135 if not BridgeValid( data.r1_bridge ) then
1136 -- Found an invalidated bridge, and we need to resend.
1137 data.time = nil
1138 data.r1_bridge = nil
1139
1140 -- Undo the try, because we caught something wrong.
1141 data.tries = data.tries - 1
1142 process = true
1143 end
1144 end
1145 end
1146
1147 -- Overall, this function (CheckSenderRoutes) seems fairly heavy for
1148 -- something that's triggered for every status message; but most of
1149 -- these conditions are rare and it should be minimal.
1150 if process then
1151 Proto.ProcessSender( sender )
1152 end
1153 end
1154end Proto.CheckSenderRoutes = CheckSenderRoutes
1155
1156-------------------------------------------------------------------------------
1157-- This generates a new UMID, using our prefix and incrementing serial. Used
1158-- whenever we are about to send a message to a destination. UMIDs are unique
1159-- per message and destination, but they may be reused if the same message is
1160-- being re-sent.
1161local function GenerateUMID()
1162 local umid = m_umid_prefix .. m_next_umid_serial
1163 m_next_umid_serial = m_next_umid_serial + 1
1164 return umid
1165end Proto.GenerateUMID = GenerateUMID
1166
1167-------------------------------------------------------------------------------
1168-- Triggered when we receive an A3 message, or an A2 message with us as the
1169-- destination. Basically tells us that a UMID was received by the last
1170-- routing point and our message should have been received by the final
1171-- destination (unless they have logged off or something). In particular, acks
1172-- are sent AFTER the R3 is put out on the line, so if we see the A3, then
1173-- the R3 most definitely went through as well.
1174local function OnAckReceived( umid )
1175 DebugLog( "Got ACK for %s.", umid )
1176 for k, sender in pairs( m_senders ) do
1177 for sender_umid, data in pairs( sender.umids ) do
1178 if sender_umid == umid then
1179 -- "CONFIRMED_DEST" is triggered when we receive an ack for a
1180 -- UMID we have sent out. "CONFIRMED" is also triggered, but
1181 -- only after a sender is entirely finished (for senders with
1182 -- multiple destinations). "DONE" is also still triggered for
1183 -- "guaranteed" senders, when everything is finished.
1184 sender.umids[umid] = nil
1185 if sender.callback then
1186 sender.callback( sender, "CONFIRMED_DEST", data.dest )
1187 end
1188
1189 if not next( sender.umids ) then
1190 if sender.callback then
1191 sender.callback( sender, "CONFIRMED" )
1192 sender.callback( sender, "DONE" )
1193 end
1194 m_senders[sender] = nil
1195 end
1196 return
1197 end
1198 end
1199 end
1200
1201 -- This isn't really an error down here, because with high network
1202 -- congestion, we might get two ACKs if we happen to send a message twice
1203 -- just because of high latency (both go through, but just take a long
1204 -- time).
1205 DebugLog( "Got ACK but couldn't match the UMID (%s).", umid )
1206end Proto.OnAckReceived = OnAckReceived
1207
1208-- Converting the localized message for
1209-- "No player named ... is currently playing."
1210-- into a Lua pattern to extract the name. Better compatibility than having
1211-- localized patterns ourself (but hopefully our conversion pattern here
1212-- works for all locales).
1213local SYSTEM_PLAYER_NOT_FOUND_PATTERN =
1214 ERR_CHAT_PLAYER_NOT_FOUND_S:gsub( "%%s", "(.+)" )
1215
1216-------------------------------------------------------------------------------
1217-- Handler for CHAT_MSG_SYSTEM game event. The only system message we're
1218-- interested in is the one that triggers from sending to an offline player.
1219local function OnChatMsgSystem( event, msg )
1220 if not Proto.startup_complete then return end
1221
1222 local name = strmatch( msg, SYSTEM_PLAYER_NOT_FOUND_PATTERN )
1223 -- This might be an ambiguated name, and in that case we won't end up doing
1224 -- anything because our senders always use fullnames.
1225 if name then
1226 DebugLog( "Removing offline player %s.", name )
1227 RemoveBridge( name )
1228 local to_process
1229 for k, sender in pairs( m_senders ) do
1230 for sender_umid, data in pairs( sender.umids ) do
1231 if data.r1_bridge == name then
1232 data.time = nil
1233
1234 -- Update this sender.
1235 to_process = to_process or {}
1236 to_process[#to_process] = sender
1237 end
1238 end
1239 end
1240
1241 if to_process then
1242 for _, sender in pairs( to_process ) do
1243 ProcessSender( sender )
1244 end
1245 end
1246 end
1247end Proto.OnChatMsgSystem = OnChatMsgSystem
1248
1249-------------------------------------------------------------------------------
1250-- This is our main sending API. The simplest example of using this is:
1251-- Proto.Send( "Catnia1H", "hoi" )
1252-- This will send the message "hoi" to Catnia on Moon Guard (1) Horde, no
1253-- matter where you are located, so long as you have a bridge to that band.
1254-- To receive the message, it must be registered through
1255-- `Proto.SetMessageHandler`.
1256-- e.g. `Proto.SetMessageHandler( "hoi", MyHandler )`
1257--
1258-- `dest` can be:
1259-- - "local" Send (broadcast) to our Cross RP channel.
1260-- - "all" Send (broadcast) to all bands that we know of.
1261-- - "active" Send (broadcast) to all bands that we ahve touched recently.
1262-- - <band> A band name sends a broadcasted message to that band.
1263-- - <dest> A complete destination sends a whispered message to that user.
1264-- - <local dest> A local destination is something that "works" but that's
1265-- just an odd request.
1266-- `msg` can either be a table or a string. If it's a table, the resulting
1267-- message is the entries concatenated together with spaces.
1268-- `options` is an optional table with all entries inside optional:
1269-- secure Boolean - send securely.
1270-- priority "LOW", "NORMAL", "FAST", or "URGENT" for the Comm layer.
1271-- guarantee Basically guarantees delivery, resending if it fails (requests
1272-- an ACK from the routers).
1273-- callback A callback to receive events about the sending status.
1274-- The callback function has the signature ( sender, event, data )
1275-- `sender` is the sender object. It will mirror the same fields that are
1276-- in the `options` passed in, so you can share userdata that way (just
1277-- watch out for internal data). `data` is the per-destination data,
1278-- containing the UMID and other things like time sent, what bridge, etc.
1279-- Events can be triggered at the sender level or the destination level. A
1280-- sender only has one message, but it can mirror that message to multiple
1281-- destinations. Per-destination events are as follows:
1282-- TIMEOUT Sending failure due to unknown network problems. (NOT sure
1283-- if the message went through or not).
1284-- LOCAL_SENT A message was sent locally (and no further verification will
1285-- be done for that message.
1286-- NOBRIDGE Couldn't send to a destination, because we have no bridge
1287-- for them. (Likely became unavailable after the sending
1288-- started).
1289-- SENT A message was put out on the line. Multiple SENTs can be
1290-- triggered, for each attempt we send the data.
1291-- CONFIRMED_DEST This is triggered after we receive an ACK for a single
1292-- destination.
1293-- Per-sender events are as follows:
1294-- DONE Triggered when a sender is completely done. This is always
1295-- the final event.
1296-- CONFIRMED Triggered when a sender has been ack'd completely, meaning a
1297-- guaranteed message was transferred successfully. "DONE"
1298-- followsimmediately after.
1299-- Finally, one thing to really keep in mind, that while this is super
1300-- convenient and all, this routing protocol is based upon TRUST. There are
1301-- a lot of opportunities for man-in-the-middle attacks or falsified
1302-- information being passed around. Due to this very reason, we do not
1303-- endorse any type of "global" Cross RP chat, as impersonation would be both
1304-- too easy to do, and too hard to trace.
1305local function Send( dest, msg, options )
1306 options = options or {}
1307 local secure = options.secure
1308 if secure and not m_secure_code then
1309 DebugLog2( "Tried to send secure message outside of secure mode." )
1310 return
1311 end
1312
1313 if type(msg) == "table" then
1314 msg = tblconcat( msg, " " )
1315 end
1316
1317 -- Local dest, which translates to a single message to our local band.
1318 if dest == "local" then dest = { m_my_band } end
1319
1320 if dest == "all" or dest == "active" then
1321 -- "all" and "active" transfer to all bands we know, or any active
1322 -- bands we know.
1323 local send_to = { m_my_band }
1324 local time = GetTime()
1325 for k, v in pairs( m_bridges ) do
1326 if not v:Empty( secure and "secure" ) then
1327 local active =
1328 time < (m_active_bands[k] or 0) + BAND_TOUCH_ACTIVE
1329 if dest == "all" or active then
1330 table.insert( send_to, k )
1331 end
1332 end
1333 end
1334
1335 dest = send_to
1336 end
1337
1338 -- `dest` may be a string, meaning a single destination, so wrap that
1339 -- in a table.
1340 if type(dest) == "string" then dest = {dest} end
1341
1342 -- We reuse the options table as our sender object.
1343 local sender = options
1344 sender.umids = dest -- And we reuse the `dest` table as our umid table.
1345 sender.msg = msg
1346
1347 for i = 1, #dest do
1348 -- Populate our UMID table, each one of these is a message that
1349 -- we need to send. One per band for broadcasts, and one per player
1350 -- that we whisper. Currently there aren't any multi-whisper types
1351 -- of messages in our program.
1352 local umid = GenerateUMID()
1353 dest[umid] = { umid = umid, dest = dest[i], tries = 0 }
1354 dest[i] = nil
1355 end
1356
1357 m_senders[sender] = sender
1358 ProcessSender( sender )
1359end Proto.Send = Send
1360
1361-------------------------------------------------------------------------------
1362-- Registers a handler for a message type. Triggered when we receive a message
1363-- through the routing protocol.
1364-- The `command` is the first word of the message (e.g. "RP1" for an RP chat
1365-- message). The signature of the handler is `( source, text, complete )`.
1366-- `source` is the message's source, a player "destination". `text` is the
1367-- message contents. For short messages, `complete` will always be true. For
1368-- longer messages, `complete` may be false, which means the callback is being
1369-- triggered for transfer progress, and can be triggered multiple times like
1370-- that. If you're only interested in complete messages, make sure this is
1371-- true.
1372-- When `complete` is false, you are still guaranteed the first chunk of the
1373-- message, so you can read header data and act accordingly (to display a
1374-- transfer-in-progress message or some such).
1375-- The handler can only trigger once with `complete` true. For short messages
1376-- that fit into a single chunk, `complete` will always be true, and you don't
1377-- need to check it.
1378local function SetMessageHandler( command, handler )
1379 m_message_handlers[command] = handler
1380end Proto.SetMessageHandler = SetMessageHandler
1381
1382-------------------------------------------------------------------------------
1383-- Currently only one handler can be registered per command.
1384local function GetMessageHandler( command )
1385 return m_message_handlers[command]
1386end Proto.GetMessageHandler = GetMessageHandler
1387
1388-------------------------------------------------------------------------------
1389-- Called from our R3 or R2 (with us as the destination) to process a routed
1390-- message received.
1391local function OnMessageReceived( source, umid, text, job )
1392 -- I think it might be possible to receive a message even if we haven't
1393 -- finished initialization. In that case, just ignore it.
1394 if not Proto.startup_complete then return end
1395 local prefix, complete = job.prefix, job.complete
1396
1397 if umid and complete then
1398 local time = GetTime()
1399 local sumid = source .. "-" .. umid
1400 local seen = m_seen_umids[sumid]
1401 if seen and time < seen + 60*10 then
1402 -- We might get duplicate messages if they are resent due to network
1403 -- problems, so we filter them out here. Only process them once.
1404 DebugLog2( "Ignoring duplicate UMID." )
1405 return
1406 end
1407
1408 -- This table is cleaned up in a routine function.
1409 m_seen_umids[sumid] = time
1410 end
1411
1412 DebugLog2( "Proto Msg", complete, source, text )
1413 local command = strmatch( text, "^%S+" )
1414 if not command then return end
1415 if prefix ~= "" and m_secure_channel ~= prefix then
1416 -- If this happens, someone might have outdated information of us, or
1417 -- there is a larger logical concern somewhere that needs to be fixed.
1418 DebugLog2( "Got secure message, but we aren't listening to it." )
1419 return
1420 end
1421
1422 -- Pass the message to the registered handler.
1423 local handler = m_message_handlers[command]
1424 if handler then
1425 handler( source, text, complete )
1426 end
1427end Proto.OnMessageReceived = OnMessageReceived
1428
1429
1430-------------------------------------------------------------------------------
1431-- STATUS
1432-------------------------------------------------------------------------------
1433-- Sends an ST (status) message. This message is what tells other clients that
1434-- they can use us as a bridge (or if they can no longer use us as a bridge,
1435-- if our links go offline or if we stop hosting).
1436-- `target` may be `nil` or a local player (fullname). If it's a player, our
1437-- status is whispered to them. This is done especially when a player logs in
1438-- and requests everyone's status (`do_request`). Otherwise, the status is
1439-- broadcast to our local channel.
1440-- `priority` is what priority we use for this status. Defaults to "NORMAL",
1441-- but there are some special circumstances that cause broadcasts to go at a
1442-- higher priority (like when we lose a link, we want that to be "FAST".
1443-- `do_request` sets the request bit in the status, causing anyone who sees the
1444-- broadcast message to reply to us by whispering their status.
1445--
1446-- Currently status is sent in three cases:
1447-- * Periodic broadcast to update players. (If we don't do this, the players
1448-- will treat us as "timed out" and not send us any data).
1449-- * As a whispered response to ST messages with the request flag set.
1450-- * As a whispered response to players who try to route messages through us
1451-- but we can't reach the destination.
1452local function BroadcastStatus( target, priority, do_request )
1453 local secure_hash1, secure_hash2 = "-", "-"
1454 local bands = "-"
1455
1456 if m_hosting then
1457 bands = {}
1458 if m_secure_code then
1459 secure_hash1, secure_hash2 = strsub( m_secure_hash, 1, 12 ),
1460 strsub( m_secure_myhash, 1, 8 )
1461 end
1462 -- Building a list of bands in the format <#><band><load>, colon
1463 -- separated. Load is the average load of all of our links. This isn't
1464 -- super optimal; it's simple. The hash prefix is for any "secure"
1465 -- links we have, meaning that we have verified them to be using the
1466 -- same secure credentials that we are broadcasting (hash1, hash2).
1467 for band, set in pairs( m_links ) do
1468 local avg = set:GetLoadAverage()
1469 if avg then
1470 local secure_mark = ""
1471 if m_secure_code then
1472 if set:SubsetCount( "secure" ) > 0 then
1473 secure_mark = "#"
1474 end
1475 end
1476 bands[#bands + 1] = secure_mark .. band .. avg
1477 end
1478 end
1479 bands = tblconcat( bands, ":" )
1480 if bands == "" then bands = "-" end
1481 end
1482
1483 -- The request bit of the status message is "-" or "?"
1484 local request = "-"
1485 if not target and do_request then
1486 request = "?"
1487 end
1488
1489 if bands == "-" then
1490 -- We don't repeatedly broadcast empty status messages. If a user isn't
1491 -- hosting, or has no links, they only send the "empty" status once,
1492 -- and then wait until they are a useful host.
1493 if m_status_last_sent_empty then
1494 return
1495 end
1496
1497 m_status_last_sent_empty = true
1498 else
1499 m_status_last_sent_empty = false
1500 end
1501
1502 local priority = priority or "NORMAL"
1503
1504 if not target then
1505 -- This stuff is only for our periodically broadcast status.
1506 -- Cancel any previous status message pending, which could be a
1507 -- "disabled" status that might be in the queue.
1508 Comm.CancelSendByTag( "st" )
1509 -- `status_broadcast_fast` is set when we lose a link, so we prioritize
1510 -- sending our status to prevent them from trying to route over that
1511 -- lost link.
1512 priority = m_status_broadcast_fast and "FAST" or priority
1513 m_status_broadcast_fast = false
1514 end
1515
1516 -- Status format is:
1517 -- ST [VERSION] [REQUEST] [SECUREHASH1] [SECUREHASH2] [BANDLIST]
1518 -- [VERSION] is our Cross RP version in full.
1519 -- [REQUEST] is "-" or "?", "?" means we are requesting everyone's status.
1520 -- [SECUREHASH] are for secure-mode, letting people know that we are in
1521 -- their secure group.
1522 -- [BANDLIST] is a colon separated band list, containing the band ID and a
1523 -- load number, so people can do some load balancing when
1524 -- selecting bridges.
1525 DebugLog2( "Sending status.", target )
1526 local job = SendProtoAddonMessage( target or "*",
1527 {"ST", Me.version, request, secure_hash1, secure_hash2, bands},
1528 false, priority )
1529 -- Tag this as "st" so we can cancel it in the queue if we overwrite it
1530 -- with a newer status message, or a disabled/logout message.
1531 job.tags = {"st"}
1532end Proto.BroadcastStatus = BroadcastStatus
1533
1534-------------------------------------------------------------------------------
1535-- Sends "HI" messages over Battle.net; this message is basically our status
1536-- message meant for links, sent during login to probe our friends list, as
1537-- well as sent periodically to let them know we're still alive.
1538-- `gameids` is either a table of gameids that we should send our HI message
1539-- to. The VALUES of this table are ignored, and only the keys matter.
1540-- i.e. `table[gameid_to_send_to] = true`
1541-- `request` makes the message a request type, meaning that whoever receives it
1542-- will respond with another HI message, a status reply.
1543-- `load_override` overwrites the reported load we have, set during
1544-- initialization to something higher than normal so we don't get a burst of
1545-- traffic if we're about to end up as a "heavy-load" node.
1546-- `priority` is the Comm priority to send this message. Defaults to "LOW".
1547--
1548local function SendHI( gameids, request, load_override, priority )
1549 if type(gameids) == "number" then
1550 gameids = {[gameids] = true}
1551 end
1552
1553 local load
1554 if m_hosting then
1555 load = min( max( load_override or #m_links, 1 ), 99 )
1556 else
1557 load = 0
1558 end
1559
1560 local short_passhash = strsub( m_secure_hash or "-", 1, 12 )
1561 local passhash = strsub( m_secure_myhash or "-", 1, 8 )
1562
1563 local request_mode = request and "?" or "-"
1564
1565 -- Format for the Bnet status message is:
1566 -- HI [VERSION] [REQUESTBIT] [LOAD] [HASH1] [HASH2]
1567 -- [VERSION] is our full Cross RP version.
1568 -- [REQUESTBIT] is "?" if we are requesting everyone's status.
1569 -- [LOAD] is how many links we have.
1570 -- [HASH1]/[HASH2] are our secure hashes.
1571
1572 for id, _ in pairs( gameids ) do
1573 local job = SendBnetMessage( id,
1574 {"HI", Me.version, request_mode, load, short_passhash, passhash},
1575 false, priority or "LOW" )
1576 job.tags = {"hi"}
1577 end
1578end Proto.SendHI = SendHI
1579
1580-------------------------------------------------------------------------------
1581-- We call this to broadcast our Bnet status (HI) to our friends list. Sort of
1582-- a simple wrapper for SendHI.
1583-- `all` is for initialization purposes, where we send to everyone rather than
1584-- only to who we have registered as a Cross RP user. After initialization, we
1585-- maintain a list of who we have seen as a Cross RP user, and send to only
1586-- them. First global broadcast is a probe.
1587-- `request`, `load_override` and `priority` are passed to `SendHI`.
1588--
1589local function BroadcastBnetStatus( all, request, load_override, priority )
1590 local send_to
1591 if all then
1592 -- We don't quite send to 'everyone' - only to people who are on
1593 -- different [linked] bands than us.
1594 send_to = {}
1595 for charname, faction, game_account in FriendsGameAccounts() do
1596 local realm = charname:match( "%-(.+)" )
1597 if not m_linked_realms[realm] or faction ~= Me.faction then
1598 send_to[game_account] = true
1599 end
1600 end
1601 else
1602 send_to = m_crossrp_gameids
1603 end
1604
1605 -- Something to watch out for here, is if we happen to cancel any pending
1606 -- HI messages that haven't gone out to unprobed destinations, they will
1607 -- forever remain invisible to us. Might happen in extreme conditions
1608 -- where the user has a ton of friends to probe at the start, where the
1609 -- requests get clogged in the queue longer than the startup wait period.
1610 Comm.CancelSendByTag( "hi" )
1611 SendHI( send_to, request, load_override, priority )
1612end Proto.BroadcastBnetStatus = BroadcastBnetStatus
1613
1614
1615-------------------------------------------------------------------------------
1616-- ROUTINE
1617-------------------------------------------------------------------------------
1618-- When we start hosting, we allow players to route messages through us. This
1619-- is the main switch ON.
1620local function StartHosting()
1621 if m_hosting then return end
1622
1623 if BNGetNumFriends() == 0 then
1624 -- Sometimes Battle.net won't work correctly during the session, and
1625 -- doesn't report any friends. In that case, we can' host.
1626 if not Proto.warned_bnet_down then
1627 Proto.warned_bnet_down = true
1628 DebugLog2( "Battle.net is down for this session. Cannot host." )
1629 end
1630 return
1631 end
1632
1633 m_hosting = true
1634 m_hosting_time = GetTime()
1635
1636 -- Trigger a status message to let people know we're started up.
1637 m_status_broadcast_time = 0
1638end Proto.StartHosting = StartHosting
1639
1640-------------------------------------------------------------------------------
1641-- This doesn't entirely stop hosting, as some players might still choose us
1642-- as a route until they register our status message. There's a grace period
1643-- that allows people to finish any messages they're busy transferring through
1644-- us.
1645local function StopHosting()
1646 if not m_hosting then return end
1647 m_hosting = false
1648 BroadcastStatus()
1649 BroadcastBnetStatus( false, false )
1650end Proto.StopHosting = StopHosting
1651
1652-------------------------------------------------------------------------------
1653-- Triggered during /reload or logging out. Unfortunately in the latter case,
1654-- the game doesn't actually send our logout messages.
1655local function Shutdown()
1656 Comm.CancelSendByTag( "st" )
1657
1658 -- Broadcast "BYE". "URGENT" means that we will call SendAddonMessage
1659 -- directly in this call, bypassing the chat throttler.
1660 Comm.SendAddonPacket( "*", nil, true, "BYE", nil, "URGENT" )
1661
1662 -- And also broadcast "BYE" to Battle.net friends on different bands.
1663 for charname, faction, game_account in FriendsGameAccounts() do
1664 local realm = charname:match( "%-(.+)" )
1665 if not m_linked_realms[realm] or faction ~= Me.faction then
1666 Comm.SendBnetPacket( game_account, nil, true, "BYE", nil,
1667 "URGENT" )
1668 end
1669 end
1670end Proto.Shutdown = Shutdown
1671
1672-------------------------------------------------------------------------------
1673-- Returns true if we're currently hosting. `include_grace_period` also makes
1674-- us return true if we aren't hosting, but are within the grace period.
1675local function IsHosting( include_grace_period )
1676 if m_hosting then
1677 return true
1678 else
1679 if include_grace_period and
1680 (GetTime() < m_hosting_time + HOSTING_GRACE_PERIOD) then
1681 return true
1682 end
1683 end
1684end Proto.IsHosting = IsHosting
1685
1686-------------------------------------------------------------------------------
1687-- Called periodically, a maintenance function to clean up any old entries in
1688-- our `seen_umids` table.
1689local function CleanSeenUMIDs()
1690 Me.Timer_Start( "proto_clean_umids", "push", 35.0, CleanSeenUMIDs )
1691 local time, seen_umids = GetTime() + 300, m_seen_umids
1692
1693 for k, v in pairs( seen_umids ) do
1694 if time > v then
1695 seen_umids[k] = nil
1696 end
1697 end
1698end Proto.CleanSeenUMIDs = CleanSeenUMIDs
1699
1700-------------------------------------------------------------------------------
1701-- Triggered periodically and through the BN_FRIEND_INFO_CHANGED message.
1702-- If we lose a path to a destination band, then we reset the status broadcast
1703-- timer, and call Update.
1704-- If `run_update` is false, we won't call `Update` (set to false when running
1705-- from inside `Update`).
1706local function PurgeOfflineLinks( run_update )
1707 for gameid,_ in pairs( m_link_ids ) do
1708 local _, charname, _, realm, _, faction = BNGetGameAccountInfo( gameid )
1709 if not charname or charname == "" then
1710 RemoveLink( gameid, true )
1711 end
1712 end
1713
1714 if m_status_broadcast_time == 0 then
1715 m_status_broadcast_fast = true
1716 if run_update then
1717 Proto.Update() -- Global call, because Update isn't defined yet.
1718 end
1719 end
1720end Proto.PurgeOfflineLinks = PurgeOfflineLinks
1721
1722-------------------------------------------------------------------------------
1723local function Update()
1724 Me.Timer_Start( "protocol_update", "push", 1.0, Update )
1725 local time = GetTime()
1726
1727 -- Turn hosting off when we enter an instance. It would probably be fine to
1728 -- leave it on, but this is just basic prudence to respect someone's time
1729 -- inside of a dungeon or raid (especially mythic), to not use any of
1730 -- their bandwidth for routing.
1731 if m_hosting then
1732 if IsInInstance() or Proto.hosting_disabled then
1733 StopHosting()
1734 end
1735 m_hosting_time = time
1736 else
1737 -- Only turn on hosting automatically if a wait period has passed since
1738 -- it was turned off. There's also a flag that can programmatically be
1739 -- set to disable hosting.
1740 if not IsInInstance() and time >= m_hosting + 300
1741 and not HasUnsuitableLag() and not Proto.hosting_disabled then
1742 StartHosting()
1743 end
1744 end
1745
1746 -- Check link health.
1747 for k, v in pairs( m_links ) do
1748 if v:RemoveExpiredNodes() then
1749 -- The NodeSet will return true if it removes the last node in the
1750 -- set, meaning that we lost a link completely. In that case we
1751 -- want to broadcast our status immediately to let people know, to
1752 -- avoid any routing problems.
1753 m_status_broadcast_time = 0
1754 m_status_broadcast_fast = true
1755 end
1756 end
1757
1758 -- Clean up any bridges that have expired. Bridges expire if they don't
1759 -- broadcast any status in so long. They will also be removed if we get a
1760 -- "player is offline" message when trying to route through them.
1761 for k, v in pairs( m_bridges ) do
1762 v:RemoveExpiredNodes()
1763 end
1764
1765 -- Clean up any offline links. Usually this will do nothing, as those will
1766 -- be caught in the BN_FRIEND_INFO_CHANGED event handler.
1767 PurgeOfflineLinks( false )
1768
1769 -- Process our senders. We don't copy this table beforehand because
1770 -- ProcessSender might add new keys. (We don't, but a sender callback
1771 -- might call Proto.Send).
1772 local senders_copy = {}
1773 for _, sender in pairs( m_senders ) do
1774 senders_copy[ #senders_copy ] = sender
1775 end
1776 for _, v in pairs( senders_copy ) do
1777 ProcessSender( v )
1778 end
1779
1780 -- Periodic status broadcasts.
1781 if m_hosting
1782 and time > m_status_broadcast_time + STATUS_BROADCAST_INTERVAL then
1783 m_status_broadcast_time = time
1784
1785 -- Both on our local channel and update all of our Bnet links.
1786 BroadcastStatus()
1787 BroadcastBnetStatus()
1788 end
1789end Proto.Update = Update
1790
1791-------------------------------------------------------------------------------
1792-- Called when we mouseover or target a unit, and we set whatever band that
1793-- unit is on to ACTIVE (record the time).
1794local function TouchUnitBand( unit )
1795 local band = GetBandFromUnit( unit )
1796 if not band then return end
1797
1798 if not IsDestLocal( band ) then
1799 band = GetLinkedBand( band )
1800 m_active_bands[band] = GetTime()
1801 end
1802end Proto.TouchUnitBand = TouchUnitBand
1803
1804
1805-------------------------------------------------------------------------------
1806-- SECURE CHANNEL
1807-------------------------------------------------------------------------------
1808-- Converts a number into base32. 6 digits.
1809local function Base32x6( number )
1810 local chars = "0123456789ABCDEFGHIJKLMONPQRSTUV"
1811 local result = ""
1812 for i = 1, 6 do
1813 local digit = 1 + (number % 32)
1814 number = bit.rshift( number, 5 )
1815 result = chars:sub( digit, digit ) .. result
1816 end
1817 return result
1818end
1819
1820-------------------------------------------------------------------------------
1821-- Generates a 10 digit channel ID from our secure code, used for an addon
1822-- prefix (and used to be for game channel names).
1823local function GetSecureChannel()
1824 local sha1, sha2 = Me.Sha256Data( "channel" .. m_secure_code )
1825 local channel = Base32x6( sha1 ) .. Base32x6( sha2 )
1826 return channel:sub( 1, 10 )
1827end Proto.GetSecureChannel = GetSecureChannel
1828
1829-------------------------------------------------------------------------------
1830-- Updates secure data linked to an id, which can be a gameid or fullname (for
1831-- link or bridge). `hash1` is the public hash. `hash2` is the personal hash.
1832local function UpdateNodeSecureData( id, hash1, hash2 )
1833 if not hash1 or hash1 == "" or hash1 == "-" then
1834 -- This person doesn't have secure keys specified.
1835 m_node_secure_data[id] = nil
1836 else
1837 local sd = m_node_secure_data[id] or {}
1838 m_node_secure_data[id] = sd
1839 -- If anything changes, then we update it.
1840 if sd.code ~= m_secure_code or sd.h1 ~= hash1 or sd.h2 ~= hash2 then
1841 sd.code = m_secure_code
1842 sd.h1 = hash1
1843 sd.h2 = hash2
1844
1845 -- `hash1` isn't really useful for anything other than
1846 -- optimization.
1847 -- Calculating the sha256 is some pretty heavy work, despite us
1848 -- having a fairly fast implementation for it, so we check if the
1849 -- `hash1` matches first before doing that.
1850 if m_secure_code and hash1 == strsub( m_secure_hash, 1, 12 ) then
1851 -- If `id` is a number, then it's a gameid, and we need a
1852 -- fullname to salt the hash.
1853 local name = type(id) == "number" and
1854 GetFullnameFromGameID( id ) or id
1855 local hash = Me.Sha256( name .. m_secure_code )
1856 sd.secure = strsub( hash, 1, 8 ) == hash2
1857 else
1858 sd.secure = false
1859 end
1860 end
1861 end
1862end Proto.UpdateNodeSecureData = UpdateNodeSecureData
1863
1864-------------------------------------------------------------------------------
1865-- This is only called during initialization of the secure state, and all
1866-- bridge and link node sets have had their "secure" tags reset. In here we
1867-- update our secure data and then properly tag any secure links or bridges.
1868local function UpdateSecureNodeSets()
1869
1870 for k, v in pairs( m_node_secure_data ) do
1871 UpdateNodeSecureData( k, v.h1, v.h2 )
1872 end
1873
1874 for band, bridge in pairs( m_bridges ) do
1875 for k, v in pairs( bridge.nodes ) do
1876 if m_node_secure_data[k] and m_node_secure_data[k].secure then
1877 bridge:ChangeNodeSubset( k, "secure" )
1878 end
1879 end
1880 end
1881
1882 for band, link in pairs( m_links ) do
1883 for k, v in pairs( link.nodes ) do
1884 if m_node_secure_data[k] and m_node_secure_data[k].secure then
1885 link:ChangeNodeSubset( k, "secure" )
1886 end
1887 end
1888 end
1889
1890 UpdateSelfBridge()
1891end Proto.UpdateSecureNodeSets = UpdateSecureNodeSets
1892
1893-------------------------------------------------------------------------------
1894-- Used to prime the secure state system before we update the settings.
1895local function ResetSecureState()
1896 -- Erase any "secure" tags for all of our links and bridges.
1897 for k, v in pairs( m_links ) do
1898 v:EraseSubset( "secure" )
1899 end
1900
1901 for k, v in pairs( m_bridges ) do
1902 v:EraseSubset( "secure" )
1903 end
1904end Proto.ResetSecureState = ResetSecureState
1905
1906-------------------------------------------------------------------------------
1907-- Enables or disables a secure state. `code` is the password used. Anyone else
1908-- who uses the same password for their secure state can communicate over
1909-- secure channels. Pass `nil` to disable the secure state.
1910--
1911local function SetSecure( code )
1912 ResetSecureState()
1913 m_secure_code = code
1914 if code then
1915 -- Enabling secure state.
1916 m_secure_channel = GetSecureChannel()
1917 m_secure_hash = Me.Sha256( m_secure_code )
1918
1919 -- The personal hash here is a way to let other people know that we
1920 -- actually have the password. This hash can't be copied because it's
1921 -- unique per player, and only people with the secure code can create
1922 -- it with their name.
1923 m_secure_myhash = Me.Sha256( Me.fullname .. m_secure_code )
1924 if not m_registered_addon_prefixes[m_secure_channel] then
1925 m_registered_addon_prefixes[m_secure_channel] = true
1926 C_ChatInfo.RegisterAddonMessagePrefix( m_secure_channel .. "+RP" )
1927 end
1928 UpdateSecureNodeSets()
1929 else
1930 -- Disabling secure state.
1931 m_secure_channel = nil
1932 m_secure_hash = nil
1933 m_secure_myhash = nil
1934 end
1935 m_status_broadcast_time = 0
1936end Proto.SetSecure = SetSecure
1937
1938
1939-------------------------------------------------------------------------------
1940-- PROTOCOL
1941-------------------------------------------------------------------------------
1942-- HI - Bnet status message.
1943-- This is a client's probe to share information about themselves, and also to
1944-- request information from us, to establish links.
1945function Proto.handlers.BNET.HI( job, sender )
1946 if not job.complete then return end
1947
1948 -- Parse message.
1949 local version, request, load, short_hash, personal_hash =
1950 strmatch( job.text, "^HI (%S+) (.) ([0-9]+) (%S+) (%S+)" )
1951 if not load then return false end
1952
1953 -- Valid loads are 0-99, with 0 meaning they aren't hosting.
1954 load = tonumber(load)
1955 if load > 99 then return false end
1956
1957 local _, charname, _, realm, _, faction = BNGetGameAccountInfo( sender )
1958 realm = gsub( realm, "[ -]", "" )
1959 if m_linked_realms[realm] and strsub( faction, 1, 1 ) == Me.faction then
1960 -- This is a local target, so this message should never be sent to us.
1961 Me.DebugLog2( "Got rogue HI message.", sender, charname )
1962 return
1963 end
1964
1965 -- Update their secure data using the hashes specified. The `short_hash`
1966 -- is just a shortcut, lets us know if they're using the same
1967 -- secure code as us. That value can just be copied though, and the
1968 -- `personal_hash` is the second step of verification, which is generated
1969 -- by hashing their fullname against the secure code.
1970 UpdateNodeSecureData( sender, short_hash, personal_hash )
1971
1972 -- AddLink is also meant for updating or refreshing links, called
1973 -- periodically for healthy links.
1974 if load > 0 then
1975 AddLink( sender, load )
1976 else
1977 RemoveLink( sender )
1978 end
1979
1980 -- If the request bit is "?", then this is sent from a user starting up,
1981 -- requesting status from everyone. We use FAST priority for this because
1982 -- they only have a few seconds before their next startup phase triggers,
1983 -- and if our message is too late, then we won't be registered as a link
1984 -- for their initial status broadcast.
1985 if request == "?" then
1986 SendHI( sender, false, nil, "FAST" )
1987 end
1988end
1989
1990-------------------------------------------------------------------------------
1991-- BYE - Service shutdown. (Bnet broadcast version.)
1992-- This is basically a warning message that the user is /reloading their UI and
1993-- are in a bad state for anything. Instantly terminate them and do not use
1994-- them for any operations until they reestablish stability.
1995function Proto.handlers.BNET.BYE( job, sender )
1996 -- `sender` is gameid.
1997 m_node_secure_data[sender] = nil
1998 Proto.RemoveLink( sender, true )
1999end
2000
2001-------------------------------------------------------------------------------
2002-- BYE - Service shutdown. (Local addon broadcast version.)
2003--
2004function Proto.handlers.BROADCAST.BYE( job, sender )
2005 -- `sender` is fullname.
2006 m_node_secure_data[sender] = nil
2007 Proto.RemoveBridge( sender )
2008end
2009
2010-------------------------------------------------------------------------------
2011-- ST - Bridge status.
2012-- Sent periodically when a player is hosting and has active links. Lets other
2013-- players know that they can route messages through them to any destinations
2014-- listed.
2015function Proto.handlers.BROADCAST.ST( job, sender )
2016 if not job.complete then return end
2017
2018 -- Ignore this from ourself.
2019 if sender == Me.fullname then return end
2020
2021 -- Parse message.
2022 local version, request, secure_hash1, secure_hash2, bands =
2023 strmatch( job.text, "^ST (%S+) (%S) (%S+) (%S+) (%S+)" )
2024 if not version then return end
2025
2026 UpdateNodeSecureData( sender, secure_hash1, secure_hash2 )
2027
2028 -- Bands is either colon separated list or "-" for "none".
2029 if bands == "-" then
2030 RemoveBridge( sender )
2031 else
2032 -- Add or update the bridge.
2033 UpdateBridge( sender, bands )
2034 end
2035
2036 if request == "?" then
2037 DebugLog2( "Status request from %s.", sender )
2038 -- "FAST" isn't /really/ necessary, but I think it adds a bit of
2039 -- urgency to players logging in, especially players who are
2040 -- relogging, to help them get into a good state ASAP so they can
2041 -- use RP chat and such.
2042 BroadcastStatus( sender, "FAST" )
2043 end
2044
2045 -- ST is also a response from a player when they can't route our data.
2046 -- That's sent in a whisper, but we check both types of messages anyway,
2047 -- since it might be useful anyway.
2048 CheckSenderRoutes( sender )
2049end
2050
2051-- Map ST to WHISPER as well.
2052Proto.handlers.WHISPER.ST = Proto.handlers.BROADCAST.ST
2053
2054-------------------------------------------------------------------------------
2055-- A1: First ACK routing checkpoint.
2056-- After sending R3 (or receiving R2 that's pointed at ourself) we send an A1
2057-- (or A2 if A1 targets ourself), which turns into A2 over Bnet, and then A3
2058-- at the final endpoint. ACK is just like a routed message but it's stripped
2059-- of any actual message or source, and just carries the UMID.
2060function Proto.handlers.WHISPER.A1( job, sender )
2061 if not IsHosting( true ) then
2062 -- Likely a logical error.
2063 DebugLog( "Ignored A1 message because we aren't hosting." )
2064 return
2065 end
2066 local umid, dest = strmatch( job.text, "^A1 (%S+) (%a*%d+[AH])" )
2067
2068 if not dest then
2069 return false
2070 end
2071
2072 local secure = job.prefix ~= ""
2073 if secure then
2074 if m_secure_channel ~= job.prefix then
2075 -- Not listening to this secure channel. Let the sender know that
2076 -- we can't handle this request by updating them with our status.
2077 DebugLog( "Couldn't send A1 message because of secure mismatch." )
2078 BroadcastStatus( sender )
2079 return false
2080 end
2081 end
2082
2083 local link = SelectLink( dest, secure )
2084 if not link then
2085 -- todo: respond to requester.
2086 -- Don't have any links to that destination, let the sender know by
2087 -- updating them with our status.
2088 BroadcastStatus( sender )
2089 return false
2090 end
2091
2092 -- We always use "FAST" priority for A* messages. If they end up getting
2093 -- stuck in network congestion, that just causes /more/ congestion because
2094 -- then the original sender will be resending their messages.
2095 SendBnetMessage( link, { "A2", umid, dest }, secure, "FAST" )
2096end
2097
2098-------------------------------------------------------------------------------
2099-- A2: Second ACK routing checkpoint.
2100-- The second step to routing, crossing the band divide over a Bnet link.
2101function Proto.handlers.BNET.A2( job, sender )
2102 if not IsHosting( true ) then
2103 -- Likely a logical error.
2104 DebugLog( "Ignored A2 message because we aren't hosting." )
2105 return
2106 end
2107 local umid, dest = job.text:match( "^A2 (%S+) (%a*%d+[AH])" )
2108 if not dest then return end
2109
2110 if dest:lower() == m_my_dest:lower() then
2111 -- This message is for us, so we can skip the A3.
2112 OnAckReceived( umid )
2113 else
2114 local send_to = DestToFullname( dest )
2115 if not send_to then return end
2116 -- Note that we can still broadcast to any secure channel, even if
2117 -- we aren't listening to it, so we copy the job prefix directly.
2118 local job = Comm.SendAddonPacket( send_to, nil, true,
2119 "A3 " .. umid, job.prefix, "FAST" )
2120 end
2121end
2122
2123---------------------------------------------------------------------------
2124-- A3: Final ACK routing checkpoint. Anything received through here is a
2125-- message for us.
2126function Proto.handlers.WHISPER.A3( job, sender )
2127 local umid = job.text:match( "^A3 (%S+)" )
2128 if not umid then return end
2129 OnAckReceived( umid )
2130end
2131
2132-------------------------------------------------------------------------------
2133-- R0 is a basic wrapper for local addon messages. Basically it's the same as
2134-- R3 except the source is always `sender`, and there isn't any other metadata
2135-- like a UMID.
2136function Proto.handlers.WHISPER.R0( job, sender )
2137 -- R0 <message>
2138 local message = job.text:sub(4)
2139 local source = DestFromFullname( sender, Me.faction )
2140 OnMessageReceived( source, nil, message, job )
2141end
2142
2143Proto.handlers.BROADCAST.R0 = Proto.handlers.WHISPER.R0
2144
2145-------------------------------------------------------------------------------
2146-- R1: First message routing checkpoint. This is when a player selects us as a
2147-- bridge and passes data to us to be routed to a foreign destination that we
2148-- have access to.
2149function Proto.handlers.WHISPER.R1( job, sender )
2150 if not IsHosting( true ) then
2151 -- Likely a logical error.
2152 DebugLog( "Ignored R1 message because we aren't hosting." )
2153 return
2154 end
2155
2156 -- One key feature we have designed in the Comm interface is the ability to
2157 -- pass data to jobs progressively, meaning that we don't need the entire
2158 -- data for a message before we can start forwarding it. That way, if we
2159 -- have a huge job of transferring 10000 bytes (someone's big ass
2160 -- profile), each checkpoint won't be waiting several seconds for the
2161 -- entire message to transfer, like a pipeline operation. In the grander
2162 -- scheme of things, bandwidth is also shared between other messages in
2163 -- progress, so one buttload of data isn't going to clog us until it's
2164 -- done.
2165 --
2166 -- Here's a picture of what it looks like. Data to be transfered is "ABCD"
2167 -- (four chunks). The left method buffers everything and gets much much
2168 -- longer if there is a lot of data to transfer, i.e. something like
2169 -- CHUNKS * 3 instead of CHUNKS + 3 time.
2170 --
2171 -- Without the pipelining. | With the pipelining.
2172 -- SOURCE----R1------R2------R3 | SOURCE----R1------R2------R3
2173 -- T BCD -> A | BCD -> A
2174 -- I CD -> AB | CD -> B -> A
2175 -- M D -> ABC | D -> C -> B -> A
2176 -- E -> BCD -> A | -> D -> C -> AB
2177 -- CD -> AB | -> D -> ABC
2178 -- D -> ABC | -> ABCD (DONE)
2179 -- -> BCD -> A
2180 -- CD -> AB
2181 -- D -> ABC
2182 -- -> ABCD (DONE)
2183 if not job.forwarder then
2184 -- This is a new job, so our task here is basically to parse the
2185 -- R1 header, create a forwarding job, and then forward this chunk.
2186 -- If it's the only chunk, then our routing for this message is
2187 -- completed right here.
2188 local umid, flags, destination, message_data =
2189 strmatch( job.text, "^R1 (%S+) (%S+) (%a*%d+[AH]) (.+)" )
2190 if not destination then
2191 DebugLog( "Bad R1 message." )
2192
2193 -- Returning `false` from a Comm job handler will prevent any
2194 -- further callbacks from being triggered for that job (basically
2195 -- pipes any further message data received into the trash).
2196 return false
2197 end
2198
2199 -- If the job is using a custom prefix, then this is a "secure"
2200 -- message.
2201 local prefix = job.prefix
2202 local secure = prefix ~= ""
2203 if secure then
2204 if m_secure_channel ~= prefix then
2205 -- Can't forward secure channels that we aren't currently on.
2206 DebugLog( "Couldn't send R1 message - secure mismatch." )
2207 BroadcastStatus( sender )
2208 return false
2209 end
2210 end
2211
2212 local link = SelectLink( destination, secure )
2213 if not link then
2214 -- We can't reach the destination requested. No link. Send the
2215 -- requester our status so they don't try this again.
2216 BroadcastStatus( sender )
2217 return false
2218 end
2219
2220 local forwarder = Comm.SendBnetPacket( link )
2221 job.forwarder = forwarder
2222 forwarder:SetPrefix( prefix ) -- Copy the prefix used.
2223
2224 -- For secure messages, we use a higher priority. Basically, we don't
2225 -- really route any sort of "extra" traffic on secure channels. All of
2226 -- the secure traffic should be directly relevant to us (our linked
2227 -- group).
2228 forwarder:SetPriority( secure and "FAST" or "LOW" )
2229
2230 local source = DestFromFullname( sender, Me.faction )
2231 forwarder:AddText( job.complete, ("R2 %s %s %s %s %s"):format(
2232 umid, flags, source, destination, message_data ))
2233
2234 -- New data received is appended, so we clear it here to make it fresh
2235 -- for the next chunk received.
2236 job.text = ""
2237 else
2238 -- For any additional chunks received, we pass them right to our
2239 -- forwarder job to the next node. Easy! `job.complete` will tie up
2240 -- the end.
2241 job.forwarder:AddText( job.complete, job.text )
2242 job.text = ""
2243 end
2244end
2245
2246-------------------------------------------------------------------------------
2247-- Send an ACK message back to the `dest` specified, carrying their `umid` back
2248-- to them, so they can confirm that their message was sent successfully.
2249-- This is called from R2, either directly inside if we are the R3 target as
2250-- well, or in a callback for after we put the R3 message out on the line.
2251-- In other words, there's a bit of importance of the order, if we send it
2252-- after we're done putting R3 out on the line, then the ACK will most
2253-- certainly fail if we fail to transmit the R3.
2254local function SendAck( dest, umid )
2255 DebugLog2( "Sending ACK", dest, umid )
2256 local sender = {
2257 ack = true;
2258 umids = { [umid] = { umid = umid, dest = dest, tries = 0} };
2259 msg = "";
2260 }
2261
2262 m_senders[sender] = sender
2263 ProcessSender( sender )
2264end Proto.SendAck = SendAck
2265
2266-------------------------------------------------------------------------------
2267-- The Comm API can call this after it successfully puts our message out on
2268-- the line (that is, after the WoW send function is called).
2269local function OnR3Sent( job )
2270 SendAck( job.ack_dest, job.umid )
2271end
2272
2273-------------------------------------------------------------------------------
2274-- R2: Second message routing checkpoint. This is after the message has crossed
2275-- a band divide, over Battle.net links.
2276function Proto.handlers.BNET.R2( job, sender )
2277 -- `skip_r3_for_self` is a flag (set inside here) that this message is
2278 -- targeting us, so we don't need to send an R3 (to ourself), and once the
2279 -- message is complete we can process it immediately.
2280 if not job.skip_r3_for_self then
2281 if not job.forwarder then
2282 local pattern = "^R2 (%S+) (%S+) (%a+%d+[AH]) (%a*)(%d+[AH]) (.+)"
2283 local umid, flags, source, dest_name, dest_band, message_data =
2284 strmatch( job.text, pattern )
2285 if not dest_name then return false end
2286
2287 local destination = dest_name .. dest_band
2288
2289 if destination:lower() == m_my_dest:lower() then
2290 -- We are the destination, so we can skip creating another
2291 -- forwarder. Once this message is complete, we proces it for
2292 -- ourself.
2293 job.skip_r3_for_self = true
2294 else
2295 -- Don't forward if we aren't hosting.
2296 if not IsHosting( true ) then
2297 -- Likely a logical error, as nobody should be picking us
2298 -- for a link if we aren't hosting.
2299 DebugLog( "Ignored R2 message because we aren't hosting." )
2300 return
2301 end
2302
2303 -- The dest can either be a full destination, or just a band
2304 -- name. In the former case, the R3 is a whispered addon
2305 -- message to that player. In the latter case, dest is ignored
2306 -- and we forward the message as an R3 to our local broadcast
2307 -- channel.
2308 local send_to
2309 if dest_name ~= "" then
2310 send_to = DestToFullname( destination )
2311 else
2312 send_to = "*"
2313 end
2314
2315 local forwarder = Comm.SendAddonPacket( send_to )
2316 job.forwarder = forwarder
2317 if flags:find("G") then
2318 -- The "G" flag dictates that this is a "guaranteed"
2319 -- message. In that case, we setup a callback to be
2320 -- triggered after the R3 message is put out on the line.
2321 -- The Comm API will call it after sending the last chunk.
2322 forwarder:SetSentCallback( OnR3Sent )
2323
2324 -- Userdata for the callback, arguments for SendAck.
2325 forwarder.umid = umid
2326 forwarder.ack_dest = source
2327 end
2328
2329 -- Forward text.
2330 forwarder:SetPrefix( job.prefix )
2331 forwarder:SetPriority( job.prefix ~= "" and "FAST" or "LOW" )
2332 forwarder:AddText( job.complete,
2333 ("R3 %s %s %s"):format( umid, source, message_data ))
2334 job.text = ""
2335 end
2336 else
2337 -- Forward text.
2338 job.forwarder:AddText( job.complete, job.text )
2339 job.text = ""
2340 end
2341 end
2342
2343 if job.skip_r3_for_self then
2344 -- This is for when we are the final endpoint already, so we skip the
2345 -- R3 message and go straight to processing.
2346 local pattern = "^R2 (%S+) (%S+) (%a+%d+[AH]) %a*%d+[AH] (.+)"
2347 local umid, flags, source, message_data = strmatch( job.text, pattern )
2348
2349 if flags:find("G") and job.complete then
2350 -- Send an ack for guaranteed messages, but only after we receive
2351 -- the entire message.
2352 SendAck( source, umid )
2353 end
2354
2355 -- Pass to message handler.
2356 OnMessageReceived( source, umid, message_data, job )
2357 end
2358end
2359
2360-------------------------------------------------------------------------------
2361-- R3: Final routing checkpoint. This is a message for us.
2362function Proto.handlers.WHISPER.R3( job, sender )
2363
2364 -- R3 contains the source of the message, the umid, and the message
2365 -- contents.
2366 local pattern = "^R3 (.+) (%a+%d+[AH]) (.+)"
2367 local umid, source, message = strmatch( job.text, pattern )
2368 if not source then return false end
2369
2370 OnMessageReceived( source, umid, message, job )
2371end
2372
2373-- Map R3 to BROADCAST as well.
2374Proto.handlers.BROADCAST.R3 = Proto.handlers.WHISPER.R3
2375
2376-------------------------------------------------------------------------------
2377-- EVENTS
2378-------------------------------------------------------------------------------
2379-- BN_FRIEND_INFO_CHANGED, triggered when Battle.net friend data (or similar)
2380-- stuff changes.
2381function Proto.OnBnFriendInfoChanged()
2382 if not Proto.startup_complete then return end
2383 -- When we see this event, a player may have gone offline, which means a
2384 -- link may have gone offline, and we want to purge them. This event is
2385 -- also spammed to death at times, so we merge duplicate events into a
2386 -- single call on the next frame (and maybe the next frame will be a safer
2387 -- spot to scan the Battle.net friends anyway to catch the offline
2388 -- status).
2389 Me.Timer_Start( "purge_offline", "ignore", 0.01, PurgeOfflineLinks, true )
2390end
2391
2392-------------------------------------------------------------------------------
2393-- UPDATE_MOUSEOVER_UNIT, triggered whenever the player mouses over a unit.
2394-- Note that this doesn't necessarily mean mousing over a player - could be
2395-- an NPC.
2396function Proto.OnMouseoverUnit()
2397 -- Basically we want to keep track of any foreign bands that we touch,
2398 -- treating them as "active" for a while, until activity with them ceases.
2399 TouchUnitBand( "mouseover" )
2400end
2401
2402-------------------------------------------------------------------------------
2403-- PLAYER_TARGET_CHANGED, triggered whenever the player changes their target.
2404function Proto.OnTargetUnit()
2405 TouchUnitBand( "target" )
2406end
2407
2408-------------------------------------------------------------------------------
2409-- Phase three of startup code, called after we have waited for status replies.
2410local function Start3()
2411 DebugLog2( "PROTO STARTUP 3" )
2412 Proto.init_state = 3
2413 Proto.startup_complete = true
2414 Me:SendMessage( "CROSSRP_PROTO_START3" )
2415
2416 -- Start our update cycle. This starts a periodic timer too.
2417 Update()
2418
2419 -- Every so often we want to clean up our UMID table. Might even be a good
2420 -- idea to leave it dirty, as it'd be minimal memory leaked.
2421 Me.Timer_Start( "proto_clean_umids", "push", 35.0, Proto.CleanSeenUMIDs )
2422
2423 Start3 = nil
2424end
2425
2426-------------------------------------------------------------------------------
2427-- Phase two of startup code, called after a wait period after sending out Bnet
2428-- probes.
2429local function Start2()
2430 DebugLog2( "PROTO STARTUP 2" )
2431 Proto.init_state = 2
2432
2433 -- Register the rest of our handlers.
2434 for dist, set in pairs( Proto.handlers ) do
2435 for command, handler in pairs( set ) do
2436 Comm.SetMessageHandler( dist, command, handler )
2437 end
2438 end
2439 Proto.handlers = nil
2440
2441 -- Don't start hosting if the player has high latency (note that this might
2442 -- be unacceptable for regions/realms that have typically high latency?).
2443 if not HasUnsuitableLag() then
2444 StartHosting()
2445 end
2446
2447 Me:SendMessage( "CROSSRP_PROTO_START2" )
2448
2449 -- This status broadcast will request the status from other players.
2450 m_status_broadcast_time = GetTime()
2451 BroadcastStatus( nil, "FAST", true )
2452 if m_hosting then
2453 -- We already sent our Bnet status, and unless we're hosting, the
2454 -- status was just to let people know that we have Cross RP and to
2455 -- get their status, not to tell them that we're hosting.
2456 -- If we are hosting, then this second Bnet status message will contain
2457 -- proper load data and let other links know that we're starting up
2458 -- our hosting.
2459 BroadcastBnetStatus( false, false )
2460 end
2461
2462 -- And we wait 3 seconds for status responses before doing the final
2463 -- initialization.
2464 Me.Timer_Start( "proto_startup3", "ignore", 3.0, Start3 )
2465
2466 Start2 = nil
2467end
2468
2469
2470-------------------------------------------------------------------------------
2471-- Phase one of startup code, called one second after Init.
2472local function Start1()
2473 DebugLog2( "PROTO STARTUP 1" )
2474 Proto.init_state = 1
2475 Proto.start_time = GetTime()
2476 -- (This used to be when we used the crossrp channel filter as a flag to
2477 -- enable RP chat display.)
2478-- if not Me.db.char.proto_crossrp_channel_added then
2479-- Me.db.char.proto_crossrp_channel_added = true
2480-- ChatFrame_AddChannel( DEFAULT_CHAT_FRAME, Me.data_channel )
2481-- end
2482
2483 -- This is the "public" prefix that we use. When in secure mode, we also
2484 -- send messages on private channels, which is
2485 -- `m_secure_channel .. "+RP"`.
2486 C_ChatInfo.RegisterAddonMessagePrefix( "+RP" )
2487
2488 -- Just registering one of our comm message handlers for now, for receiving
2489 -- replies from people we probe. We don't want to receive other message
2490 -- types right now, since we're not in a good state to yet.
2491 Comm.SetMessageHandler( "BNET", "HI", Proto.handlers.BNET.HI )
2492
2493 -- Sending this message before we do the broadcast status, as the message
2494 -- callbacks might adjust our settings, and in turn adjust what we send
2495 -- in the probes.
2496 Me:SendMessage( "CROSSRP_PROTO_START" )
2497
2498 BroadcastBnetStatus( true, true, nil, "FAST" )
2499
2500 -- Sometimes Bnet messages seem quite delayed, so using 3 seconds as a
2501 -- wait time. It's around 500ms with minimum latency for a there-and-back
2502 -- message, and that's 2500ms extra.
2503 Me.Timer_Start( "proto_startup2", "ignore", 3, Start2 )
2504 Start1 = nil
2505end
2506
2507-------------------------------------------------------------------------------
2508-- Start up the protocol service. Entry point.
2509--
2510local function Init()
2511
2512 -- Generating a UMID prefix. This is combined with an incrementing number
2513 -- to make UMIDs. They're used to filter duplicate messages when doing
2514 -- extra sends for message guarantees.
2515 local prefix_digits =
2516 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
2517 local umid_prefix = ""
2518 for i = 1, 5 do
2519 local digit = math.random( 1, #prefix_digits )
2520 umid_prefix = umid_prefix .. prefix_digits:sub( digit, digit )
2521 end
2522 m_umid_prefix = umid_prefix
2523
2524 -- Cache our destination.
2525 m_my_dest = DestFromFullname( Me.fullname, Me.faction )
2526 m_my_band = GetBandFromDest( m_my_dest )
2527
2528 -- Cache realms linked to ours. GetAutoCompleteRealms returns a table of
2529 -- realms that are connected to us.
2530 m_linked_realms[Me.realm] = true
2531 for k, v in pairs( GetAutoCompleteRealms() ) do
2532 m_linked_realms[v] = true
2533 end
2534
2535 -- We have a small delay here, as the channel system is usually fairly
2536 -- funky right whne you log in.
2537 Proto.init_state = 0
2538 Me.Timer_Start( "proto_start", "push", START_DELAY, function()
2539 JoinGameChannel( "crossrp", Start1 )
2540 end)
2541
2542 -- Clean up memory.
2543 Proto.Init = nil
2544 Init = nil
2545end
2546
2547Proto.Init = Init