bump to 3.1
[QuestHelper.git] / comm.lua
blob49c558076863a95789bc81276c7e6a23850690d0
1 QuestHelper_File["comm.lua"] = "Development Version"
2 QuestHelper_Loadtime["comm.lua"] = GetTime()
4 -- We can't send more than 256 bytes per message.
5 local comm_version = 1
6 local max_msg_size = 256-4-1-1 -- To allow room for "QHpr\t" ... "\0"
7 local max_chunk_size = max_msg_size - 2 -- To allow room for prefix of "x:"
8 local enabled_sharing = false
10 function QuestHelper:SendData(data,name)
11 if QuestHelper_Pref.comm then
12 if name then
13 self:TextOut("SENT/"..name..":|cff00ff00"..data.."|r")
14 else
15 self:TextOut("SENT/PARTY:|cff00ff00"..data.."|r")
16 end
17 end
19 if string.len(data) > max_msg_size then
20 -- Large pieces of data are broken into pieces.
21 local i = 1
22 while true do
23 local chunk = string.sub(data, i, i + max_chunk_size - 1)
24 i = i + max_chunk_size
25 if i > string.len(data) then
26 -- End chunk
27 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", "X:"..chunk, name and "WHISPER" or "PARTY", name)
28 break
29 else
30 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", "x:"..chunk, name and "WHISPER" or "PARTY", name)
31 end
32 end
33 else
34 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", data, name and "WHISPER" or "PARTY", name)
35 end
36 end
38 local escapes =
40 "\\", "\\\\",
41 "\n", "\\n",
42 ":", "\\;"
45 local function EscapeString(str)
46 for i = 1,#escapes,2 do
47 str = string.gsub(str, escapes[i], escapes[i+1])
48 end
50 return str
51 end
53 local function UnescapeString(str)
54 for i = #escapes,1,-2 do
55 str = string.gsub(str, escapes[i], escapes[i-1])
56 end
58 return str
59 end
61 local temp_table = {}
62 local function GetList(str)
63 while table.remove(temp_table) do end
64 for arg in string.gmatch(str, "([^:]*):?") do
65 table.insert(temp_table, arg)
66 end
68 -- If i remove this assert, make sure I keep the side effect.
69 assert(table.remove(temp_table) == "")
71 return temp_table
72 end
74 --[[
75 Message types:
77 QuestHelper sends its addon messages with the prefix 'QHpr'
79 syn:<VERSION>
80 Sent to new users, letting them know we know nothing about them. VERSION is the communication they are using.
81 Both clients normally send a syn: and bother respond to the other with hello:. If one client just reloaded,
82 this will just be a syn: from the reloading client and a hello: from the existing client.
84 As a special case, syn:0 indicates that the user has turned off objective sharing. Don't need to reply with
85 hello in this case. You can, but of course, they won't see or care.
87 hello:<VERSION>
88 Sent in response to syn: VERSION is the communication version they are using.
90 id:<ID>:<CATEGORY>:<WHAT>
91 Lets other users know that they are sharing an objective under ID. CATEGORY and WHAT are escaped strings,
92 to be passed to QuestHelper:GetObjective().
94 dep:<ID>:<DEP1>:<DEP2>:...
95 Lets other users know that one of their objectives depends on another. ID is the id of the objective, and
96 is followed by the IDs of the objectives it depends on.
97 For the sake of sanity, only quest objectives are allowed to have dependencies, and can't depend on
98 other quest objectives.
100 upd:<ID>:<PRI>:<HAVE>:<NEED>
101 Lets other users know that something about one of their shared objectives has changed.
103 rem:<ID>
104 Lets other users know that they have removed an objective that they were previously sharing.
106 x:<DATA>
107 User wants to send a message larger than what blizzard will allow.
108 DATA is appended to the previous data chunk.
109 Will ignore if we don't know their version yet, since they might have been in the middle
110 of sending something when we noticed it.
112 X:<DATA>
113 Same as x:, but this is the last chunk of data, and after this the combined data can be used as a message.
117 local shared_objectives = {}
118 local users = {}
119 local shared_users = 0
121 local function CreateUser(name)
122 if QuestHelper_Pref.comm then
123 QuestHelper:TextOut("Created user: "..name)
126 user = QuestHelper:CreateTable()
128 user.name=name
129 user.version=0
130 user.syn_req=false
131 user.obj=QuestHelper:CreateTable()
133 for i, obj in ipairs(shared_objectives) do -- Mark this user as knowing nothing about any of our objectives.
134 assert(obj.peer)
135 obj.peer[user] = 0
138 return user
141 local function SharedObjectiveReason(user, objective)
142 if objective.cat == "quest" then
143 return QHFormat("PEER_TURNIN", user.name, select(3, string.find(objective.obj, "^%d+/%d*/(.*)$")) or "something impossible")
144 elseif objective.cat == "loc" then
145 local i
146 local _, _, c, z, x, y = string.find(objective.obj, "^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
148 if not y then
149 _, _, i, x, y = string.find(objective.obj, "^(%d+),([%d%.]+),([%d%.]+)$")
150 else
151 i = QuestHelper_IndexLookup[c] and QuestHelper_IndexLookup[c][z]
154 return QHFormat("PEER_LOCATON", user.name, i and QuestHelper_NameLookup[i] or "the black empty void")
155 elseif objective.cat == "item" then
156 return QHFormat("PEER_ITEM", user.name, objective.obj)
157 else
158 return QHFormat("PEER_OTHER", user.name, objective.obj)
162 local function ReleaseUser(user)
163 for id, objective in pairs(user.obj) do
164 QuestHelper:SetObjectiveProgress(objective, user.name, nil, nil)
165 QuestHelper:RemoveObjectiveWatch(objective, SharedObjectiveReason(user, objective))
166 user.obj[id] = nil
169 for i, obj in ipairs(shared_objectives) do
170 assert(obj.peer)
171 obj.peer[user] = nil
174 if QuestHelper_Pref.comm then
175 QuestHelper:TextOut("Released user: "..user.name)
178 QuestHelper:ReleaseTable(user.obj)
179 QuestHelper:ReleaseTable(user)
182 function QuestHelper:DoShareObjective(objective)
183 for i = 1, #shared_objectives do assert(objective ~= shared_objectives[i]) end -- Just testing.
185 assert(objective.peer == nil)
186 objective.peer = self:CreateTable()
188 for name, user in pairs(users) do
189 -- Peers know nothing about this objective.
190 objective.peer[user] = 0
193 for o in pairs(objective.before) do
194 if o.peer then
195 for u, l in pairs(o.peer) do
196 -- Peers don't know about this dependency.
197 o.peer[u] = math.min(l, 1)
202 table.insert(shared_objectives, objective)
205 function QuestHelper:DoUnshareObjective(objective)
206 for i = 1, #shared_objectives do
207 if objective == shared_objectives[i] then
208 local need_announce = false
210 assert(objective.peer)
212 for user, level in pairs(objective.peer) do
213 if level > 0 then
214 need_announce = true
217 objective.peer[user] = nil
220 self:ReleaseTable(objective.peer)
221 objective.peer = nil
223 if need_announce then
224 self:SendData("rem:"..objective.id)
227 table.remove(shared_objectives, i)
228 return
232 assert(false) -- Should have found the objective.
235 function QuestHelper:HandleRemoteData(data, name)
236 if enabled_sharing then
237 local user = users[name]
238 if not user then
239 user = CreateUser(name)
240 users[name] = user
243 local _, _, message_type, message_data = string.find(data, "^(.-):(.*)$")
245 if message_type == "x" then
246 if user.version > 0 then
247 --self:TextOut("RECV/"..name..":<chunk>")
248 user.xmsg = (user.xmsg or "")..message_data
249 else
250 --self:TextOut("RECV/"..name..":<ignored chunk>")
252 return
253 elseif message_type == "X" then
254 if user.version > 0 then
255 --self:TextOut("RECV/"..name..":<chunk end>")
256 _, _, message_type, message_data = string.find((user.xmsg or "")..message_data, "^(.-):(.*)$")
257 user.xmsg = nil
258 else
259 --self:TextOut("RECV/"..name..":<ignored chunk end>")
260 return
264 if QuestHelper_Pref.comm then
265 self:TextOut("RECV/"..name..":|cff00ff00"..data.."|r")
268 if message_type == "syn" then
269 -- User has just noticed us. Is either new, or reloaded their UI.
271 local new_version = tonumber(message_data) or 0
273 if new_version == 0 and user.version > 0 then
274 shared_users = shared_users - 1
275 elseif new_version > 0 and user.version == 0 then
276 shared_users = shared_users + 1
279 self.sharing = shared_users > 0
280 user.version = new_version
282 for i, obj in ipairs(shared_objectives) do -- User apparently knows nothing about us.
283 assert(obj.peer)
284 obj.peer[user] = 0
287 for id, obj in pairs(user.obj) do -- And apparently all their objective ids are now null and void.
288 self:SetObjectiveProgress(obj, user.name, nil, nil)
289 self:RemoveObjectiveWatch(obj, SharedObjectiveReason(user, obj))
290 user.obj[id] = nil
293 -- Say hello to the new user.
294 if user.version > 0 then
295 self:SendData("hello:"..comm_version, name)
297 elseif message_type == "hello" then
298 local new_version = tonumber(message_data) or 0
300 if new_version == 0 and user.version > 0 then
301 shared_users = shared_users - 1
302 elseif new_version > 0 and user.version == 0 then
303 shared_users = shared_users + 1
306 self.sharing = shared_users > 0
307 user.version = new_version
309 if user.version > comm_version then
310 self:TextOut(QHFormat("PEER_NEWER", name))
311 elseif user.version < comm_version then
312 self:TextOut(QHFormat("PEER_OLDER", name))
315 elseif message_type == "id" then
316 local list = GetList(message_data)
317 local id, cat, what = tonumber(list[1]), list[2], list[3]
318 if id and cat and what and not user.obj[id] then
319 user.obj[id] = self:GetObjective(UnescapeString(cat), UnescapeString(what))
320 self:AddObjectiveWatch(user.obj[id], SharedObjectiveReason(user, user.obj[id]))
322 elseif message_type == "dep" then
323 local list = GetList(message_data)
324 local id = tonumber(list[1])
325 local obj = id and user.obj[id]
326 if obj and obj.cat == "quest" then
327 for i = 2, #list do
328 local depid = tonumber(list[i])
329 local depobj = depid and user.obj[depid]
330 if depobj and depobj.cat ~= "quest" then
331 self:ObjectiveObjectDependsOn(obj, depobj)
333 if depobj.cat == "item" then
334 if not depobj.quest then
335 depobj.quest = obj
341 elseif message_type == "upd" then
342 local _, _, id, priority, have, need = string.find(message_data, "^(%d+):(%d+):([^:]*):(.*)")
343 id, priority = tonumber(id), tonumber(priority)
345 if id and priority and have and need then
346 local obj = user.obj[id]
347 if obj then
348 have, need = UnescapeString(have), UnescapeString(need)
349 have, need = tonumber(have) or have, tonumber(need) or need
350 if have == "" or need == "" then have, need = nil, nil end
351 self:SetObjectivePriority(obj, priority)
352 self:SetObjectiveProgress(obj, user.name, have, need)
355 elseif message_type == "rem" then
356 local id = tonumber(message_data)
357 local obj = id and user.obj[id]
358 if obj then
359 self:SetObjectiveProgress(obj, name, nil, nil)
360 self:RemoveObjectiveWatch(obj, SharedObjectiveReason(user, obj))
361 user.obj[id] = nil
363 else
364 self:TextOut(QHFormat("UNKNOWN_MESSAGE", message_type, name))
369 function QuestHelper:PumpCommMessages()
370 if shared_users > 0 and enabled_sharing then
371 local best_level, best_count, best_obj = 3, 255, nil
373 for i, o in pairs(shared_objectives) do
374 local level, count = 255, 0
376 for u, l in pairs(o.peer) do
377 if u.version > 0 then
378 level = math.min(l, level)
379 count = count + 1
383 if level < best_level or (level == best_level and count > best_count) then
384 best_level, best_count, best_obj = level, count, o
388 if best_obj then
389 if best_level == 0 then
390 self:SendData("id:"..best_obj.id..":"..EscapeString(best_obj.cat)..":"..EscapeString(best_obj.obj))
391 best_level = 1
392 elseif best_level == 1 then
393 if next(best_obj.after, nil) then
394 local data, meaningful = "dep:"..best_obj.id, false
395 for o in pairs(best_obj.after) do
396 if o.peer then
397 data = data .. ":" .. o.id
398 meaningful = true
401 if meaningful then
402 self:SendData(data)
405 best_level = 2
406 elseif best_level == 2 then
407 local prog = best_obj.progress and best_obj.progress[UnitName("player")]
408 if prog then
409 self:SendData("upd:"..best_obj.id..":"..best_obj.priority..":"..EscapeString(prog[1])..":"..EscapeString(prog[2]))
410 else
411 self:SendData("upd:"..best_obj.id..":"..best_obj.priority.."::")
413 best_level = 3
416 for u in pairs(best_obj.peer) do -- All peers have just seen this.
417 if u.version > 0 then
418 best_obj.peer[u] = math.max(best_obj.peer[u], best_level)
425 function QuestHelper:HandlePartyChange()
426 if enabled_sharing then
427 for name, user in pairs(users) do
428 user.seen = false
431 for i = 1,4 do
432 local name, realm = UnitName("party"..i)
433 -- For some mysterous reason, out of range party members return an empty string as the realm name instead of nil.
434 if name and name ~= UNKNOWNOBJECT and not realm or realm == "" then
435 local user = users[name]
436 if not user then
437 user = CreateUser(name)
438 users[name] = user
441 if not user.syn_req then
442 self:SendData("syn:"..comm_version, name)
443 user.syn_req = true
446 user.seen = true
450 local count = 0
452 for name, user in pairs(users) do
453 if not user.seen then
454 ReleaseUser(user)
455 users[name] = nil
456 elseif user.version > 0 then
457 count = count + 1
461 shared_users = count
462 self.sharing = count > 0
466 function QuestHelper:EnableSharing()
467 if not enabled_sharing then
468 enabled_sharing = true
469 self:HandlePartyChange()
473 function QuestHelper:DisableSharing()
474 if enabled_sharing then
475 enabled_sharing = false
476 for name, user in pairs(users) do
477 if user.version > 0 then self:SendData("syn:0", name) end
478 ReleaseUser(user)
479 users[name] = nil
481 shared_users = 0
482 self.sharing = false