Added TomTom to menu button. If both TomTom and Cartographer are available, the names...
[QuestHelper.git] / comm.lua
blob2ed44dee823769e827fbe478dc887bee4bf0d0ff
2 -- We can't send more than 256 bytes per message.
3 local comm_version = 1
4 local max_msg_size = 256-4-1-1 -- To allow room for "QHpr\t" ... "\0"
5 local max_chunk_size = max_msg_size - 2 -- To allow room for prefix of "x:"
6 local enabled_sharing = false
8 function QuestHelper:SendData(data,name)
9 if QuestHelper_Pref.comm then
10 if name then
11 self:TextOut("SENT/"..name..":|cff00ff00"..data.."|r")
12 else
13 self:TextOut("SENT/PARTY:|cff00ff00"..data.."|r")
14 end
15 end
17 if string.len(data) > max_msg_size then
18 -- Large pieces of data are broken into pieces.
19 local i = 1
20 while true do
21 local chunk = string.sub(data, i, i + max_chunk_size - 1)
22 i = i + max_chunk_size
23 if i > string.len(data) then
24 -- End chunk
25 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", "X:"..chunk, name and "WHISPER" or "PARTY", name)
26 break
27 else
28 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", "x:"..chunk, name and "WHISPER" or "PARTY", name)
29 end
30 end
31 else
32 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", data, name and "WHISPER" or "PARTY", name)
33 end
34 end
36 local escapes =
38 "\\", "\\\\",
39 "\n", "\\n",
40 ":", "\\;"
43 local function EscapeString(str)
44 for i = 1,#escapes,2 do
45 str = string.gsub(str, escapes[i], escapes[i+1])
46 end
48 return str
49 end
51 local function UnescapeString(str)
52 for i = #escapes,1,-2 do
53 str = string.gsub(str, escapes[i], escapes[i-1])
54 end
56 return str
57 end
59 local temp_table = {}
60 local function GetList(str)
61 while table.remove(temp_table) do end
62 for arg in string.gmatch(str, "([^:]*):?") do
63 table.insert(temp_table, arg)
64 end
66 -- If i remove this assert, make sure I keep the side effect.
67 assert(table.remove(temp_table) == "")
69 return temp_table
70 end
72 --[[
73 Message types:
75 QuestHelper sends its addon messages with the prefix 'QHpr'
77 syn:<VERSION>
78 Sent to new users, letting them know we know nothing about them. VERSION is the communication they are using.
79 Both clients normally send a syn: and bother respond to the other with hello:. If one client just reloaded,
80 this will just be a syn: from the reloading client and a hello: from the existing client.
82 As a special case, syn:0 indicates that the user has turned off objective sharing. Don't need to reply with
83 hello in this case. You can, but of course, they won't see or care.
85 hello:<VERSION>
86 Sent in response to syn: VERSION is the communication version they are using.
88 id:<ID>:<CATEGORY>:<WHAT>
89 Lets other users know that they are sharing an objective under ID. CATEGORY and WHAT are escaped strings,
90 to be passed to QuestHelper:GetObjective().
92 dep:<ID>:<DEP1>:<DEP2>:...
93 Lets other users know that one of their objectives depends on another. ID is the id of the objective, and
94 is followed by the IDs of the objectives it depends on.
95 For the sake of sanity, only quest objectives are allowed to have dependencies, and can't depend on
96 other quest objectives.
98 upd:<ID>:<PRI>:<HAVE>:<NEED>
99 Lets other users know that something about one of their shared objectives has changed.
101 rem:<ID>
102 Lets other users know that they have removed an objective that they were previously sharing.
104 x:<DATA>
105 User wants to send a message larger than what blizzard will allow.
106 DATA is appended to the previous data chunk.
107 Will ignore if we don't know their version yet, since they might have been in the middle
108 of sending something when we noticed it.
110 X:<DATA>
111 Same as x:, but this is the last chunk of data, and after this the combined data can be used as a message.
115 local shared_objectives = {}
116 local users = {}
117 local shared_users = 0
119 local function CreateUser(name)
120 if QuestHelper_Pref.comm then
121 QuestHelper:TextOut("Created user: "..name)
124 user = QuestHelper:CreateTable()
126 user.name=name
127 user.version=0
128 user.syn_req=false
129 user.obj=QuestHelper:CreateTable()
131 for i, obj in ipairs(shared_objectives) do -- Mark this user as knowing nothing about any of our objectives.
132 assert(obj.peer)
133 obj.peer[user] = 0
136 return user
139 local function SharedObjectiveReason(user, objective)
140 if objective.cat == "quest" then
141 return QHFormat("PEER_TURNIN", user.name, select(3, string.find(objective.obj, "^%d+/%d*/(.*)$")) or "something impossible")
142 elseif objective.cat == "loc" then
143 local i
144 local _, _, c, z, x, y = string.find(objective.obj, "^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
146 if not y then
147 _, _, i, x, y = string.find(objective.obj, "^(%d+),([%d%.]+),([%d%.]+)$")
148 else
149 i = QuestHelper_IndexLookup[c] and QuestHelper_IndexLookup[c][z]
152 return QHFormat("PEER_LOCATON", user.name, i and QuestHelper_NameLookup[i] or "the black empty void")
153 elseif objective.cat == "item" then
154 return QHFormat("PEER_ITEM", user.name, objective.obj)
155 else
156 return QHFormat("PEER_OTHER", user.name, objective.obj)
160 local function ReleaseUser(user)
161 for id, objective in pairs(user.obj) do
162 QuestHelper:SetObjectiveProgress(objective, user.name, nil, nil)
163 QuestHelper:RemoveObjectiveWatch(objective, SharedObjectiveReason(user, objective))
164 user.obj[id] = nil
167 for i, obj in ipairs(shared_objectives) do
168 assert(obj.peer)
169 obj.peer[user] = nil
172 if QuestHelper_Pref.comm then
173 QuestHelper:TextOut("Released user: "..user.name)
176 QuestHelper:ReleaseTable(user.obj)
177 QuestHelper:ReleaseTable(user)
180 function QuestHelper:DoShareObjective(objective)
181 for i = 1, #shared_objectives do assert(objective ~= shared_objectives[i]) end -- Just testing.
183 assert(objective.peer == nil)
184 objective.peer = self:CreateTable()
186 for name, user in pairs(users) do
187 -- Peers know nothing about this objective.
188 objective.peer[user] = 0
191 for o in pairs(objective.before) do
192 if o.peer then
193 for u, l in pairs(o.peer) do
194 -- Peers don't know about this dependency.
195 o.peer[u] = math.min(l, 1)
200 table.insert(shared_objectives, objective)
203 function QuestHelper:DoUnshareObjective(objective)
204 for i = 1, #shared_objectives do
205 if objective == shared_objectives[i] then
206 local need_announce = false
208 assert(objective.peer)
210 for user, level in pairs(objective.peer) do
211 if level > 0 then
212 need_announce = true
215 objective.peer[user] = nil
218 self:ReleaseTable(objective.peer)
219 objective.peer = nil
221 if need_announce then
222 self:SendData("rem:"..objective.id)
225 table.remove(shared_objectives, i)
226 return
230 assert(false) -- Should have found the objective.
233 function QuestHelper:HandleRemoteData(data, name)
234 if enabled_sharing then
235 local user = users[name]
236 if not user then
237 user = CreateUser(name)
238 users[name] = user
241 local _, _, message_type, message_data = string.find(data, "^(.-):(.*)$")
243 if message_type == "x" then
244 if user.version > 0 then
245 --self:TextOut("RECV/"..name..":<chunk>")
246 user.xmsg = (user.xmsg or "")..message_data
247 else
248 --self:TextOut("RECV/"..name..":<ignored chunk>")
250 return
251 elseif message_type == "X" then
252 if user.version > 0 then
253 --self:TextOut("RECV/"..name..":<chunk end>")
254 _, _, message_type, message_data = string.find((user.xmsg or "")..message_data, "^(.-):(.*)$")
255 user.xmsg = nil
256 else
257 --self:TextOut("RECV/"..name..":<ignored chunk end>")
258 return
262 if QuestHelper_Pref.comm then
263 self:TextOut("RECV/"..name..":|cff00ff00"..data.."|r")
266 if message_type == "syn" then
267 -- User has just noticed us. Is either new, or reloaded their UI.
269 local new_version = tonumber(message_data) or 0
271 if new_version == 0 and user.version > 0 then
272 shared_users = shared_users - 1
273 elseif new_version > 0 and user.version == 0 then
274 shared_users = shared_users + 1
277 self.sharing = shared_users > 0
278 user.version = new_version
280 for i, obj in ipairs(shared_objectives) do -- User apparently knows nothing about us.
281 assert(obj.peer)
282 obj.peer[user] = 0
285 for id, obj in pairs(user.obj) do -- And apparently all their objective ids are now null and void.
286 self:SetObjectiveProgress(obj, user.name, nil, nil)
287 self:RemoveObjectiveWatch(obj, SharedObjectiveReason(user, obj))
288 user.obj[id] = nil
291 -- Say hello to the new user.
292 if user.version > 0 then
293 self:SendData("hello:"..comm_version, name)
295 elseif message_type == "hello" then
296 local new_version = tonumber(message_data) or 0
298 if new_version == 0 and user.version > 0 then
299 shared_users = shared_users - 1
300 elseif new_version > 0 and user.version == 0 then
301 shared_users = shared_users + 1
304 self.sharing = shared_users > 0
305 user.version = new_version
307 if user.version > comm_version then
308 self:TextOut(QHFormat("PEER_NEWER", name))
309 elseif user.version < comm_version then
310 self:TextOut(QHFormat("PEER_OLDER", name))
313 elseif message_type == "id" then
314 local list = GetList(message_data)
315 local id, cat, what = tonumber(list[1]), list[2], list[3]
316 if id and cat and what and not user.obj[id] then
317 user.obj[id] = self:GetObjective(UnescapeString(cat), UnescapeString(what))
318 self:AddObjectiveWatch(user.obj[id], SharedObjectiveReason(user, user.obj[id]))
320 elseif message_type == "dep" then
321 local list = GetList(message_data)
322 local id = tonumber(list[1])
323 local obj = id and user.obj[id]
324 if obj and obj.cat == "quest" then
325 for i = 2, #list do
326 local depid = tonumber(list[i])
327 local depobj = depid and user.obj[depid]
328 if depobj and depobj.cat ~= "quest" then
329 self:ObjectiveObjectDependsOn(obj, depobj)
331 if depobj.cat == "item" then
332 if not depobj.quest then
333 depobj.quest = obj
339 elseif message_type == "upd" then
340 local _, _, id, priority, have, need = string.find(message_data, "^(%d+):(%d+):([^:]*):(.*)")
341 id, priority = tonumber(id), tonumber(priority)
343 if id and priority and have and need then
344 local obj = user.obj[id]
345 if obj then
346 have, need = UnescapeString(have), UnescapeString(need)
347 have, need = tonumber(have) or have, tonumber(need) or need
348 if have == "" or need == "" then have, need = nil, nil end
349 self:SetObjectivePriority(obj, priority)
350 self:SetObjectiveProgress(obj, user.name, have, need)
353 elseif message_type == "rem" then
354 local id = tonumber(message_data)
355 local obj = id and user.obj[id]
356 if obj then
357 self:SetObjectiveProgress(obj, name, nil, nil)
358 self:RemoveObjectiveWatch(obj, SharedObjectiveReason(user, obj))
359 user.obj[id] = nil
361 else
362 self:TextOut(QHFormat("UNKNOWN_MESSAGE", message_type, name))
367 function QuestHelper:PumpCommMessages()
368 if shared_users > 0 and enabled_sharing then
369 local best_level, best_count, best_obj = 3, 255, nil
371 for i, o in pairs(shared_objectives) do
372 local level, count = 255, 0
374 for u, l in pairs(o.peer) do
375 if u.version > 0 then
376 level = math.min(l, level)
377 count = count + 1
381 if level < best_level or (level == best_level and count > best_count) then
382 best_level, best_count, best_obj = level, count, o
386 if best_obj then
387 if best_level == 0 then
388 self:SendData("id:"..best_obj.id..":"..EscapeString(best_obj.cat)..":"..EscapeString(best_obj.obj))
389 best_level = 1
390 elseif best_level == 1 then
391 if next(best_obj.after, nil) then
392 local data, meaningful = "dep:"..best_obj.id, false
393 for o in pairs(best_obj.after) do
394 if o.peer then
395 data = data .. ":" .. o.id
396 meaningful = true
399 if meaningful then
400 self:SendData(data)
403 best_level = 2
404 elseif best_level == 2 then
405 local prog = best_obj.progress and best_obj.progress[UnitName("player")]
406 if prog then
407 self:SendData("upd:"..best_obj.id..":"..best_obj.priority..":"..EscapeString(prog[1])..":"..EscapeString(prog[2]))
408 else
409 self:SendData("upd:"..best_obj.id..":"..best_obj.priority.."::")
411 best_level = 3
414 for u in pairs(best_obj.peer) do -- All peers have just seen this.
415 if u.version > 0 then
416 best_obj.peer[u] = math.max(best_obj.peer[u], best_level)
423 function QuestHelper:HandlePartyChange()
424 if enabled_sharing then
425 for name, user in pairs(users) do
426 user.seen = false
429 for i = 1,4 do
430 local name, realm = UnitName("party"..i)
431 -- For some mysterous reason, out of range party members return an empty string as the realm name instead of nil.
432 if name and name ~= UNKNOWNOBJECT and not realm or realm == "" then
433 local user = users[name]
434 if not user then
435 user = CreateUser(name)
436 users[name] = user
439 if not user.syn_req then
440 self:SendData("syn:"..comm_version, name)
441 user.syn_req = true
444 user.seen = true
448 local count = 0
450 for name, user in pairs(users) do
451 if not user.seen then
452 ReleaseUser(user)
453 users[name] = nil
454 elseif user.version > 0 then
455 count = count + 1
459 shared_users = count
460 self.sharing = count > 0
464 function QuestHelper:EnableSharing()
465 if not enabled_sharing then
466 enabled_sharing = true
467 self:HandlePartyChange()
471 function QuestHelper:DisableSharing()
472 if enabled_sharing then
473 enabled_sharing = false
474 for name, user in pairs(users) do
475 if user.version > 0 then self:SendData("syn:0", name) end
476 ReleaseUser(user)
477 users[name] = nil
479 shared_users = 0
480 self.sharing = false