whups
[QuestHelper.git] / comm.lua
bloba8c96b1eaa44ee4fd464b42441c28f0337dd732f
1 QuestHelper_File["comm.lua"] = "Development Version"
2 QuestHelper_Loadtime["comm.lua"] = GetTime()
4 function QuestHelper:HandleRemoteData() end
5 function QuestHelper:PumpCommMessages() end
6 function QuestHelper:HandlePartyChange() end
7 function QuestHelper:EnableSharing() end
8 function QuestHelper:DisableSharing() end
10 do return end
12 -- We can't send more than 256 bytes per message.
13 local comm_version = 1
14 local max_msg_size = 256-4-1-1 -- To allow room for "QHpr\t" ... "\0"
15 local max_chunk_size = max_msg_size - 2 -- To allow room for prefix of "x:"
16 local enabled_sharing = false
18 function QuestHelper:SendData(data,name)
19 if QuestHelper_Pref.comm then
20 if name then
21 self:TextOut("SENT/"..name..":|cff00ff00"..data.."|r")
22 else
23 self:TextOut("SENT/PARTY:|cff00ff00"..data.."|r")
24 end
25 end
27 if string.len(data) > max_msg_size then
28 -- Large pieces of data are broken into pieces.
29 local i = 1
30 while true do
31 local chunk = string.sub(data, i, i + max_chunk_size - 1)
32 i = i + max_chunk_size
33 if i > string.len(data) then
34 -- End chunk
35 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", "X:"..chunk, name and "WHISPER" or "PARTY", name)
36 break
37 else
38 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", "x:"..chunk, name and "WHISPER" or "PARTY", name)
39 end
40 end
41 else
42 ChatThrottleLib:SendAddonMessage("BULK", "QHpr", data, name and "WHISPER" or "PARTY", name)
43 end
44 end
46 local escapes =
48 "\\", "\\\\",
49 "\n", "\\n",
50 ":", "\\;"
53 local function EscapeString(str)
54 for i = 1,#escapes,2 do
55 str = string.gsub(str, escapes[i], escapes[i+1])
56 end
58 return str
59 end
61 local function UnescapeString(str)
62 for i = #escapes,1,-2 do
63 str = string.gsub(str, escapes[i], escapes[i-1])
64 end
66 return str
67 end
69 local temp_table = {}
70 local function GetList(str)
71 while table.remove(temp_table) do end
72 for arg in string.gmatch(str, "([^:]*):?") do
73 table.insert(temp_table, arg)
74 end
76 -- If i remove this assert, make sure I keep the side effect.
77 assert(table.remove(temp_table) == "")
79 return temp_table
80 end
82 --[[
83 Message types:
85 QuestHelper sends its addon messages with the prefix 'QHpr'
87 syn:<VERSION>
88 Sent to new users, letting them know we know nothing about them. VERSION is the communication they are using.
89 Both clients normally send a syn: and bother respond to the other with hello:. If one client just reloaded,
90 this will just be a syn: from the reloading client and a hello: from the existing client.
92 As a special case, syn:0 indicates that the user has turned off objective sharing. Don't need to reply with
93 hello in this case. You can, but of course, they won't see or care.
95 hello:<VERSION>
96 Sent in response to syn: VERSION is the communication version they are using.
98 id:<ID>:<CATEGORY>:<WHAT>
99 Lets other users know that they are sharing an objective under ID. CATEGORY and WHAT are escaped strings,
100 to be passed to QuestHelper:GetObjective().
102 dep:<ID>:<DEP1>:<DEP2>:...
103 Lets other users know that one of their objectives depends on another. ID is the id of the objective, and
104 is followed by the IDs of the objectives it depends on.
105 For the sake of sanity, only quest objectives are allowed to have dependencies, and can't depend on
106 other quest objectives.
108 upd:<ID>:<PRI>:<HAVE>:<NEED>
109 Lets other users know that something about one of their shared objectives has changed.
111 rem:<ID>
112 Lets other users know that they have removed an objective that they were previously sharing.
114 x:<DATA>
115 User wants to send a message larger than what blizzard will allow.
116 DATA is appended to the previous data chunk.
117 Will ignore if we don't know their version yet, since they might have been in the middle
118 of sending something when we noticed it.
120 X:<DATA>
121 Same as x:, but this is the last chunk of data, and after this the combined data can be used as a message.
125 local shared_objectives = {}
126 local users = {}
127 local shared_users = 0
129 local function CreateUser(name)
130 if QuestHelper_Pref.comm then
131 QuestHelper:TextOut("Created user: "..name)
134 user = QuestHelper:CreateTable()
136 user.name=name
137 user.version=0
138 user.syn_req=false
139 user.obj=QuestHelper:CreateTable()
141 for i, obj in ipairs(shared_objectives) do -- Mark this user as knowing nothing about any of our objectives.
142 assert(obj.peer)
143 obj.peer[user] = 0
146 return user
149 local function SharedObjectiveReason(user, objective)
150 if objective.cat == "quest" then
151 return QHFormat("PEER_TURNIN", user.name, select(3, string.find(objective.obj, "^%d+/%d*/(.*)$")) or "something impossible")
152 elseif objective.cat == "loc" then
153 local i
154 local _, _, c, z, x, y = string.find(objective.obj, "^(%d+),(%d+),([%d%.]+),([%d%.]+)$")
156 if not y then
157 _, _, i, x, y = string.find(objective.obj, "^(%d+),([%d%.]+),([%d%.]+)$")
158 else
159 i = QuestHelper_IndexLookup[c] and QuestHelper_IndexLookup[c][z]
162 return QHFormat("PEER_LOCATON", user.name, i and QuestHelper_NameLookup[i] or "the black empty void")
163 elseif objective.cat == "item" then
164 return QHFormat("PEER_ITEM", user.name, objective.obj)
165 else
166 return QHFormat("PEER_OTHER", user.name, objective.obj)
170 local function ReleaseUser(user)
171 for id, objective in pairs(user.obj) do
172 QuestHelper:SetObjectiveProgress(objective, user.name, nil, nil)
173 QuestHelper:RemoveObjectiveWatch(objective, SharedObjectiveReason(user, objective))
174 user.obj[id] = nil
177 for i, obj in ipairs(shared_objectives) do
178 assert(obj.peer)
179 obj.peer[user] = nil
182 if QuestHelper_Pref.comm then
183 QuestHelper:TextOut("Released user: "..user.name)
186 QuestHelper:ReleaseTable(user.obj)
187 QuestHelper:ReleaseTable(user)
190 function QuestHelper:DoShareObjective(objective)
191 for i = 1, #shared_objectives do assert(objective ~= shared_objectives[i]) end -- Just testing.
193 assert(objective.peer == nil)
194 objective.peer = self:CreateTable()
196 for name, user in pairs(users) do
197 -- Peers know nothing about this objective.
198 objective.peer[user] = 0
201 for o in pairs(objective.before) do
202 if o.peer then
203 for u, l in pairs(o.peer) do
204 -- Peers don't know about this dependency.
205 o.peer[u] = math.min(l, 1)
210 table.insert(shared_objectives, objective)
213 function QuestHelper:DoUnshareObjective(objective)
214 for i = 1, #shared_objectives do
215 if objective == shared_objectives[i] then
216 local need_announce = false
218 assert(objective.peer)
220 for user, level in pairs(objective.peer) do
221 if level > 0 then
222 need_announce = true
225 objective.peer[user] = nil
228 self:ReleaseTable(objective.peer)
229 objective.peer = nil
231 if need_announce then
232 self:SendData("rem:"..objective.id)
235 table.remove(shared_objectives, i)
236 return
240 assert(false) -- Should have found the objective.
243 function QuestHelper:HandleRemoteData(data, name)
244 if enabled_sharing then
245 local user = users[name]
246 if not user then
247 user = CreateUser(name)
248 users[name] = user
251 local _, _, message_type, message_data = string.find(data, "^(.-):(.*)$")
253 if message_type == "x" then
254 if user.version > 0 then
255 --self:TextOut("RECV/"..name..":<chunk>")
256 user.xmsg = (user.xmsg or "")..message_data
257 else
258 --self:TextOut("RECV/"..name..":<ignored chunk>")
260 return
261 elseif message_type == "X" then
262 if user.version > 0 then
263 --self:TextOut("RECV/"..name..":<chunk end>")
264 _, _, message_type, message_data = string.find((user.xmsg or "")..message_data, "^(.-):(.*)$")
265 user.xmsg = nil
266 else
267 --self:TextOut("RECV/"..name..":<ignored chunk end>")
268 return
272 if QuestHelper_Pref.comm then
273 self:TextOut("RECV/"..name..":|cff00ff00"..data.."|r")
276 if message_type == "syn" then
277 -- User has just noticed us. Is either new, or reloaded their UI.
279 local new_version = tonumber(message_data) or 0
281 if new_version == 0 and user.version > 0 then
282 shared_users = shared_users - 1
283 elseif new_version > 0 and user.version == 0 then
284 shared_users = shared_users + 1
287 self.sharing = shared_users > 0
288 user.version = new_version
290 for i, obj in ipairs(shared_objectives) do -- User apparently knows nothing about us.
291 assert(obj.peer)
292 obj.peer[user] = 0
295 for id, obj in pairs(user.obj) do -- And apparently all their objective ids are now null and void.
296 self:SetObjectiveProgress(obj, user.name, nil, nil)
297 self:RemoveObjectiveWatch(obj, SharedObjectiveReason(user, obj))
298 user.obj[id] = nil
301 -- Say hello to the new user.
302 if user.version > 0 then
303 self:SendData("hello:"..comm_version, name)
305 elseif message_type == "hello" then
306 local new_version = tonumber(message_data) or 0
308 if new_version == 0 and user.version > 0 then
309 shared_users = shared_users - 1
310 elseif new_version > 0 and user.version == 0 then
311 shared_users = shared_users + 1
314 self.sharing = shared_users > 0
315 user.version = new_version
317 if user.version > comm_version then
318 self:TextOut(QHFormat("PEER_NEWER", name))
319 elseif user.version < comm_version then
320 self:TextOut(QHFormat("PEER_OLDER", name))
323 elseif message_type == "id" then
324 local list = GetList(message_data)
325 local id, cat, what = tonumber(list[1]), list[2], list[3]
326 if id and cat and what and not user.obj[id] then
327 user.obj[id] = self:GetObjective(UnescapeString(cat), UnescapeString(what))
328 self:AddObjectiveWatch(user.obj[id], SharedObjectiveReason(user, user.obj[id]))
330 elseif message_type == "dep" then
331 local list = GetList(message_data)
332 local id = tonumber(list[1])
333 local obj = id and user.obj[id]
334 if obj and obj.cat == "quest" then
335 for i = 2, #list do
336 local depid = tonumber(list[i])
337 local depobj = depid and user.obj[depid]
338 if depobj and depobj.cat ~= "quest" then
339 self:ObjectiveObjectDependsOn(obj, depobj)
341 if depobj.cat == "item" then
342 if not depobj.quest then
343 depobj.quest = obj
349 elseif message_type == "upd" then
350 local _, _, id, priority, have, need = string.find(message_data, "^(%d+):(%d+):([^:]*):(.*)")
351 id, priority = tonumber(id), tonumber(priority)
353 if id and priority and have and need then
354 local obj = user.obj[id]
355 if obj then
356 have, need = UnescapeString(have), UnescapeString(need)
357 have, need = tonumber(have) or have, tonumber(need) or need
358 if have == "" or need == "" then have, need = nil, nil end
359 self:SetObjectivePriority(obj, priority)
360 self:SetObjectiveProgress(obj, user.name, have, need)
363 elseif message_type == "rem" then
364 local id = tonumber(message_data)
365 local obj = id and user.obj[id]
366 if obj then
367 self:SetObjectiveProgress(obj, name, nil, nil)
368 self:RemoveObjectiveWatch(obj, SharedObjectiveReason(user, obj))
369 user.obj[id] = nil
371 else
372 self:TextOut(QHFormat("UNKNOWN_MESSAGE", message_type, name))
377 function QuestHelper:PumpCommMessages()
378 if shared_users > 0 and enabled_sharing then
379 local best_level, best_count, best_obj = 3, 255, nil
381 for i, o in pairs(shared_objectives) do
382 local level, count = 255, 0
384 for u, l in pairs(o.peer) do
385 if u.version > 0 then
386 level = math.min(l, level)
387 count = count + 1
391 if level < best_level or (level == best_level and count > best_count) then
392 best_level, best_count, best_obj = level, count, o
396 if best_obj then
397 if best_level == 0 then
398 self:SendData("id:"..best_obj.id..":"..EscapeString(best_obj.cat)..":"..EscapeString(best_obj.obj))
399 best_level = 1
400 elseif best_level == 1 then
401 if next(best_obj.after, nil) then
402 local data, meaningful = "dep:"..best_obj.id, false
403 for o in pairs(best_obj.after) do
404 if o.peer then
405 data = data .. ":" .. o.id
406 meaningful = true
409 if meaningful then
410 self:SendData(data)
413 best_level = 2
414 elseif best_level == 2 then
415 local prog = best_obj.progress and best_obj.progress[UnitName("player")]
416 if prog then
417 self:SendData("upd:"..best_obj.id..":"..best_obj.priority..":"..EscapeString(prog[1])..":"..EscapeString(prog[2]))
418 else
419 self:SendData("upd:"..best_obj.id..":"..best_obj.priority.."::")
421 best_level = 3
424 for u in pairs(best_obj.peer) do -- All peers have just seen this.
425 if u.version > 0 then
426 best_obj.peer[u] = math.max(best_obj.peer[u], best_level)
433 function QuestHelper:HandlePartyChange()
434 if enabled_sharing then
435 for name, user in pairs(users) do
436 user.seen = false
439 for i = 1,4 do
440 local name, realm = UnitName("party"..i)
441 -- For some mysterous reason, out of range party members return an empty string as the realm name instead of nil.
442 if name and name ~= UNKNOWNOBJECT and not realm or realm == "" then
443 local user = users[name]
444 if not user then
445 user = CreateUser(name)
446 users[name] = user
449 if not user.syn_req then
450 self:SendData("syn:"..comm_version, name)
451 user.syn_req = true
454 user.seen = true
458 local count = 0
460 for name, user in pairs(users) do
461 if not user.seen then
462 ReleaseUser(user)
463 users[name] = nil
464 elseif user.version > 0 then
465 count = count + 1
469 shared_users = count
470 self.sharing = count > 0
474 function QuestHelper:EnableSharing()
475 if not enabled_sharing then
476 enabled_sharing = true
477 self:HandlePartyChange()
481 function QuestHelper:DisableSharing()
482 if enabled_sharing then
483 enabled_sharing = false
484 for name, user in pairs(users) do
485 if user.version > 0 then self:SendData("syn:0", name) end
486 ReleaseUser(user)
487 users[name] = nil
489 shared_users = 0
490 self.sharing = false