Added hooks for AddObjectiveWatch and RemoveObjectiveWatch, so we can update the...
[QuestHelper.git] / comm.lua
blob18dab78df2387e986d6862482eb69ea6feea2696
1 QuestHelper_File["comm.lua"] = "Development Version"
3 -- We can't send more than 256 bytes per message.
4 local comm_version = 1
5 local max_msg_size = 256-4-1-1 -- To allow room for "QHpr\t" ... "\0"
6 local max_chunk_size = max_msg_size - 2 -- To allow room for prefix of "x:"
7 local enabled_sharing = false
9 function QuestHelper:SendData(data,name)
10 if QuestHelper_Pref.comm then
11 if name then
12 self:TextOut("SENT/"..name..":|cff00ff00"..data.."|r")
13 else
14 self:TextOut("SENT/PARTY:|cff00ff00"..data.."|r")
15 end
16 end
18 if string.len(data) > max_msg_size then
19 -- Large pieces of data are broken into pieces.
20 local i = 1
21 while true do
22 local chunk = string.sub(data, i, i + max_chunk_size - 1)
23 i = i + max_chunk_size
24 if i > string.len(data) then
25 -- End chunk
26 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", "X:"..chunk, name and "WHISPER" or "PARTY", name)
27 break
28 else
29 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", "x:"..chunk, name and "WHISPER" or "PARTY", name)
30 end
31 end
32 else
33 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", data, name and "WHISPER" or "PARTY", name)
34 end
35 end
37 local escapes =
39 "\\", "\\\\",
40 "\n", "\\n",
41 ":", "\\;"
44 local function EscapeString(str)
45 for i = 1,#escapes,2 do
46 str = string.gsub(str, escapes[i], escapes[i+1])
47 end
49 return str
50 end
52 local function UnescapeString(str)
53 for i = #escapes,1,-2 do
54 str = string.gsub(str, escapes[i], escapes[i-1])
55 end
57 return str
58 end
60 local temp_table = {}
61 local function GetList(str)
62 while table.remove(temp_table) do end
63 for arg in string.gmatch(str, "([^:]*):?") do
64 table.insert(temp_table, arg)
65 end
67 -- If i remove this assert, make sure I keep the side effect.
68 assert(table.remove(temp_table) == "")
70 return temp_table
71 end
73 --[[
74 Message types:
76 QuestHelper sends its addon messages with the prefix 'QHpr'
78 syn:<VERSION>
79 Sent to new users, letting them know we know nothing about them. VERSION is the communication they are using.
80 Both clients normally send a syn: and bother respond to the other with hello:. If one client just reloaded,
81 this will just be a syn: from the reloading client and a hello: from the existing client.
83 As a special case, syn:0 indicates that the user has turned off objective sharing. Don't need to reply with
84 hello in this case. You can, but of course, they won't see or care.
86 hello:<VERSION>
87 Sent in response to syn: VERSION is the communication version they are using.
89 id:<ID>:<CATEGORY>:<WHAT>
90 Lets other users know that they are sharing an objective under ID. CATEGORY and WHAT are escaped strings,
91 to be passed to QuestHelper:GetObjective().
93 dep:<ID>:<DEP1>:<DEP2>:...
94 Lets other users know that one of their objectives depends on another. ID is the id of the objective, and
95 is followed by the IDs of the objectives it depends on.
96 For the sake of sanity, only quest objectives are allowed to have dependencies, and can't depend on
97 other quest objectives.
99 upd:<ID>:<PRI>:<HAVE>:<NEED>
100 Lets other users know that something about one of their shared objectives has changed.
102 rem:<ID>
103 Lets other users know that they have removed an objective that they were previously sharing.
105 x:<DATA>
106 User wants to send a message larger than what blizzard will allow.
107 DATA is appended to the previous data chunk.
108 Will ignore if we don't know their version yet, since they might have been in the middle
109 of sending something when we noticed it.
111 X:<DATA>
112 Same as x:, but this is the last chunk of data, and after this the combined data can be used as a message.
116 local shared_objectives = {}
117 local users = {}
118 local shared_users = 0
120 local function CreateUser(name)
121 if QuestHelper_Pref.comm then
122 QuestHelper:TextOut("Created user: "..name)
125 user = QuestHelper:CreateTable()
127 user.name=name
128 user.version=0
129 user.syn_req=false
130 user.obj=QuestHelper:CreateTable()
132 for i, obj in ipairs(shared_objectives) do -- Mark this user as knowing nothing about any of our objectives.
133 assert(obj.peer)
134 obj.peer[user] = 0
137 return user
140 local function SharedObjectiveReason(user, objective)
141 if objective.cat == "quest" then
142 return QHFormat("PEER_TURNIN", user.name, select(3, string.find(objective.obj, "^%d+/%d*/(.*)$")) or "something impossible")
143 elseif objective.cat == "loc" then
144 local i
145 local _, _, c, z, x, y = string.find(objective.obj, "^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
147 if not y then
148 _, _, i, x, y = string.find(objective.obj, "^(%d+),([%d%.]+),([%d%.]+)$")
149 else
150 i = QuestHelper_IndexLookup[c] and QuestHelper_IndexLookup[c][z]
153 return QHFormat("PEER_LOCATON", user.name, i and QuestHelper_NameLookup[i] or "the black empty void")
154 elseif objective.cat == "item" then
155 return QHFormat("PEER_ITEM", user.name, objective.obj)
156 else
157 return QHFormat("PEER_OTHER", user.name, objective.obj)
161 local function ReleaseUser(user)
162 for id, objective in pairs(user.obj) do
163 QuestHelper:SetObjectiveProgress(objective, user.name, nil, nil)
164 QuestHelper:RemoveObjectiveWatch(objective, SharedObjectiveReason(user, objective))
165 user.obj[id] = nil
168 for i, obj in ipairs(shared_objectives) do
169 assert(obj.peer)
170 obj.peer[user] = nil
173 if QuestHelper_Pref.comm then
174 QuestHelper:TextOut("Released user: "..user.name)
177 QuestHelper:ReleaseTable(user.obj)
178 QuestHelper:ReleaseTable(user)
181 function QuestHelper:DoShareObjective(objective)
182 for i = 1, #shared_objectives do assert(objective ~= shared_objectives[i]) end -- Just testing.
184 assert(objective.peer == nil)
185 objective.peer = self:CreateTable()
187 for name, user in pairs(users) do
188 -- Peers know nothing about this objective.
189 objective.peer[user] = 0
192 for o in pairs(objective.before) do
193 if o.peer then
194 for u, l in pairs(o.peer) do
195 -- Peers don't know about this dependency.
196 o.peer[u] = math.min(l, 1)
201 table.insert(shared_objectives, objective)
204 function QuestHelper:DoUnshareObjective(objective)
205 for i = 1, #shared_objectives do
206 if objective == shared_objectives[i] then
207 local need_announce = false
209 assert(objective.peer)
211 for user, level in pairs(objective.peer) do
212 if level > 0 then
213 need_announce = true
216 objective.peer[user] = nil
219 self:ReleaseTable(objective.peer)
220 objective.peer = nil
222 if need_announce then
223 self:SendData("rem:"..objective.id)
226 table.remove(shared_objectives, i)
227 return
231 assert(false) -- Should have found the objective.
234 function QuestHelper:HandleRemoteData(data, name)
235 if enabled_sharing then
236 local user = users[name]
237 if not user then
238 user = CreateUser(name)
239 users[name] = user
242 local _, _, message_type, message_data = string.find(data, "^(.-):(.*)$")
244 if message_type == "x" then
245 if user.version > 0 then
246 --self:TextOut("RECV/"..name..":<chunk>")
247 user.xmsg = (user.xmsg or "")..message_data
248 else
249 --self:TextOut("RECV/"..name..":<ignored chunk>")
251 return
252 elseif message_type == "X" then
253 if user.version > 0 then
254 --self:TextOut("RECV/"..name..":<chunk end>")
255 _, _, message_type, message_data = string.find((user.xmsg or "")..message_data, "^(.-):(.*)$")
256 user.xmsg = nil
257 else
258 --self:TextOut("RECV/"..name..":<ignored chunk end>")
259 return
263 if QuestHelper_Pref.comm then
264 self:TextOut("RECV/"..name..":|cff00ff00"..data.."|r")
267 if message_type == "syn" then
268 -- User has just noticed us. Is either new, or reloaded their UI.
270 local new_version = tonumber(message_data) or 0
272 if new_version == 0 and user.version > 0 then
273 shared_users = shared_users - 1
274 elseif new_version > 0 and user.version == 0 then
275 shared_users = shared_users + 1
278 self.sharing = shared_users > 0
279 user.version = new_version
281 for i, obj in ipairs(shared_objectives) do -- User apparently knows nothing about us.
282 assert(obj.peer)
283 obj.peer[user] = 0
286 for id, obj in pairs(user.obj) do -- And apparently all their objective ids are now null and void.
287 self:SetObjectiveProgress(obj, user.name, nil, nil)
288 self:RemoveObjectiveWatch(obj, SharedObjectiveReason(user, obj))
289 user.obj[id] = nil
292 -- Say hello to the new user.
293 if user.version > 0 then
294 self:SendData("hello:"..comm_version, name)
296 elseif message_type == "hello" then
297 local new_version = tonumber(message_data) or 0
299 if new_version == 0 and user.version > 0 then
300 shared_users = shared_users - 1
301 elseif new_version > 0 and user.version == 0 then
302 shared_users = shared_users + 1
305 self.sharing = shared_users > 0
306 user.version = new_version
308 if user.version > comm_version then
309 self:TextOut(QHFormat("PEER_NEWER", name))
310 elseif user.version < comm_version then
311 self:TextOut(QHFormat("PEER_OLDER", name))
314 elseif message_type == "id" then
315 local list = GetList(message_data)
316 local id, cat, what = tonumber(list[1]), list[2], list[3]
317 if id and cat and what and not user.obj[id] then
318 user.obj[id] = self:GetObjective(UnescapeString(cat), UnescapeString(what))
319 self:AddObjectiveWatch(user.obj[id], SharedObjectiveReason(user, user.obj[id]))
321 elseif message_type == "dep" then
322 local list = GetList(message_data)
323 local id = tonumber(list[1])
324 local obj = id and user.obj[id]
325 if obj and obj.cat == "quest" then
326 for i = 2, #list do
327 local depid = tonumber(list[i])
328 local depobj = depid and user.obj[depid]
329 if depobj and depobj.cat ~= "quest" then
330 self:ObjectiveObjectDependsOn(obj, depobj)
332 if depobj.cat == "item" then
333 if not depobj.quest then
334 depobj.quest = obj
340 elseif message_type == "upd" then
341 local _, _, id, priority, have, need = string.find(message_data, "^(%d+):(%d+):([^:]*):(.*)")
342 id, priority = tonumber(id), tonumber(priority)
344 if id and priority and have and need then
345 local obj = user.obj[id]
346 if obj then
347 have, need = UnescapeString(have), UnescapeString(need)
348 have, need = tonumber(have) or have, tonumber(need) or need
349 if have == "" or need == "" then have, need = nil, nil end
350 self:SetObjectivePriority(obj, priority)
351 self:SetObjectiveProgress(obj, user.name, have, need)
354 elseif message_type == "rem" then
355 local id = tonumber(message_data)
356 local obj = id and user.obj[id]
357 if obj then
358 self:SetObjectiveProgress(obj, name, nil, nil)
359 self:RemoveObjectiveWatch(obj, SharedObjectiveReason(user, obj))
360 user.obj[id] = nil
362 else
363 self:TextOut(QHFormat("UNKNOWN_MESSAGE", message_type, name))
368 function QuestHelper:PumpCommMessages()
369 if shared_users > 0 and enabled_sharing then
370 local best_level, best_count, best_obj = 3, 255, nil
372 for i, o in pairs(shared_objectives) do
373 local level, count = 255, 0
375 for u, l in pairs(o.peer) do
376 if u.version > 0 then
377 level = math.min(l, level)
378 count = count + 1
382 if level < best_level or (level == best_level and count > best_count) then
383 best_level, best_count, best_obj = level, count, o
387 if best_obj then
388 if best_level == 0 then
389 self:SendData("id:"..best_obj.id..":"..EscapeString(best_obj.cat)..":"..EscapeString(best_obj.obj))
390 best_level = 1
391 elseif best_level == 1 then
392 if next(best_obj.after, nil) then
393 local data, meaningful = "dep:"..best_obj.id, false
394 for o in pairs(best_obj.after) do
395 if o.peer then
396 data = data .. ":" .. o.id
397 meaningful = true
400 if meaningful then
401 self:SendData(data)
404 best_level = 2
405 elseif best_level == 2 then
406 local prog = best_obj.progress and best_obj.progress[UnitName("player")]
407 if prog then
408 self:SendData("upd:"..best_obj.id..":"..best_obj.priority..":"..EscapeString(prog[1])..":"..EscapeString(prog[2]))
409 else
410 self:SendData("upd:"..best_obj.id..":"..best_obj.priority.."::")
412 best_level = 3
415 for u in pairs(best_obj.peer) do -- All peers have just seen this.
416 if u.version > 0 then
417 best_obj.peer[u] = math.max(best_obj.peer[u], best_level)
424 function QuestHelper:HandlePartyChange()
425 if enabled_sharing then
426 for name, user in pairs(users) do
427 user.seen = false
430 for i = 1,4 do
431 local name, realm = UnitName("party"..i)
432 -- For some mysterous reason, out of range party members return an empty string as the realm name instead of nil.
433 if name and name ~= UNKNOWNOBJECT and not realm or realm == "" then
434 local user = users[name]
435 if not user then
436 user = CreateUser(name)
437 users[name] = user
440 if not user.syn_req then
441 self:SendData("syn:"..comm_version, name)
442 user.syn_req = true
445 user.seen = true
449 local count = 0
451 for name, user in pairs(users) do
452 if not user.seen then
453 ReleaseUser(user)
454 users[name] = nil
455 elseif user.version > 0 then
456 count = count + 1
460 shared_users = count
461 self.sharing = count > 0
465 function QuestHelper:EnableSharing()
466 if not enabled_sharing then
467 enabled_sharing = true
468 self:HandlePartyChange()
472 function QuestHelper:DisableSharing()
473 if enabled_sharing then
474 enabled_sharing = false
475 for name, user in pairs(users) do
476 if user.version > 0 then self:SendData("syn:0", name) end
477 ReleaseUser(user)
478 users[name] = nil
480 shared_users = 0
481 self.sharing = false