艾泽拉斯日常

wow插件补充说明篇

2019-10-24  本文已影响0人  听风轻咛

关于TOC文件

关于lua文件加载

假设插件名为TestAddon,有两个Lua文件test.lua,another.lua`。

下面是加载插件的代码:

-- Load TestAddon
local TestAddon = {}

f = loadfile("test.lua")
f("TestAddon", TestAddon)

f = loadfile("another.lua")
f("TestAddon", TestAddon)
-- 上面两个参数,第一个表示插件名,第二个是一个table(这个table可以作为各个文件间交换数据的存储位置)

test.lua:

local addonName, addon = ...

print(addonName .. " is loaded.")

addon.TestAddon = 123

another.lua:

local addonName, addon = ...
print("TestAddon is " .. addon.TestAddon)

每次书写全局变量都需要addon.,开头也是很麻烦的事情,下面推荐一种普通插件的写法:

test.lua中:

-- 插件第一个lua文件中,以下一行代码确保代码以addon为环境,而不是_G
-- 在addon环境中可以访问_G的任意思变量;
-- 所定义的全部全局变量会保存在addon表中,而不是_G;
setfenv(1, setmetatable(select(2, ...), {
    __index = function(self, key) 
        local v = _G[key];
        rawset(self, key, v);
        return v
    end
}))

function testA()
    print("test case A")
end

another.lua中:

setfenv(1, select(2, ...))

-- 在test.lua中定义的函数可以直接调用
testA()

插件的系统事件

local frame = CreateFrame("Frame")

frame:RegsterEvent("ADDON_LOADED")

frame:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        local addonName = ...
        print(addonName .. " is loaded.")
    end
end)

解释下上面的代码:

frame = CreateFrame(type[, name[, parent[, inheritFrom]])

因为我们创建的Frame控件仅用来处理系统事件,所以并不在意它如何被显示(实际上因为没有任何的显示设置,它不会被描绘,也无法被用户实际看到),我们可以省略所有参数来创建一个匿名的Frame界面控件。

frame:RegisterEvent("ADDON_LOADED")

RegisterEvent方法被用来将frame注册到监听 ADDON_LOADED 系统事件的列表中,当 ADDON_LOADED 系统事件发生时,frame会被通知,并进行处理。这个方法只有一个参数,就是需要注册的系统事件名称。

另外,如果需要取消系统事件监听的话,可以使用 UnregisterEvent 方法来取消一个系统事件的监听:

frame:UnregisterEvent("ADDON_LOADED")

wow中,使用CreateFrame创建的Frame对象本身含有一组预定义的方法,这些方法可以用来设置/获取Frame如何被显示,大小,位置等等。如果感兴趣的,可以利用Cube之类的代码编辑器运行下面的代码看看,Frame控件拥有的方法:
for k in pairs(getmetatable(UIParent).__index) do print(k) end

frame:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        local addonName = ...
        print( addonName .. " is loaded.")
    end
end)
-- https://bbs.nga.cn/read.php?&tid=6622128&pid=133288279&to=1

当系统事件发生时,系统会根据监听列表调用各个监听对象去处理系统事件,这里涉及到监听对象如何预先设置处理函数的问题。这点是通过注册控件事件来完成。

RegisterEvent 方法注册系统事件是将 frame 控件对象自己注册到系统事件的监听列表。

重要

使用 if 根据 event 判定的处理方式当我们需要处理大量系统事件时,会比较麻烦,所以,推荐的OnEvent处理代码如下:

-- 当某个系统事件发生时,查找控件对象本身,是否存在和系统事件同名的函数,如果有,就认为是系统事件处理函数,并调用;
-- 注意调用该函数时,控件对象作为第一个参数被传入,这种做法比较适合面向对象的处理思路

frame:SetScript("OnEvent", function(self, event, ...)
    if type(self[event]) == "function" then
        return self[event](self, ...)
    end
end)

function frame:ADDON_LOADED(name)
    print(name .. " is loaded. ")
end

自定义宏命令(代码文件执行阶段)

以下是一个简单的宏命令:

_G.SLASH_TestCmd1 = "/test"
_G.SLASH_TestCmd2 = "/tt"

_G.SlashCmdList.TestCmd = function(msg, input)
    print(msg)
end

_G中创建形如SLASH_ + 名称 + 数字来设置命令,在_G.SlashCmdList为 名称 设置处理函数,如下,上述命令调用;

/tt 这是一个测试

则会打印以下内容,“这是一个测试”;

插件初始化和SavedVariables(游戏进行间数据保存)

TestAddon.toc

## Interface: 50400
## Title: TestAddon
## Notes: A test addon
## DefaultState: Enabled
## LoadOnDemand: 0
## SavedVariables: TestAddonSave

TestAddon.lua

TestAddon.lua

local addonName, addon = ...

-- 构建系统事件监听器
local frm = CreateFrame("Frame")
frame:SetScript("OnEvent", function (self, event, ...)   if type( self[event] ) == "function" then return self[ event ] ( self, ... ) end  end)

-- 注册和处理系统事件
frm:RegisterEvent("ADDON_LOADED")

function frm: ADDON_LOADED(name)
    -- 仅处理自身加载的情况
    if name == addonName then
        -- 获取或者创建SavedVariables
        TestAddonSave = TestAddonSave or {}
    end
end

当退出游戏后,你可以在 WTF\Account[账户名]\SavedVariables\ 下面找到TestAddon.lua,它里面的内容是形如:

TestAddonSave = {

}

PLAYER_LOGIN 进入游戏世界

AutoSell.lua:

local addonName, addon = ...

-- 构建系统事件监听器
local frm = CreateFrame("Frame")
frame:SetScript("OnEvent", function (self, event, ...)   if type( self[event] ) == "function" then return self[ event ] ( self, ... ) end  end)

-- 注册和处理系统事件
frm:RegisterEvent("PLAYER_LOGIN")

-- 进入游戏后,监听商店界面打开事件
function frm:PLAYER_LOGIN( )
    self:RegisterEvent("MERCHANT_SHOW")
end

function frm:MERCHANT_SHOW()
    -- 当商店界面打开时,遍历包裹,售出所有品质为0(灰色)的物品
    for bag = 0, NUM_BAG_FRAMES do
    for slot = 1, GetContainerNumSlots(bag) do
      local itemId = GetContainerItemID(bag,slot)
      if itemId then
        local _, _, itemRarity = GetItemInfo(itemId)

        if itemRarity == 0 then
          UseContainerItem(bag,slot)
        end
      end
    end
  end
end

延时处理和定时器

/in 延时命令

/in 40 /raid 嗜血结束,该自定义命令实现了一个这样的功能,施放嗜血40秒后,通知团队时间结束;

-- 常用函数最好局部变量化,可以加快访问速度
local GetTime = GetTime
local tremove = tremove
local tinsert = tinsert
local floor = floor
local tonumber = tonumber
local strtrim = strtrim
local GetUnitName = GetUnitName

local TARGET_TOKEN_NOT_FOUND = TARGET_TOKEN_NOT_FOUND

-- 事件监听用对象
local frm = CreateFrame("Frame")

-- 隐藏frm后,OnUpdate 事件不会被触发,避免额外的CPU消耗
frm:Hide()

-- 任务列表
local taskList = {}

-- 任务有无标识
local hasTask = false

-- 可重复使用的任务缓存表
-- 实际上同时运行的/in命令很少,每次创建一个新的table会造成额外的浪费
-- 所以复用每个table可以提升性能
local cache = {}

-- 新建任务
local function newTask(delay, command)
  -- 优先使用缓存表,没有备用再创建新表
  local task = tremove(cache) or {}

  -- 替换 %t -> 目标名字
  if command:find("%%t") then
    command = command:gsub("%%t", GetUnitName("target") or TARGET_TOKEN_NOT_FOUND)
  end

  -- 替换 %f -> 焦点名字
  if command:find("%%f") then
    command = command:gsub("%%f", GetUnitName("focus") or TARGET_TOKEN_NOT_FOUND)
  end

    -- 补充 /
  if command:sub(1, 1) ~= "/" then command = "/" .. command end

  -- 因为以0.1秒为最小单位,所以乘10倍整数化利于后续处理
  task.Time = floor( ( GetTime() + delay ) * 10 )
  task.Command = command

  -- 添加任务进入任务列表
  taskList[task] = true

  if not hasTask then
    -- 调整标记,并显示frm,开始监听OnUpdate事件
    hasTask = true
    frm:Show()
  end
end

-- 销毁任务
local function disposeTask(task)
  -- 移除任务
  taskList[task] = nil

  -- 放入缓存表备用
  tinsert(cache, task)

  -- 如果没有额外任务,调整标记,并隐藏frm,停止监听OnUpdate事件
  if not next(taskList) then
    hasTask = false
    frm:Hide()
  end
end

-- 运行后续命令,调用了WOW自带插件的函数,现在不用太在意如何实现
local function runTask(task)
  local command = task.Command

  -- 销毁任务
  disposeTask(task)

  -- 运行命令
  if not command then return end

  ChatFrame10.editBox:SetText(command)
  ChatEdit_SendText(ChatFrame10.editBox)
  ChatFrame10.editBox:SetText("")
end

-- 每0.1秒扫描任务表,所以用lastScan记录上次扫描时间*10 的整数值
local lastScan = 0

frm:SetScript("OnUpdate", function(self, elapsed)
  local nowScan = floor( GetTime() * 10 )

  -- 因为一般情况下,两次OnUpdate间隔仅0.01-0.06左右,不满0.1秒
  -- 减少扫描次数来提高性能
  if lastScan == nowScan then return end

  -- 记录新时间
  lastScan = nowScan

  -- 扫描任务列表并执行
  for task in pairs(taskList) do
    if nowScan >= task.Time then
      -- 使用pcall确保不会因为错误中断扫描
      local ok, ret = pcall(runTask, task)

      -- 如果有错,交给其他错误程序处理
      if not ok then geterrorhandler()(ret) end
    end
  end
end)

-- 注册命令行
SLASH_CMDIN1 = "/in"

-- 处理命令
SlashCmdList.CMDIN = function(msg, input)
    if type(msg) == "string" then
      -- 分拆形如 '40.0 /raid 嗜血结束' -> '40.0', '/raid 嗜血结束'
      local delay, command = msg:match("([%d.]+)%s*(.+)")

      delay = tonumber(delay)
      command = strtrim(command or "")

      -- 检查输入,生成新的任务
      if delay and delay >= 0.1 and command ~= "" then
        return newTask(delay, command)
      end
    end
end

定时器

写一个自动循环叫卖的小插件,命令名字是 /sell,/sell off 用来停止叫卖,/sell 30 /s 无脑循环叫卖 表示每 30 秒叫卖一次,那么,可以修改上面的代码:

-- 常用函数最好局部变量化,可以加快访问速度
local GetTime = GetTime
local tremove = tremove
local tinsert = tinsert
local floor = floor
local tonumber = tonumber
local strtrim = strtrim

-- 事件监听用对象
local frm = CreateFrame("Frame")

-- 隐藏frm后,OnUpdate 事件不会被触发,避免额外的CPU消耗
frm:Hide()

-- 任务列表
local taskList = {}

-- 任务有无标识
local hasTask = false

-- 可重复使用的任务缓存表
-- 实际上同时运行的/in命令很少,每次创建一个新的table会造成额外的浪费
-- 所以复用每个table可以提升性能
local cache = {}

-- 新建任务
local function newTask(delay, command)
  -- 优先使用缓存表,没有备用再创建新表
  local task = tremove(cache) or {}

    -- 补充 /
  if command:sub(1, 1) ~= "/" then command = "/" .. command end

  -- 因为以0.1秒为最小单位,所以乘10倍整数化利于后续处理
  task.Delay = delay
  task.Time = floor( ( GetTime() + delay ) * 10 )
  task.Command = command

  -- 添加任务进入任务列表
  taskList[task] = true

  if not hasTask then
    -- 调整标记,并显示frm,开始监听OnUpdate事件
    hasTask = true
    frm:Show()
  end
end

-- 销毁任务
local function disposeTask()
  if hasTask then
    -- 放入缓存表备用
    for task in pairs(taskList) do
      tinsert(cache, task)
    end

    -- 清空任务列表
    wipe(taskList)

    -- 调整标记,并隐藏frm,停止监听OnUpdate事件
    hasTask = false
    frm:Hide()
  end
end

-- 运行后续命令,调用了WOW自带插件的函数,现在不用太在意如何实现
local function runTask(task)
  local command = task.Command

  -- 重新计算任务下次时间
  task.Time = floor( ( GetTime() + task.Delay ) * 10 )

  -- 运行命令
  if not command then return end

  ChatFrame10.editBox:SetText(command)
  ChatEdit_SendText(ChatFrame10.editBox)
  ChatFrame10.editBox:SetText("")
end

-- 每0.1秒扫描任务表,所以用lastScan记录上次扫描时间*10 的整数值
local lastScan = 0

frm:SetScript("OnUpdate", function(self, elapsed)
  local nowScan = floor( GetTime() * 10 )

  -- 因为一般情况下,两次OnUpdate间隔仅0.01-0.06左右,不满0.1秒
  -- 减少扫描次数来提高性能
  if lastScan == nowScan then return end

  -- 记录新时间
  lastScan = nowScan

  -- 扫描任务列表并执行
  for task in pairs(taskList) do
    if nowScan >= task.Time then
      -- 使用pcall确保不会因为错误中断扫描
      local ok, ret = pcall(runTask, task)

      -- 如果有错,交给其他错误程序处理
      if not ok then geterrorhandler()(ret) end
    end
  end
end)

-- 注册命令行
SLASH_CMDSELL1 = "/sell"

-- 处理命令
SlashCmdList.CMDSELL = function(msg, input)
  if msg == "off" then
    return disposeTask()
    elseif type(msg) == "string" then
      -- 分拆形如 '40.0 /say come on' -> '40.0', '/say come on'
      local delay, command = msg:match("([%d.]+)%s*(.+)")

      delay = tonumber(delay)
      command = strtrim(command or "")

      -- 检查输入,生成新的任务
      if delay and delay >= 0.1 and command ~= "" then
        return newTask(delay, command)
      end
    end
end
上一篇下一篇

猜你喜欢

热点阅读