mirror of
https://github.com/rivi-s/pfQuest-turtle.git
synced 2026-11-10 20:17:35 +00:00
483 lines
17 KiB
Lua
483 lines
17 KiB
Lua
local function ExtendPfQuestConfig()
|
|
-- Check if already added (prevents duplicates)
|
|
for _, entry in pairs(pfQuest_defconfig) do
|
|
if entry.config == "autoQuests" then
|
|
return true
|
|
end
|
|
end
|
|
|
|
table.insert(
|
|
pfQuest_defconfig,
|
|
{
|
|
text = "Quest Log Automation",
|
|
type = "header",
|
|
page = "questing"
|
|
}
|
|
)
|
|
|
|
table.insert(pfQuest_defconfig,
|
|
{
|
|
text = "Automatically accept and complete quests",
|
|
default = "0",
|
|
type = "checkbox",
|
|
config = "autoQuests"
|
|
})
|
|
|
|
table.insert(pfQuest_defconfig,
|
|
{
|
|
text = "Skip accepting low level quests",
|
|
default = "0",
|
|
type = "checkbox",
|
|
config = "autoQuestsSkipLowLevel"
|
|
})
|
|
|
|
table.insert(pfQuest_defconfig,
|
|
{
|
|
text = "Automate runecloth donations",
|
|
default = "0",
|
|
type = "checkbox",
|
|
config = "automateRuneclothDonations"
|
|
})
|
|
|
|
if not pfQuest_config["autoQuests"] then
|
|
pfQuest_config["autoQuests"] = "0"
|
|
end
|
|
|
|
if not pfQuest_config["autoQuestsSkipLowLevel"] then
|
|
pfQuest_config["autoQuestsSkipLowLevel"] = "1"
|
|
end
|
|
|
|
if not pfQuest_config["automateRuneclothDonations"] then
|
|
pfQuest_config["automateRuneclothDonations"] = "0"
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local configExtenderFrame = CreateFrame("Frame")
|
|
configExtenderFrame:RegisterEvent("VARIABLES_LOADED")
|
|
configExtenderFrame:SetScript("OnEvent", function()
|
|
ExtendPfQuestConfig()
|
|
end)
|
|
if pfQuest_defconfig and pfQuest_config then ExtendPfQuestConfig() end
|
|
|
|
local questLogFrame = CreateFrame("Frame")
|
|
questLogFrame:RegisterEvent("QUEST_DETAIL")
|
|
questLogFrame:RegisterEvent('GOSSIP_SHOW')
|
|
questLogFrame:RegisterEvent('QUEST_COMPLETE')
|
|
questLogFrame:RegisterEvent('QUEST_GREETING')
|
|
questLogFrame:RegisterEvent('QUEST_PROGRESS')
|
|
|
|
-- Selecting a quest changes the dialog asynchronously. Repeated right-clicks
|
|
-- can emit another greeting/gossip event before that transition completes.
|
|
-- Use a short debounce instead of persistent state: some Turtle dialogs do not
|
|
-- emit a follow-up event, which must never leave automation locked forever.
|
|
local interactionLockedUntil = 0
|
|
local rewardedCurrentDialog = false
|
|
|
|
local function BeginInteraction(kind)
|
|
local now = GetTime()
|
|
if now < interactionLockedUntil then
|
|
return false
|
|
end
|
|
interactionLockedUntil = now + 0.25
|
|
return true
|
|
end
|
|
|
|
local function EndInteraction()
|
|
interactionLockedUntil = 0
|
|
end
|
|
|
|
-- Rewarding a quest can reindex the whole quest log a frame later. Re-scan
|
|
-- once it settles so another quest from the same NPC is redrawn from its own
|
|
-- current completion state instead of the just-removed quest's old slot.
|
|
local postRewardRefresh = CreateFrame("Frame")
|
|
postRewardRefresh:Hide()
|
|
postRewardRefresh.elapsed = 0
|
|
postRewardRefresh:SetScript("OnUpdate", function()
|
|
this.elapsed = this.elapsed + arg1
|
|
if this.elapsed < 0.35 then
|
|
return
|
|
end
|
|
|
|
if pfQuest and pfQuest.UpdateQuestlog then
|
|
pfQuest:UpdateQuestlog()
|
|
pfQuest.updateQuestLog = true
|
|
pfQuest.updateQuestGivers = true
|
|
end
|
|
if pfMap then
|
|
pfMap.queue_update = GetTime()
|
|
end
|
|
|
|
-- One delayed pass is enough to remove the rewarded quest from the
|
|
-- minimap. Without it, a marker could survive until an unrelated hover
|
|
-- or map refresh when the World Map was closed during turn-in.
|
|
if not (WorldMapFrame and WorldMapFrame:IsShown()) then
|
|
this:Hide()
|
|
return
|
|
end
|
|
|
|
-- Scripted dialogue chains can advance their next quest state after the
|
|
-- first reward refresh. Run a second short pass before stopping so an old
|
|
-- yellow turn-in pin cannot remain on the next unfinished quest.
|
|
if this.passes == 0 then
|
|
this.passes = 1
|
|
this.elapsed = 0
|
|
return
|
|
end
|
|
|
|
-- Turtle can reindex the remaining quest rows after the reward has been
|
|
-- claimed. Redraw those active entries once, after that reindex, so a
|
|
-- leftover unfinished quest does not retain the rewarded quest's yellow
|
|
-- turn-in marker. This only runs following an automated reward.
|
|
-- Rebuilding every remaining quest is useful only when the World Map is
|
|
-- visible. Doing it immediately after a reward can hitch badly on newer
|
|
-- quests with many objective nodes, even though no map is on screen.
|
|
if WorldMapFrame and WorldMapFrame:IsShown() and pfQuest and pfQuest.questlog and pfDatabase and pfMap then
|
|
for questid, data in pairs(pfQuest.questlog) do
|
|
if type(questid) == "number" and data.qlogid and data.title then
|
|
pfMap:DeleteNode("PFQUEST", data.title)
|
|
pfDatabase:SearchQuestID(questid, { ["addon"] = "PFQUEST", ["qlogid"] = data.qlogid })
|
|
end
|
|
end
|
|
pfMap.queue_update = GetTime()
|
|
end
|
|
|
|
this:Hide()
|
|
end)
|
|
|
|
local function SchedulePostRewardRefresh()
|
|
postRewardRefresh.elapsed = 0
|
|
postRewardRefresh.passes = 0
|
|
postRewardRefresh:Show()
|
|
end
|
|
|
|
local function CompleteQuestWithRewards()
|
|
if GetNumQuestChoices() == 0 then
|
|
GetQuestReward()
|
|
SchedulePostRewardRefresh()
|
|
end
|
|
end
|
|
|
|
local function SkipLowLevelQuest(isLowLevel)
|
|
return pfQuest_config["autoQuestsSkipLowLevel"] == "1" and isLowLevel
|
|
end
|
|
|
|
local function IsAvailableQuestLowLevel(index)
|
|
-- GetAvailableQuestInfo is not exposed by every Turtle client build.
|
|
if GetAvailableQuestInfo then
|
|
return GetAvailableQuestInfo(index)
|
|
end
|
|
|
|
-- The classic API returns title, level, then its low-level/trivial flag.
|
|
if GetAvailableTitle then
|
|
local _, _, isLowLevel = GetAvailableTitle(index)
|
|
return isLowLevel
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function IsTrivialQuest()
|
|
local title = GetTitleText()
|
|
return string.find(string.lower(title), "low level") ~= nil
|
|
end
|
|
|
|
-- Turtle's GetActiveTitle only returns a title. The matching quest-log row
|
|
-- does expose completion state, so use it to identify turn-ins at greetings.
|
|
local function IsGreetingQuestComplete(title)
|
|
for qlogid = 1, 40 do
|
|
local qtitle, _, _, header, _, complete = pfQuestCompat.GetQuestLogTitle(qlogid)
|
|
if qtitle and not header and qtitle == title then
|
|
return complete and true or false
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Report/talk quests can be listed as active at their destination before
|
|
-- Turtle marks them complete. They have no objective rows, unlike incomplete
|
|
-- kill or collection quests, so they are safe to select at a quest greeting.
|
|
local function IsGreetingQuestReady(title)
|
|
local npcName = UnitName("npc")
|
|
if npcName then npcName = string.lower(npcName) end
|
|
|
|
for qlogid = 1, 40 do
|
|
local qtitle, _, _, header, _, complete = pfQuestCompat.GetQuestLogTitle(qlogid)
|
|
if qtitle and not header and qtitle == title then
|
|
local objectiveCount = GetNumQuestLeaderBoards(qlogid) or 0
|
|
if complete or objectiveCount == 0 then return true end
|
|
|
|
-- Some Turtle talk objectives remain incomplete until their NPC
|
|
-- dialog is opened. When the live objective target is the NPC we
|
|
-- are currently speaking to, selecting this active quest is the
|
|
-- action that grants credit and enables completion.
|
|
if npcName then
|
|
for objectiveIndex = 1, objectiveCount do
|
|
local text, _, done = pfQuestCompat.GetQuestLogLeaderBoard(objectiveIndex, qlogid)
|
|
local _, _, target = string.find(text or "", "^(.-):")
|
|
local normalizedTarget = target and string.lower(target)
|
|
if not done and normalizedTarget
|
|
and string.find(normalizedTarget, npcName, 1, true) then return true end
|
|
end
|
|
end
|
|
|
|
-- Some newer Turtle report quests expose a synthetic unfinished
|
|
-- objective row even though their database record has no real
|
|
-- objective. The active row is only offered by the destination
|
|
-- NPC, so it is safe to select and advance these simple turn-ins.
|
|
if pfDatabase and pfDatabase.GetQuestIDs and pfDB and pfDB["quests"]
|
|
and pfDB["quests"]["data"] then
|
|
local ids = pfDatabase:GetQuestIDs(qlogid, true)
|
|
local questId = ids and tonumber(ids[1])
|
|
local questData = questId and pfDB["quests"]["data"][questId]
|
|
if questData and not questData["obj"] then return true end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Turtle can report false from IsQuestCompletable() for simple talk/report
|
|
-- quests, even when their quest-log row is already complete. QUEST_PROGRESS
|
|
-- is only allowed to advance when either API confirms completion or the
|
|
-- selected/current quest is marked complete in the log.
|
|
local function IsQuestReadyToComplete()
|
|
if IsQuestCompletable and IsQuestCompletable() then
|
|
return true
|
|
end
|
|
|
|
if GetQuestLogSelection then
|
|
local selection = GetQuestLogSelection()
|
|
if selection then
|
|
local _, _, _, header, _, complete = pfQuestCompat.GetQuestLogTitle(selection)
|
|
if not header and complete then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Talk/report quests can reach QUEST_PROGRESS before Turtle marks their
|
|
-- quest-log row complete. Apply the same zero-objective check used when
|
|
-- selecting a completed greeting row so the enabled Continue button is
|
|
-- advanced without treating an unfinished kill or collection quest as
|
|
-- ready.
|
|
local title = GetTitleText and GetTitleText()
|
|
return title and IsGreetingQuestReady(title) or false
|
|
end
|
|
|
|
-- Some Turtle chains use QUEST_PROGRESS for a scripted dialogue or item
|
|
-- handoff without marking the quest-log row complete. The client keeps this
|
|
-- button disabled for an ordinary unfinished quest, so its enabled state is
|
|
-- the authoritative signal; its caption varies between custom quest scripts.
|
|
local function IsQuestDialogueContinue()
|
|
local button = QuestFrameCompleteButton
|
|
if not button or not button:IsShown() or not button:IsEnabled() then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function SelectFirstAvailableQuest()
|
|
if not GetNumAvailableQuests or GetNumAvailableQuests() < 1 then
|
|
return false
|
|
end
|
|
|
|
if SkipLowLevelQuest(IsAvailableQuestLowLevel(1)) then
|
|
return false
|
|
end
|
|
|
|
SelectAvailableQuest(1)
|
|
return true
|
|
end
|
|
|
|
-- Some Turtle NPCs open a normal quest greeting but expose their available
|
|
-- quests only through the gossip API (not GetNumAvailableQuests). This is
|
|
-- notably used by cross-faction/server-custom quest offerings.
|
|
local function SelectFirstGossipAvailableQuest()
|
|
if not GetGossipAvailableQuests or not SelectGossipAvailableQuest then
|
|
return false
|
|
end
|
|
|
|
local available = { GetGossipAvailableQuests() }
|
|
local questIndex = 0
|
|
local i = 1
|
|
while i <= table.getn(available) do
|
|
if type(available[i]) == "string" then
|
|
questIndex = questIndex + 1
|
|
-- Turtle's custom gossip list can omit the usual boolean
|
|
-- low-level flag, yielding title, level, title, level. Only a
|
|
-- real boolean may be used to skip a low-level quest.
|
|
local isLowLevel = type(available[i + 2]) == "boolean" and available[i + 2] or false
|
|
if not SkipLowLevelQuest(isLowLevel) then
|
|
SelectGossipAvailableQuest(questIndex)
|
|
return true
|
|
end
|
|
i = i + (type(available[i + 2]) == "boolean" and 3 or 2)
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function SelectFirstCompletedActiveQuest()
|
|
if not GetNumActiveQuests then
|
|
return false
|
|
end
|
|
|
|
local numActiveQuests = GetNumActiveQuests()
|
|
for i = 1, numActiveQuests do
|
|
local title, complete = GetActiveTitle(i)
|
|
if title and (complete == true or IsGreetingQuestReady(title)) then
|
|
SelectActiveQuest(i)
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Gossip uses a separate quest list. Its active rows can include unfinished
|
|
-- quests, so never select one merely because it is first in the list.
|
|
local function SelectFirstCompletedGossipActiveQuest()
|
|
if not GetGossipActiveQuests or not SelectGossipActiveQuest then
|
|
return false
|
|
end
|
|
|
|
local active = { GetGossipActiveQuests() }
|
|
local questIndex = 0
|
|
local i = 1
|
|
while i <= table.getn(active) do
|
|
if type(active[i]) == "string" then
|
|
questIndex = questIndex + 1
|
|
-- The standard Turtle tuple is title, level, low-level, complete.
|
|
-- Its explicit completion flag is more reliable than matching a
|
|
-- repeated title back to one quest-log row. Retain the log check
|
|
-- for client variants that omit the flag.
|
|
local hasCompleteFlag = type(active[i + 3]) == "boolean"
|
|
local isComplete = hasCompleteFlag and active[i + 3] or false
|
|
if isComplete or IsGreetingQuestReady(active[i]) then
|
|
SelectGossipActiveQuest(questIndex)
|
|
return true
|
|
end
|
|
if hasCompleteFlag then
|
|
i = i + 4
|
|
elseif type(active[i + 2]) == "boolean" then
|
|
i = i + 3
|
|
else
|
|
i = i + 2
|
|
end
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function SelectAutoQuestDialog()
|
|
-- Turtle can expose either list family for the same NPC and can populate
|
|
-- them on different frames. Always inspect both, prefer ready turn-ins,
|
|
-- and accept an available quest only when no ready turn-in is visible.
|
|
return SelectFirstCompletedActiveQuest()
|
|
or SelectFirstCompletedGossipActiveQuest()
|
|
or SelectFirstAvailableQuest()
|
|
or SelectFirstGossipAvailableQuest()
|
|
end
|
|
|
|
-- Turtle may populate normal or gossip quest lists shortly after the initial
|
|
-- event. Retry both list families briefly instead of depending on which event
|
|
-- happened to arrive after the server data.
|
|
local questGreetingRetry = CreateFrame("Frame")
|
|
questGreetingRetry:Hide()
|
|
questGreetingRetry.elapsed = 0
|
|
questGreetingRetry:SetScript("OnUpdate", function()
|
|
this.elapsed = this.elapsed + arg1
|
|
if this.elapsed < 0.1 then
|
|
return
|
|
end
|
|
|
|
-- A few server-scripted greetings populate their active list after the
|
|
-- normal one-second window. Keep retrying briefly; automation remains
|
|
-- opt-in and stops as soon as a row is selected.
|
|
if (pfQuest_config["autoQuests"] == "1" and not IsShiftKeyDown() and SelectAutoQuestDialog()) or this.elapsed >= 2.5 then
|
|
this:Hide()
|
|
end
|
|
end)
|
|
|
|
local function RetryAutoQuestDialog()
|
|
questGreetingRetry.elapsed = 0
|
|
questGreetingRetry:Show()
|
|
end
|
|
|
|
questLogFrame:SetScript("OnEvent", function()
|
|
if pfQuest_config["autoQuests"] == "0" or IsShiftKeyDown() then
|
|
return
|
|
end
|
|
|
|
if event == "QUEST_PROGRESS" then
|
|
EndInteraction()
|
|
-- A progress dialog identifies a new turn-in attempt. Reset the reward
|
|
-- guard here so consecutive quests with the same title can both be
|
|
-- completed while duplicate QUEST_COMPLETE events for one dialog are
|
|
-- still ignored.
|
|
rewardedCurrentDialog = false
|
|
if IsQuestReadyToComplete() or IsQuestDialogueContinue() then
|
|
CompleteQuest()
|
|
end
|
|
end
|
|
|
|
if event == "QUEST_COMPLETE" then
|
|
EndInteraction()
|
|
if rewardedCurrentDialog then
|
|
return
|
|
end
|
|
if GetNumQuestChoices() == 0 then
|
|
-- Some Turtle clients emit QUEST_COMPLETE more than once after a
|
|
-- reward is claimed. Guard this dialog rather than its title: two
|
|
-- different quests at one NPC may legitimately share that title.
|
|
rewardedCurrentDialog = true
|
|
GetQuestReward()
|
|
SchedulePostRewardRefresh()
|
|
elseif QuestFrameRewardPanel.itemChoice and QuestFrameRewardPanel.itemChoice > 0 then
|
|
rewardedCurrentDialog = true
|
|
GetQuestReward(QuestFrameRewardPanel.itemChoice)
|
|
end
|
|
end
|
|
|
|
if event == "QUEST_GREETING" then
|
|
if not BeginInteraction("greeting") then
|
|
RetryAutoQuestDialog()
|
|
return
|
|
end
|
|
|
|
-- Selecting a turn-in opens the completion dialog on the next client
|
|
-- update. QUEST_COMPLETE below then safely claims a no-choice reward.
|
|
if not SelectAutoQuestDialog() then
|
|
EndInteraction()
|
|
RetryAutoQuestDialog()
|
|
end
|
|
end
|
|
|
|
if event == "QUEST_DETAIL" then
|
|
EndInteraction()
|
|
if not IsTrivialQuest() then
|
|
AcceptQuest()
|
|
end
|
|
end
|
|
|
|
if event == "GOSSIP_SHOW" then
|
|
if not BeginInteraction("gossip") then
|
|
RetryAutoQuestDialog()
|
|
return
|
|
end
|
|
|
|
if not SelectAutoQuestDialog() then
|
|
EndInteraction()
|
|
RetryAutoQuestDialog()
|
|
end
|
|
end
|
|
end)
|