Made purge command reset the locale of the saved data, and added files for 0.44 release.
[QuestHelper.git] / main.lua
blobe66d014191382ecddbbd8e612426222d52d500ab
1 QuestHelper = CreateFrame("Frame", "QuestHelper", nil)
3 -- Just to make sure it's always 'seen' (there's nothing that can be seen, but still...), and therefore always updating.
4 QuestHelper:SetFrameStrata("TOOLTIP")
6 QuestHelper_SaveVersion = 7
7 QuestHelper_CharVersion = 1
8 QuestHelper_Locale = GetLocale() -- This variable is used only for the collected data, and has nothing to do with displayed text.
9 QuestHelper_Quests = {}
10 QuestHelper_Objectives = {}
12 QuestHelper_Pref =
15 QuestHelper_DefaultPref =
17 filter_level=true,
18 filter_zone=false,
19 filter_done=false,
20 share = true,
21 scale = 1,
22 solo = false,
23 comm = false,
24 show_ants = true,
25 level = 2,
26 hide = false,
27 cart_wp = true,
28 flight_time = true,
29 locale = GetLocale(), -- This variable is used for display purposes, and has nothing to do with the collected data.
30 perf_scale = 1 -- How much background processing can the current machine handle? Higher means more load, lower means better performance.
33 QuestHelper_FlightInstructors = {}
34 QuestHelper_FlightLinks = {}
35 QuestHelper_FlightRoutes = {}
36 QuestHelper_KnownFlightRoutes = {}
38 QuestHelper.tooltip = CreateFrame("GameTooltip", "QuestHelperTooltip", nil, "GameTooltipTemplate")
39 QuestHelper.objective_objects = {}
40 QuestHelper.user_objectives = {}
41 QuestHelper.quest_objects = {}
42 QuestHelper.player_level = 1
43 QuestHelper.locale = QuestHelper_Locale
45 QuestHelper.faction = (UnitFactionGroup("player") == "Alliance" and 1) or
46 (UnitFactionGroup("player") == "Horde" and 2)
48 assert(QuestHelper.faction)
50 QuestHelper.font = {serif=GameFontNormal:GetFont(), sans=ChatFontNormal:GetFont(), fancy=QuestTitleFont:GetFont()}
52 QuestHelper.route = {}
53 QuestHelper.to_add = {}
54 QuestHelper.to_remove = {}
55 QuestHelper.quest_log = {}
56 QuestHelper.pos = {nil, {}, 0, 0, 1, "You are here.", 0}
57 QuestHelper.sharing = false -- Will be set to true when sharing with at least one user.
59 function QuestHelper.tooltip:GetPrevLines() -- Just a helper to make life easier.
60 local last = self:NumLines()
61 local name = self:GetName()
62 return _G[name.."TextLeft"..last], _G[name.."TextRight"..last]
63 end
65 function QuestHelper:SetTargetLocation(i, x, y, toffset)
66 -- Informs QuestHelper that you're going to be at some location in toffset seconds.
67 local c, z = unpack(QuestHelper_ZoneLookup[i])
69 self.target = self:CreateTable()
70 self.target[2] = self:CreateTable()
72 self.target_time = time()+(toffset or 0)
74 x, y = self.Astrolabe:TranslateWorldMapPosition(c, z, x, y, c, 0)
75 self.target[1] = self.zone_nodes[i]
76 self.target[3] = x * self.continent_scales_x[c]
77 self.target[4] = y * self.continent_scales_y[c]
79 for i, n in ipairs(self.target[1]) do
80 local a, b = n.x-self.target[3], n.y-self.target[4]
81 self.target[2][i] = math.sqrt(a*a+b*b)
82 end
83 end
85 function QuestHelper:UnsetTargetLocation()
86 -- Unsets the target set above.
87 if self.target then
88 self:ReleaseTable(self.target[2])
89 self:ReleaseTable(self.target)
90 self.target = nil
91 self.target_time = nil
92 end
93 end
95 function QuestHelper:OnEvent(event)
96 if event == "VARIABLES_LOADED" then
97 QHFormatSetLocale(QuestHelper_Pref.locale or GetLocale())
98 if not QuestHelper_UID then
99 QuestHelper_UID = self:CreateUID()
101 QuestHelper_SaveDate = time()
103 QuestHelper_BuildZoneLookup()
105 if QuestHelper_Locale ~= GetLocale() then
106 self:TextOut(QHText("LOCALE_ERROR"))
107 return
110 self.Astrolabe = DongleStub("Astrolabe-0.4")
112 if not self:ZoneSanity() then
113 self:TextOut(QHText("ZONE_LAYOUT_ERROR"))
114 message("QuestHelper: "..QHText("ZONE_LAYOUT_ERROR"))
115 return
118 QuestHelper_UpgradeDatabase(_G)
119 QuestHelper_UpgradeComplete()
121 if QuestHelper_SaveVersion ~= 7 then
122 self:TextOut(QHText("DOWNGRADE_ERROR"))
123 return
126 self.player_level = UnitLevel("player")
128 self:ResetPathing()
130 self:UnregisterEvent("VARIABLES_LOADED")
131 self:RegisterEvent("PLAYER_TARGET_CHANGED")
132 self:RegisterEvent("LOOT_OPENED")
133 self:RegisterEvent("QUEST_COMPLETE")
134 self:RegisterEvent("QUEST_LOG_UPDATE")
135 self:RegisterEvent("QUEST_PROGRESS")
136 self:RegisterEvent("MERCHANT_SHOW")
137 self:RegisterEvent("QUEST_DETAIL")
138 self:RegisterEvent("TAXIMAP_OPENED")
139 self:RegisterEvent("PLAYER_CONTROL_GAINED")
140 self:RegisterEvent("PLAYER_CONTROL_LOST")
141 self:RegisterEvent("PLAYER_LEVEL_UP")
142 self:RegisterEvent("PARTY_MEMBERS_CHANGED")
143 self:RegisterEvent("CHAT_MSG_ADDON")
144 self:RegisterEvent("CHAT_MSG_SYSTEM")
145 self:RegisterEvent("BAG_UPDATE")
146 self:RegisterEvent("GOSSIP_SHOW")
148 self:SetScript("OnUpdate", self.OnUpdate)
150 for key, def in pairs(QuestHelper_DefaultPref) do
151 if QuestHelper_Pref[key] == nil then
152 QuestHelper_Pref[key] = def
156 if QuestHelper_Pref.share and not QuestHelper_Pref.solo then
157 self:EnableSharing()
160 if QuestHelper_Pref.hide then
161 self.map_overlay:Hide()
164 self:HandlePartyChange()
165 self:Nag("all")
167 for locale in pairs(QuestHelper_StaticData) do
168 if locale ~= self.locale then
169 -- Will delete references to locales you don't use.
170 QuestHelper_StaticData[locale] = nil
174 local static = QuestHelper_StaticData[self.locale]
176 if static then
177 if static.flight_instructors then for faction in pairs(static.flight_instructors) do
178 if faction ~= self.faction then
179 -- Will delete references to flight instructors that don't belong to your faction.
180 static.flight_instructors[faction] = nil
182 end end
184 if static.quest then for faction in pairs(static.quest) do
185 if faction ~= self.faction then
186 -- Will delete references to quests that don't belong to your faction.
187 static.quest[faction] = nil
189 end end
192 -- Adding QuestHelper_CharVersion, so I know if I've already converted this characters saved data.
193 if not QuestHelper_CharVersion then
194 -- Changing per-character flight routes, now only storing the flight points they have,
195 -- will attempt to guess the routes from this.
196 local routes = {}
198 for i, l in pairs(QuestHelper_KnownFlightRoutes) do
199 for key in pairs(l) do
200 routes[key] = true
204 QuestHelper_KnownFlightRoutes = routes
206 -- Deleting the player's home again.
207 -- But using the new CharVersion variable I'm adding is cleaner that what I was doing, so I'll go with it.
208 QuestHelper_Home = nil
209 QuestHelper_CharVersion = 1
212 if not QuestHelper_Home then
213 -- Not going to bother complaining about the player's home not being set, uncomment this when the home is used in routing.
214 -- self:TextOut(QHText("HOME_NOT_KNOWN"))
217 QuestHelper_InitMapButton()
219 collectgarbage("collect") -- Free everything we aren't using.
221 if self.debug_objectives then
222 for name, data in pairs(self.debug_objectives) do
223 self:LoadDebugObjective(name, data)
228 if event == "GOSSIP_SHOW" then
229 local name, id = UnitName("npc"), self:GetUnitID("npc")
230 if name and id then
231 self:GetObjective("monster", name).o.id = id
232 --self:TextOut("NPC: "..name.." = "..id)
236 if event == "PLAYER_TARGET_CHANGED" then
237 local name, id = UnitName("target"), self:GetUnitID("target")
238 if name and id then
239 self:GetObjective("monster", name).o.id = id
240 --self:TextOut("Target: "..name.." = "..id)
243 if UnitExists("target") and UnitIsVisible("target") and UnitCreatureType("target") ~= "Critter" and not UnitIsPlayer("target") and not UnitPlayerControlled("target") then
244 local index, x, y = self:UnitPosition("target")
246 if index then -- Might not have a position if inside an instance.
247 local w = 0.1
249 -- Modify the weight based on how far they are from us.
250 -- We don't know the exact location (using our own location), so the farther, the less sure we are that it's correct.
251 if CheckInteractDistance("target", 3) then w = 1
252 elseif CheckInteractDistance("target", 2) then w = 0.89
253 elseif CheckInteractDistance("target", 1) or CheckInteractDistance("target", 4) then w = 0.33 end
255 local monster_objective = self:GetObjective("monster", UnitName("target"))
256 self:AppendObjectivePosition(monster_objective, index, x, y, w)
257 monster_objective.o.faction = UnitFactionGroup("target")
259 local level = UnitLevel("target")
260 if level and level >= 1 then
261 local w = monster_objective.o.levelw or 0
262 monster_objective.o.level = ((monster_objective.o.level or 0)*w+level)/(w+1)
263 monster_objective.o.levelw = w+1
269 if event == "LOOT_OPENED" then
270 local target = UnitName("target")
271 if target and UnitIsDead("target") and UnitCreatureType("target") ~= "Critter" and not UnitIsPlayer("target") and not UnitPlayerControlled("target") then
272 local index, x, y = self:UnitPosition("target")
274 local monster_objective = self:GetObjective("monster", target)
275 monster_objective.o.looted = (monster_objective.o.looted or 0) + 1
277 if index then -- Might not have a position if inside an instance.
278 self:AppendObjectivePosition(monster_objective, index, x, y)
281 for i = 1, GetNumLootItems() do
282 local icon, name, number, rarity = GetLootSlotInfo(i)
283 if name then
284 if number and number >= 1 then
285 self:AppendItemObjectiveDrop(self:GetObjective("item", name), name, target, number)
286 else
287 local total = 0
288 local _, _, amount = string.find(name, "(%d+) "..COPPER)
289 if amount then total = total + amount end
290 _, _, amount = string.find(name, "(%d+) "..SILVER)
291 if amount then total = total + amount * 100 end
292 _, _, amount = string.find(name, "(%d+) "..GOLD)
293 if amount then total = total + amount * 10000 end
295 if total > 0 then
296 self:AppendObjectiveDrop(self:GetObjective("item", "money"), target, total)
301 else
302 local container = nil
304 -- Go through the players inventory and look for a locked item, we're probably looting it.
305 for bag = 0,NUM_BAG_SLOTS do
306 for slot = 1,GetContainerNumSlots(bag) do
307 local link = GetContainerItemLink(bag, slot)
308 if link and select(3, GetContainerItemInfo(bag, slot)) then
309 if container == nil then
310 -- Found a locked item and haven't previously assigned to container, assign its name, or false if we fail to parse it.
311 container = select(3, string.find(link, "|h%[(.+)%]|h|r")) or false
312 else
313 -- Already tried to assign to a container. If there are multiple locked items, we give up.
314 container = false
320 if container then
321 local container_objective = self:GetObjective("item", container)
322 container_objective.o.opened = (container_objective.o.opened or 0) + 1
324 for i = 1, GetNumLootItems() do
325 local icon, name, number, rarity = GetLootSlotInfo(i)
326 if name and number >= 1 then
327 self:AppendItemObjectiveContainer(self:GetObjective("item", name), container, number)
330 else
331 -- No idea where the items came from.
332 local index, x, y = self:PlayerPosition()
334 if index then
335 for i = 1, GetNumLootItems() do
336 local icon, name, number, rarity = GetLootSlotInfo(i)
337 if name and number >= 1 then
338 self:AppendItemObjectivePosition(self:GetObjective("item", name), name, index, x, y)
346 if event == "CHAT_MSG_SYSTEM" then
347 local home_name = self:convertPattern(ERR_DEATHBIND_SUCCESS_S)(arg1)
348 if home_name then
349 if self.i then
350 self:TextOut(QHText("HOME_CHANGED"))
351 self:TextOut(QHText("WILL_RESET_PATH"))
353 local home = QuestHelper_Home
354 if not home then
355 home = {}
356 QuestHelper_Home = home
359 home[1], home[2], home[3], home[4] = self.i, self.x, self.y, home_name
360 self.defered_graph_reset = true
365 if event == "CHAT_MSG_ADDON" then
366 if arg1 == "QHpr" and (arg3 == "PARTY" or arg3 == "WHISPER") and arg4 ~= UnitName("player") then
367 self:HandleRemoteData(arg2, arg4)
371 if event == "PARTY_MEMBERS_CHANGED" then
372 self:HandlePartyChange()
375 if event == "QUEST_LOG_UPDATE" or
376 event == "PLAYER_LEVEL_UP" or
377 event == "PARTY_MEMBERS_CHANGED" then
378 self.defered_quest_scan = true
381 if event == "QUEST_DETAIL" then
382 if not self.quest_giver then self.quest_giver = {} end
383 local npc = UnitName("npc")
384 if npc then
385 -- Some NPCs aren't actually creatures, and so their positions might not be marked by PLAYER_TARGET_CHANGED.
386 local index, x, y = self:UnitPosition("npc")
388 if index then -- Might not have a position if inside an instance.
389 local npc_objective = self:GetObjective("monster", npc)
390 self:AppendObjectivePosition(npc_objective, index, x, y)
391 self.quest_giver[GetTitleText()] = npc
396 if event == "QUEST_COMPLETE" or event == "QUEST_PROGRESS" then
397 local quest = GetTitleText()
398 if quest then
399 local level, hash = self:GetQuestLevel(quest)
400 if not level or level < 1 then
401 --self:TextOut("Don't know quest level for ".. quest.."!")
402 return
404 local q = self:GetQuest(quest, level, hash)
406 if q.need_hash then
407 q.o.hash = hash
410 local unit = UnitName("npc")
411 if unit then
412 q.o.finish = unit
413 q.o.pos = nil
415 -- Some NPCs aren't actually creatures, and so their positions might not be marked by PLAYER_TARGET_CHANGED.
416 local index, x, y = self:UnitPosition("npc")
417 if index then -- Might not have a position if inside an instance.
418 local npc_objective = self:GetObjective("monster", unit)
419 self:AppendObjectivePosition(npc_objective, index, x, y)
421 elseif not q.o.finish then
422 local index, x, y = self:PlayerPosition()
423 if index then -- Might not have a position if inside an instance.
424 self:AppendObjectivePosition(q, index, x, y)
430 if event == "MERCHANT_SHOW" then
431 local npc_name = UnitName("npc")
432 if npc_name then
433 local npc_objective = self:GetObjective("monster", npc_name)
434 local index = 1
435 while true do
436 local item_name = GetMerchantItemInfo(index)
437 if item_name then
438 index = index + 1
439 local item_objective = self:GetObjective("item", item_name)
440 if not item_objective.o.vendor then
441 item_objective.o.vendor = {npc_name}
442 else
443 local known = false
444 for i, vendor in ipairs(item_objective.o.vendor) do
445 if npc_name == vendor then
446 known = true
447 break
450 if not known then
451 table.insert(item_objective.o.vendor, npc_name)
454 else
455 break
461 if event == "TAXIMAP_OPENED" then
462 self:taxiMapOpened()
465 if event == "PLAYER_CONTROL_GAINED" then
466 self:flightEnded()
469 if event == "PLAYER_CONTROL_LOST" then
470 self:flightBegan()
473 if event == "BAG_UPDATE" then
474 for slot = 1,GetContainerNumSlots(arg1) do
475 local link = GetContainerItemLink(arg1, slot)
476 if link then
477 local id, name = select(3, string.find(link, "|Hitem:(%d+):.-|h%[(.-)%]|h"))
478 if name then
479 self:GetObjective("item", name).o.id = tonumber(id)
486 local map_shown_decay = 0
487 local delayed_action = 100
488 local update_count = 0
490 function QuestHelper:OnUpdate()
492 update_count = update_count - 1
494 if update_count <= 0 then
496 -- Reset the update count for next time around; this will make sure the body executes every time
497 -- when perf_scale >= 1, and down to 1 in 10 iterations when perf_scale < 1, or when hidden.
498 update_count = update_count + (QuestHelper_Pref.hide and 10 or 1/QuestHelper_Pref.perf_scale)
500 if update_count < 0 then
501 -- Make sure the count doesn't go perpetually negative; don't know what will happen if it underflows.
502 update_count = 0
505 if self.Astrolabe.WorldMapVisible then
506 -- We won't trust that the zone returned by Astrolabe is correct until map_shown_decay is 0.
507 map_shown_decay = 2
508 elseif map_shown_decay > 0 then
509 map_shown_decay = map_shown_decay - 1
510 else
511 SetMapToCurrentZone()
514 delayed_action = delayed_action - 1
515 if delayed_action <= 0 then
516 delayed_action = 100
517 self:HandlePartyChange()
521 local nc, nz, nx, ny = self.Astrolabe:GetCurrentPlayerPosition()
523 if nc and nc == self.c and map_shown_decay > 0 and self.z > 0 and self.z ~= nz then
524 -- There's a chance Astrolable will return the wrong zone if you're messing with the world map, if you can
525 -- be seen in that zone but aren't in it.
526 local nnx, nny = self.Astrolabe:TranslateWorldMapPosition(nc, nz, nx, ny, nc, self.z)
527 if nnx > 0 and nny > 0 and nnx < 1 and nny < 1 then
528 nz, nx, ny = self.z, nnx, nny
532 if nc and nc > 0 and nz == 0 and nc == self.c and self.z > 0 then
533 nx, ny = self.Astrolabe:TranslateWorldMapPosition(nc, nz, nx, ny, nc, self.z)
534 if nx and ny and nx > -0.1 and ny > -0.1 and nx < 1.1 and ny < 1.1 then
535 nz = self.z
536 else
537 nc, nz, nx, ny = nil, nil, nil, nil
541 if nc and nz > 0 then
542 if nc > 0 and nz > 0 then
543 self.c, self.z, self.x, self.y = nc or self.c, nz or self.z, nx or self.x, ny or self.y
544 self.i = QuestHelper_IndexLookup[self.c][self.z]
546 if not self.target then
547 self.pos[3], self.pos[4] = self.Astrolabe:TranslateWorldMapPosition(self.c, self.z, self.x, self.y, self.c, 0)
548 assert(self.pos[3])
549 assert(self.pos[4])
550 self.pos[1] = self.zone_nodes[self.i]
551 self.pos[3] = self.pos[3] * self.continent_scales_x[self.c]
552 self.pos[4] = self.pos[4] * self.continent_scales_y[self.c]
553 for i, n in ipairs(self.pos[1]) do
554 if not n.x then
555 for i, j in pairs(n) do self:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
556 assert(false)
558 local a, b = n.x-self.pos[3], n.y-self.pos[4]
559 self.pos[2][i] = math.sqrt(a*a+b*b)
565 if self.target then
566 self.pos[1], self.pos[3], self.pos[4] = self.target[1], self.target[3], self.target[4]
567 local extra_time = math.max(0, self.target_time-time())
568 for i in ipairs(self.pos[1]) do
569 self.pos[2][i] = self.target[2][i]+extra_time
573 if self.pos[1] then
574 if self.defered_quest_scan then
575 self.defered_quest_scan = false
576 self:ScanQuestLog()
579 if coroutine.status(self.update_route) ~= "dead" then
580 local state, err = coroutine.resume(self.update_route, self)
581 if not state then self:TextOut("|cffff0000The routing co-routine just exploded|r: |cffffff77"..err.."|r") end
585 local level = UnitLevel("player")
586 if level >= 58 and self.player_level < 58 then
587 self.defered_graph_reset = true
589 self.player_level = level
591 self:PumpCommMessages()
595 QuestHelper:RegisterEvent("VARIABLES_LOADED")
596 QuestHelper:SetScript("OnEvent", QuestHelper.OnEvent)