wow插件补充说明篇
关于TOC文件
- Interface - 标记该插件用于wow的哪个游戏版本,如果标记值低于当前游戏版本将不能被加载,除非用户选择加载过期插件,所以,大版本更新时,最好修改这个标记值来符合新的游戏版本,可以在进入游戏后使用宏来获得当前游戏的版本号:
/run print((select(4, GetBuildInfo())))
- Title - 插件的名称,不需要和插件名一致,会被显示在选择人物界面的插件列表中。如果需要实现不同语种的客户端显示不同的文字的话(本地化机能),可以修改标签名,在标签名后加上一个"-",再跟上客户端的语种名就可以了,比如国服的实际标签名: Title-zhCN。如果客户端的语种对应的标签不存在,那么就使用默认的Title标签的值。
- Notes - 在插件列表中,鼠标移到插件名称上时显示的提示信息。同样,可以用类似-zhCN这样的后缀来做本地化申明。
- RequiredDeps, Dependencies, 或者任意以 "Dep" 开始的字符串 - 一些以逗号分割的其他插件的名字,表示本插件必须在这些插件都存在,且都已经被加载的情况下,才能被加载,也就是插件依赖。
- OptionalDeps - 一些以逗号分割的其他插件的名字,表示如果这些插件存在,那么这些插件需要在本插件加载前,被加载,可选依赖。
- LoadOnDemand - 如果这个值被设置为1,那么这个插件不会在游戏开始时就加载,而是由其他插件在需要时,另行加载,wow自带的一些插件都是如此设置的。
- LoadWith - 一些以逗号分割的其他插件的名字, 如果本插件的LoadOnDemand为1,那么,当这些插件在游戏开始被加载时,也加载本插件。
- LoadManagers - 一些以逗号分割的其他插件的名字, 如果这些插件都不存在,那么客户端会自动加载本插件,如果这些插件中的一个存在,那么,本插件将按照LoadOnDemand设置为1的情况处理。
- SavedVariables - 一些以逗号分割的变量名称,这些变量一般是保存在游戏中的Lua全局环境中的表,这些变量的值在退出游戏时将被保存到硬盘上,在下一次游戏开始再被加载进入游戏,可以用来保存用户的配置信息等。这些变量的值对同一账户在同一服务器的所有角色都是通用的。
- SavedVariablesPerCharacter - 类似SavedVariables,不过,每个角色都有各自不同的变量值,也就是说,SavedVariables适合保存通用配置,SavedVariablesPerCharacter适合保存个人配置。
- DefaultState - 本插件安装后默认是否开启,如果设置成 "disabled",那么只有在插件列表中勾选本插件后,才会被加载。默认值 "enabled"
- Author - 作者名字
- Version - 插件自身的版本号
关于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]])
- type - 控件类型,本贴中,只需要用到最基本的Frame控件类型。
- name - 控件名称,如果设定,生成的控件将以这个名字被保存到_G中,多数情况下可以省略,确保和其他插件制作的控件不会产生冲突。
- parent - 每个界面控件都会附着到一个父界面控件,当父界面控件隐藏时,子界面元素也相应的会被隐藏,一般情况下,WOW提供了UIParent 和 WorldFrame 作为其他界面控件的父界面控件。
- 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 控件对象自己注册到系统事件的监听列表。
- self - 控件对象本身
- event - 系统事件名称,对于上面的例子,event只可能时 ADDON_LOADED
- ... - 系统事件参数,对于ADDON_LOADED事件来说,即被加载的插件名称
重要
使用 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