Added TomTom to menu button. If both TomTom and Cartographer are available, the names...
[QuestHelper.git] / main.lua
blobfdced815a9179eaeb7972624cc8db244714a8c65
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 filter_blocked=false, -- Hides blocked objectives, such as quest turn-ins for incomplete quests
21 share = true,
22 scale = 1,
23 solo = false,
24 comm = false,
25 show_ants = true,
26 level = 2,
27 hide = false,
28 cart_wp = true,
29 tomtom_wp = true,
30 flight_time = true,
31 locale = GetLocale(), -- This variable is used for display purposes, and has nothing to do with the collected data.
32 perf_scale = 1, -- How much background processing can the current machine handle? Higher means more load, lower means better performance.
33 map_button = true
36 QuestHelper_FlightInstructors = {}
37 QuestHelper_FlightLinks = {}
38 QuestHelper_FlightRoutes = {}
39 QuestHelper_KnownFlightRoutes = {}
41 QuestHelper.tooltip = CreateFrame("GameTooltip", "QuestHelperTooltip", nil, "GameTooltipTemplate")
42 QuestHelper.objective_objects = {}
43 QuestHelper.user_objectives = {}
44 QuestHelper.quest_objects = {}
45 QuestHelper.player_level = 1
46 QuestHelper.locale = QuestHelper_Locale
48 QuestHelper.faction = (UnitFactionGroup("player") == "Alliance" and 1) or
49 (UnitFactionGroup("player") == "Horde" and 2)
51 assert(QuestHelper.faction)
53 QuestHelper.font = {serif=GameFontNormal:GetFont(), sans=ChatFontNormal:GetFont(), fancy=QuestTitleFont:GetFont()}
55 function QuestHelper:GetFontPath(list_string, font)
56 if list_string then
57 for name in string.gmatch(list_string, "[^;]+") do
58 if font:SetFont(name, 10) then
59 return name
60 elseif font:SetFont("Interface\\AddOns\\QuestHelper\\Fonts\\"..name, 10) then
61 return "Interface\\AddOns\\QuestHelper\\Fonts\\"..name
62 end
63 end
64 end
65 end
67 function QuestHelper:SetLocaleFonts()
68 self.font.sans = nil
69 self.font.serif = nil
70 self.font.fancy = nil
72 local font = self:CreateText(self)
74 if QuestHelper_Locale ~= QuestHelper_Pref.locale then
75 -- Only use alternate fonts if using a language the client wasn't intended for.
76 local replacements = QuestHelper_SubstituteFonts[QuestHelper_Pref.locale]
77 if replacements then
78 self.font.sans = self:GetFontPath(replacements.sans, font)
79 self.font.serif = self:GetFontPath(replacements.serif, font)
80 self.font.fancy = self:GetFontPath(replacements.fancy, font)
81 end
82 end
84 self.font.sans = self.font.sans or self:GetFontPath(QuestHelper_Pref.locale.."_sans.ttf", font)
85 self.font.serif = self.font.serif or self:GetFontPath(QuestHelper_Pref.locale.."_serif.ttf", font) or self.font.sans
86 self.font.fancy = self.font.fancy or self:GetFontPath(QuestHelper_Pref.locale.."_fancy.ttf", font) or self.font.serif
88 self:ReleaseText(font)
90 self.font.sans = self.font.sans or ChatFontNormal:GetFont()
91 self.font.serif = self.font.serif or GameFontNormal:GetFont()
92 self.font.fancy = self.font.fancy or QuestTitleFont:GetFont()
94 -- Need to change the font of the chat frame, for any messages that QuestHelper displays.
95 -- This should do nothing if not using an alternate font.
96 DEFAULT_CHAT_FRAME:SetFont(self.font.sans, select(2, DEFAULT_CHAT_FRAME:GetFont()))
98 if QuestHelperWorldMapButton then
99 QuestHelperWorldMapButton:SetFont(self.font.serif, select(2, QuestHelperWorldMapButton:GetFont()))
103 QuestHelper.route = {}
104 QuestHelper.to_add = {}
105 QuestHelper.to_remove = {}
106 QuestHelper.quest_log = {}
107 QuestHelper.pos = {nil, {}, 0, 0, 1, "You are here.", 0}
108 QuestHelper.sharing = false -- Will be set to true when sharing with at least one user.
110 function QuestHelper.tooltip:GetPrevLines() -- Just a helper to make life easier.
111 local last = self:NumLines()
112 local name = self:GetName()
113 return _G[name.."TextLeft"..last], _G[name.."TextRight"..last]
116 function QuestHelper:SetTargetLocation(i, x, y, toffset)
117 -- Informs QuestHelper that you're going to be at some location in toffset seconds.
118 local c, z = unpack(QuestHelper_ZoneLookup[i])
120 self.target = self:CreateTable()
121 self.target[2] = self:CreateTable()
123 self.target_time = time()+(toffset or 0)
125 x, y = self.Astrolabe:TranslateWorldMapPosition(c, z, x, y, c, 0)
126 self.target[1] = self.zone_nodes[i]
127 self.target[3] = x * self.continent_scales_x[c]
128 self.target[4] = y * self.continent_scales_y[c]
130 for i, n in ipairs(self.target[1]) do
131 local a, b = n.x-self.target[3], n.y-self.target[4]
132 self.target[2][i] = math.sqrt(a*a+b*b)
136 function QuestHelper:UnsetTargetLocation()
137 -- Unsets the target set above.
138 if self.target then
139 self:ReleaseTable(self.target[2])
140 self:ReleaseTable(self.target)
141 self.target = nil
142 self.target_time = nil
146 function QuestHelper:OnEvent(event)
147 if event == "VARIABLES_LOADED" then
148 QHFormatSetLocale(QuestHelper_Pref.locale or GetLocale())
150 if not QuestHelper_UID then
151 QuestHelper_UID = self:CreateUID()
153 QuestHelper_SaveDate = time()
155 QuestHelper_BuildZoneLookup()
157 if QuestHelper_Locale ~= GetLocale() then
158 self:TextOut(QHText("LOCALE_ERROR"))
159 return
162 self.Astrolabe = DongleStub("Astrolabe-0.4")
164 if not self:ZoneSanity() then
165 self:TextOut(QHText("ZONE_LAYOUT_ERROR"))
166 message("QuestHelper: "..QHText("ZONE_LAYOUT_ERROR"))
167 return
170 QuestHelper_UpgradeDatabase(_G)
171 QuestHelper_UpgradeComplete()
173 if QuestHelper_SaveVersion ~= 7 then
174 self:TextOut(QHText("DOWNGRADE_ERROR"))
175 return
178 self.player_level = UnitLevel("player")
180 self:ResetPathing()
182 self:UnregisterEvent("VARIABLES_LOADED")
183 self:RegisterEvent("PLAYER_TARGET_CHANGED")
184 self:RegisterEvent("LOOT_OPENED")
185 self:RegisterEvent("QUEST_COMPLETE")
186 self:RegisterEvent("QUEST_LOG_UPDATE")
187 self:RegisterEvent("QUEST_PROGRESS")
188 self:RegisterEvent("MERCHANT_SHOW")
189 self:RegisterEvent("QUEST_DETAIL")
190 self:RegisterEvent("TAXIMAP_OPENED")
191 self:RegisterEvent("PLAYER_CONTROL_GAINED")
192 self:RegisterEvent("PLAYER_CONTROL_LOST")
193 self:RegisterEvent("PLAYER_LEVEL_UP")
194 self:RegisterEvent("PARTY_MEMBERS_CHANGED")
195 self:RegisterEvent("CHAT_MSG_ADDON")
196 self:RegisterEvent("CHAT_MSG_SYSTEM")
197 self:RegisterEvent("BAG_UPDATE")
198 self:RegisterEvent("GOSSIP_SHOW")
200 self:SetScript("OnUpdate", self.OnUpdate)
202 for key, def in pairs(QuestHelper_DefaultPref) do
203 if QuestHelper_Pref[key] == nil then
204 QuestHelper_Pref[key] = def
208 self:SetLocaleFonts()
210 if QuestHelper_Pref.share and not QuestHelper_Pref.solo then
211 self:EnableSharing()
214 if QuestHelper_Pref.hide then
215 self.map_overlay:Hide()
218 self:HandlePartyChange()
219 self:Nag("all")
221 for locale in pairs(QuestHelper_StaticData) do
222 if locale ~= self.locale then
223 -- Will delete references to locales you don't use.
224 QuestHelper_StaticData[locale] = nil
228 local static = QuestHelper_StaticData[self.locale]
230 if static then
231 if static.flight_instructors then for faction in pairs(static.flight_instructors) do
232 if faction ~= self.faction then
233 -- Will delete references to flight instructors that don't belong to your faction.
234 static.flight_instructors[faction] = nil
236 end end
238 if static.quest then for faction in pairs(static.quest) do
239 if faction ~= self.faction then
240 -- Will delete references to quests that don't belong to your faction.
241 static.quest[faction] = nil
243 end end
246 -- Adding QuestHelper_CharVersion, so I know if I've already converted this characters saved data.
247 if not QuestHelper_CharVersion then
248 -- Changing per-character flight routes, now only storing the flight points they have,
249 -- will attempt to guess the routes from this.
250 local routes = {}
252 for i, l in pairs(QuestHelper_KnownFlightRoutes) do
253 for key in pairs(l) do
254 routes[key] = true
258 QuestHelper_KnownFlightRoutes = routes
260 -- Deleting the player's home again.
261 -- But using the new CharVersion variable I'm adding is cleaner that what I was doing, so I'll go with it.
262 QuestHelper_Home = nil
263 QuestHelper_CharVersion = 1
266 if not QuestHelper_Home then
267 -- Not going to bother complaining about the player's home not being set, uncomment this when the home is used in routing.
268 -- self:TextOut(QHText("HOME_NOT_KNOWN"))
271 if QuestHelper_Pref.map_button then
272 QuestHelper:InitMapButton()
275 if QuestHelper_Pref.cart_wp then
276 self:EnableCartographer()
279 if QuestHelper_Pref.tomtom_wp then
280 self:EnableTomTom()
283 collectgarbage("collect") -- Free everything we aren't using.
285 if self.debug_objectives then
286 for name, data in pairs(self.debug_objectives) do
287 self:LoadDebugObjective(name, data)
292 if event == "GOSSIP_SHOW" then
293 local name, id = UnitName("npc"), self:GetUnitID("npc")
294 if name and id then
295 self:GetObjective("monster", name).o.id = id
296 --self:TextOut("NPC: "..name.." = "..id)
300 if event == "PLAYER_TARGET_CHANGED" then
301 local name, id = UnitName("target"), self:GetUnitID("target")
302 if name and id then
303 self:GetObjective("monster", name).o.id = id
304 --self:TextOut("Target: "..name.." = "..id)
307 if UnitExists("target") and UnitIsVisible("target") and UnitCreatureType("target") ~= "Critter" and not UnitIsPlayer("target") and not UnitPlayerControlled("target") then
308 local index, x, y = self:UnitPosition("target")
310 if index then -- Might not have a position if inside an instance.
311 local w = 0.1
313 -- Modify the weight based on how far they are from us.
314 -- We don't know the exact location (using our own location), so the farther, the less sure we are that it's correct.
315 if CheckInteractDistance("target", 3) then w = 1
316 elseif CheckInteractDistance("target", 2) then w = 0.89
317 elseif CheckInteractDistance("target", 1) or CheckInteractDistance("target", 4) then w = 0.33 end
319 local monster_objective = self:GetObjective("monster", UnitName("target"))
320 self:AppendObjectivePosition(monster_objective, index, x, y, w)
321 monster_objective.o.faction = UnitFactionGroup("target")
323 local level = UnitLevel("target")
324 if level and level >= 1 then
325 local w = monster_objective.o.levelw or 0
326 monster_objective.o.level = ((monster_objective.o.level or 0)*w+level)/(w+1)
327 monster_objective.o.levelw = w+1
333 if event == "LOOT_OPENED" then
334 local target = UnitName("target")
335 if target and UnitIsDead("target") and UnitCreatureType("target") ~= "Critter" and not UnitIsPlayer("target") and not UnitPlayerControlled("target") then
336 local index, x, y = self:UnitPosition("target")
338 local monster_objective = self:GetObjective("monster", target)
339 monster_objective.o.looted = (monster_objective.o.looted or 0) + 1
341 if index then -- Might not have a position if inside an instance.
342 self:AppendObjectivePosition(monster_objective, index, x, y)
345 for i = 1, GetNumLootItems() do
346 local icon, name, number, rarity = GetLootSlotInfo(i)
347 if name then
348 if number and number >= 1 then
349 self:AppendItemObjectiveDrop(self:GetObjective("item", name), name, target, number)
350 else
351 local total = 0
352 local _, _, amount = string.find(name, "(%d+) "..COPPER)
353 if amount then total = total + amount end
354 _, _, amount = string.find(name, "(%d+) "..SILVER)
355 if amount then total = total + amount * 100 end
356 _, _, amount = string.find(name, "(%d+) "..GOLD)
357 if amount then total = total + amount * 10000 end
359 if total > 0 then
360 self:AppendObjectiveDrop(self:GetObjective("item", "money"), target, total)
365 else
366 local container = nil
368 -- Go through the players inventory and look for a locked item, we're probably looting it.
369 for bag = 0,NUM_BAG_SLOTS do
370 for slot = 1,GetContainerNumSlots(bag) do
371 local link = GetContainerItemLink(bag, slot)
372 if link and select(3, GetContainerItemInfo(bag, slot)) then
373 if container == nil then
374 -- Found a locked item and haven't previously assigned to container, assign its name, or false if we fail to parse it.
375 container = select(3, string.find(link, "|h%[(.+)%]|h|r")) or false
376 else
377 -- Already tried to assign to a container. If there are multiple locked items, we give up.
378 container = false
384 if container then
385 local container_objective = self:GetObjective("item", container)
386 container_objective.o.opened = (container_objective.o.opened or 0) + 1
388 for i = 1, GetNumLootItems() do
389 local icon, name, number, rarity = GetLootSlotInfo(i)
390 if name and number >= 1 then
391 self:AppendItemObjectiveContainer(self:GetObjective("item", name), container, number)
394 else
395 -- No idea where the items came from.
396 local index, x, y = self:PlayerPosition()
398 if index then
399 for i = 1, GetNumLootItems() do
400 local icon, name, number, rarity = GetLootSlotInfo(i)
401 if name and number >= 1 then
402 self:AppendItemObjectivePosition(self:GetObjective("item", name), name, index, x, y)
410 if event == "CHAT_MSG_SYSTEM" then
411 local home_name = self:convertPattern(ERR_DEATHBIND_SUCCESS_S)(arg1)
412 if home_name then
413 if self.i then
414 self:TextOut(QHText("HOME_CHANGED"))
415 self:TextOut(QHText("WILL_RESET_PATH"))
417 local home = QuestHelper_Home
418 if not home then
419 home = {}
420 QuestHelper_Home = home
423 home[1], home[2], home[3], home[4] = self.i, self.x, self.y, home_name
424 self.defered_graph_reset = true
429 if event == "CHAT_MSG_ADDON" then
430 if arg1 == "QHpr" and (arg3 == "PARTY" or arg3 == "WHISPER") and arg4 ~= UnitName("player") then
431 self:HandleRemoteData(arg2, arg4)
435 if event == "PARTY_MEMBERS_CHANGED" then
436 self:HandlePartyChange()
439 if event == "QUEST_LOG_UPDATE" or
440 event == "PLAYER_LEVEL_UP" or
441 event == "PARTY_MEMBERS_CHANGED" then
442 self.defered_quest_scan = true
445 if event == "QUEST_DETAIL" then
446 if not self.quest_giver then self.quest_giver = {} end
447 local npc = UnitName("npc")
448 if npc then
449 -- Some NPCs aren't actually creatures, and so their positions might not be marked by PLAYER_TARGET_CHANGED.
450 local index, x, y = self:UnitPosition("npc")
452 if index then -- Might not have a position if inside an instance.
453 local npc_objective = self:GetObjective("monster", npc)
454 self:AppendObjectivePosition(npc_objective, index, x, y)
455 self.quest_giver[GetTitleText()] = npc
460 if event == "QUEST_COMPLETE" or event == "QUEST_PROGRESS" then
461 local quest = GetTitleText()
462 if quest then
463 local level, hash = self:GetQuestLevel(quest)
464 if not level or level < 1 then
465 --self:TextOut("Don't know quest level for ".. quest.."!")
466 return
468 local q = self:GetQuest(quest, level, hash)
470 if q.need_hash then
471 q.o.hash = hash
474 local unit = UnitName("npc")
475 if unit then
476 q.o.finish = unit
477 q.o.pos = nil
479 -- Some NPCs aren't actually creatures, and so their positions might not be marked by PLAYER_TARGET_CHANGED.
480 local index, x, y = self:UnitPosition("npc")
481 if index then -- Might not have a position if inside an instance.
482 local npc_objective = self:GetObjective("monster", unit)
483 self:AppendObjectivePosition(npc_objective, index, x, y)
485 elseif not q.o.finish then
486 local index, x, y = self:PlayerPosition()
487 if index then -- Might not have a position if inside an instance.
488 self:AppendObjectivePosition(q, index, x, y)
494 if event == "MERCHANT_SHOW" then
495 local npc_name = UnitName("npc")
496 if npc_name then
497 local npc_objective = self:GetObjective("monster", npc_name)
498 local index = 1
499 while true do
500 local item_name = GetMerchantItemInfo(index)
501 if item_name then
502 index = index + 1
503 local item_objective = self:GetObjective("item", item_name)
504 if not item_objective.o.vendor then
505 item_objective.o.vendor = {npc_name}
506 else
507 local known = false
508 for i, vendor in ipairs(item_objective.o.vendor) do
509 if npc_name == vendor then
510 known = true
511 break
514 if not known then
515 table.insert(item_objective.o.vendor, npc_name)
518 else
519 break
525 if event == "TAXIMAP_OPENED" then
526 self:taxiMapOpened()
529 if event == "PLAYER_CONTROL_GAINED" then
530 self:flightEnded()
533 if event == "PLAYER_CONTROL_LOST" then
534 self:flightBegan()
537 if event == "BAG_UPDATE" then
538 for slot = 1,GetContainerNumSlots(arg1) do
539 local link = GetContainerItemLink(arg1, slot)
540 if link then
541 local id, name = select(3, string.find(link, "|Hitem:(%d+):.-|h%[(.-)%]|h"))
542 if name then
543 self:GetObjective("item", name).o.id = tonumber(id)
550 local map_shown_decay = 0
551 local delayed_action = 100
552 local update_count = 0
554 function QuestHelper:OnUpdate()
556 update_count = update_count - 1
558 if update_count <= 0 then
560 -- Reset the update count for next time around; this will make sure the body executes every time
561 -- when perf_scale >= 1, and down to 1 in 10 iterations when perf_scale < 1, or when hidden.
562 update_count = update_count + (QuestHelper_Pref.hide and 10 or 1/QuestHelper_Pref.perf_scale)
564 if update_count < 0 then
565 -- Make sure the count doesn't go perpetually negative; don't know what will happen if it underflows.
566 update_count = 0
569 if self.Astrolabe.WorldMapVisible then
570 -- We won't trust that the zone returned by Astrolabe is correct until map_shown_decay is 0.
571 map_shown_decay = 2
572 elseif map_shown_decay > 0 then
573 map_shown_decay = map_shown_decay - 1
574 else
575 SetMapToCurrentZone()
578 delayed_action = delayed_action - 1
579 if delayed_action <= 0 then
580 delayed_action = 100
581 self:HandlePartyChange()
585 local nc, nz, nx, ny = self.Astrolabe:GetCurrentPlayerPosition()
587 if nc and nc == self.c and map_shown_decay > 0 and self.z > 0 and self.z ~= nz then
588 -- There's a chance Astrolable will return the wrong zone if you're messing with the world map, if you can
589 -- be seen in that zone but aren't in it.
590 local nnx, nny = self.Astrolabe:TranslateWorldMapPosition(nc, nz, nx, ny, nc, self.z)
591 if nnx > 0 and nny > 0 and nnx < 1 and nny < 1 then
592 nz, nx, ny = self.z, nnx, nny
596 if nc and nc > 0 and nz == 0 and nc == self.c and self.z > 0 then
597 nx, ny = self.Astrolabe:TranslateWorldMapPosition(nc, nz, nx, ny, nc, self.z)
598 if nx and ny and nx > -0.1 and ny > -0.1 and nx < 1.1 and ny < 1.1 then
599 nz = self.z
600 else
601 nc, nz, nx, ny = nil, nil, nil, nil
605 if nc and nz > 0 then
606 if nc > 0 and nz > 0 then
607 self.c, self.z, self.x, self.y = nc or self.c, nz or self.z, nx or self.x, ny or self.y
608 self.i = QuestHelper_IndexLookup[self.c][self.z]
610 if not self.target then
611 self.pos[3], self.pos[4] = self.Astrolabe:TranslateWorldMapPosition(self.c, self.z, self.x, self.y, self.c, 0)
612 assert(self.pos[3])
613 assert(self.pos[4])
614 self.pos[1] = self.zone_nodes[self.i]
615 self.pos[3] = self.pos[3] * self.continent_scales_x[self.c]
616 self.pos[4] = self.pos[4] * self.continent_scales_y[self.c]
617 for i, n in ipairs(self.pos[1]) do
618 if not n.x then
619 for i, j in pairs(n) do self:TextOut("[%q]=%s %s", i, type(j), tostring(j) or "???") end
620 assert(false)
622 local a, b = n.x-self.pos[3], n.y-self.pos[4]
623 self.pos[2][i] = math.sqrt(a*a+b*b)
629 if self.target then
630 self.pos[1], self.pos[3], self.pos[4] = self.target[1], self.target[3], self.target[4]
631 local extra_time = math.max(0, self.target_time-time())
632 for i in ipairs(self.pos[1]) do
633 self.pos[2][i] = self.target[2][i]+extra_time
637 if self.pos[1] then
638 if self.defered_quest_scan then
639 self.defered_quest_scan = false
640 self:ScanQuestLog()
643 if coroutine.status(self.update_route) ~= "dead" then
644 local state, err = coroutine.resume(self.update_route, self)
645 if not state then self:TextOut("|cffff0000The routing co-routine just exploded|r: |cffffff77"..err.."|r") end
649 local level = UnitLevel("player")
650 if level >= 58 and self.player_level < 58 then
651 self.defered_graph_reset = true
653 self.player_level = level
655 self:PumpCommMessages()
659 QuestHelper:RegisterEvent("VARIABLES_LOADED")
660 QuestHelper:SetScript("OnEvent", QuestHelper.OnEvent)