LUACN论坛

 找回密码
 加入我们

QQ登录

只需一步,快速开始

搜索
热搜: YJWOW MagicStone BoL
查看: 929|回复: 5

从零开始学插件——牛牛爸的插件学习笔记

[复制链接]
发表于 2015-8-13 20:20:35 | 显示全部楼层 |阅读模式
从零开始学插件——牛牛爸的插件学习笔记(1)

作为一个陪伴WOW走过10年的老玩家,一直做着编写代码的工作,却没有写一个像样的插件,实属一大憾事。这一次,牛牛爸将再次走进Lua的知识殿堂,探寻WOW API的秘密花园。以能写一个实用并便于使用的插件作为最终目的。如果你也想写一个自己的插件,不妨跟着牛牛爸一起来学习。

首先要说明下,牛牛爸并不是一个程序员,但做了十几年的网页设计工作,比较熟悉HTML、JavaScript、VBScript,而这次学习Lua的主要参考对象也是JavaScript。如果你熟悉JavaScript或其它编程语言的话,学习Lua会事半功倍,如果你没有编程方面的基础,也不要紧。牛牛爸会给你一些代码实例,并告诉你与之相关的知识点,擅长钻研的你,一定可以顺藤摸瓜、举一反三。

目前WOW插件主要分两大类,一类是以界面定制为主,俗称UI类,另一类则是以实现某种功能为主,俗称功能类。在这里我们的主要研究方向是功能类,所以XML方面的知识基本不会涉及。

从某种角度来讲,功能类的插件比UI类的插件要复杂的多,因为它需要编程语言来支撑,也就是Lua。什么是Lua?相信很多来看此帖朋友都已大体了解。不了解的朋友可以去各大百科网站自行脑补。再次申明,我在这里不会向你复制粘贴一些网上到处都有的定义,我更倾向于告诉你要实现什么功能需要用到什么知识,你上哪里能找到这方面的知识。

先列几个我常去查资料的网站:
http://www.wowinterface.com/
http://wowwiki.wikia.com/
以上两个都是英文网站,不要担心,牛牛爸的英语也很烂,只不过牛牛爸还有这个:
http://translate.google.cn/
Google撤出天朝后,留在天朝的东西已不多,而这个翻译页面无疑是当中最实用的。
当然我们也需要Google搜索,因为在查找编程资料方面,百度是那么的力不从心。对于打不开Google主页的朋友,可以尝试:
http://www.xiexingwen.com/
上面那个网站有时候会当掉,你也可以在百度中搜索“Google镜像”找寻其它Google镜像网站。

有了以上几个网站,你就相当于有了一块耕地,有了粮食种子,你还怕吃不到粮食吗?我知道你已经迫不及待地想要写一个插件出来。好的,不要着急。我们先来了解下插件是由什么文件组成的。其实这些基础知识在NGA或多玩魔兽论坛都可以很容易找到,但我在这里还是要提一下,不然有人会说我净说些没用的。

一个最简单的插件一般有两个文件组成,分别是toc文件和lua文件。toc文件存储着插件的信息,lua文件存储着插件的脚本代码。它们都是纯文本文件,可以使用Windows自带的记事本来编辑,但要注意它们的编码是UTF-8的。我建议你使用Notepad++这款小软件来编写这些文件。编写好的toc文件和lua文件要放到一个以英文命名的文件夹里,然后把这个文件夹复制到wow文件夹下的InterfaceAddOns文件下,如果你已经在使用大脚或魔盒之类的插件,你会发现这里已经有很多文件夹了,像Omen啦,Recount啦,是不是都很熟悉吧?

好了,不说废话了,现在我们就来做一个插件。什么?还没讲toc文件的构成?那是联盟的讲法,而我们是部落。我们先把插件做出来,然后再来理解它的构成。

做个什么插件呢?Hello World?那是联盟的做法,我们部落才不去搞那些没用的。有部落兄弟说了:“咱们明明是部落,为什么动作条两边是狮鹫却不是双足飞龙?”我觉得这个问题提得好。那边又有部落兄弟说了:“我的动作条两边是两个大球啊。”我用忿恨的目光回应他:“你给我死开,你用暗黑界面了不起啊?”其实我也早觉得那两只狮鹫有些碍眼,对于用小屏幕的朋友来说,屏幕左下角用来放伤害统计窗口最适合不过了,但那狮鹫趴在那里实在有碍瞻观。我们第一个插件就是要拿掉这两条狮鹫。

看代码:

CapHide.toc文件:

  • ## Interface: 60100
  • ## Version: 0.1
  • ## Title: CapHide
  • ## Notes: Hide Your MainMenuBarEndCap
  • ## Title-zhCN: 隐藏动作条装饰
  • ## Notes-zhCN: 隐藏动作条两边的狮鹫装饰
  • ## Author: Bill
  • CapHide.lua

复制代码

第1行Interface是声明插件适用的WOW客户端版本,目前6.1,被记作60100。假如你把这里写成60000,你的插件就成过期的了。除非你在WOW的插件设置里启用过期的插件,否则你的插件将不被游戏加载。相应的,等到WOW客户端更新到6.2时,你需要将这里更新为60200。

第2行Version是声明插件自己的版本。为什么不写“1.0”?低调,要注意低调!其实你写“10.0”也没人管你,主要是自己用以区分。每当插件改进了,就可以把这里的数往上加加。因为Interface和Version往往是改动最频繁的,所以放在前两行。

第3行Title是插件的名称,会在英文客户端下显示,所以要写上英文。但估计咱们写的插件也不会有老外来用,所以空着这里或去掉这一行也是可以的。

第4行Notes是插件的说明,也是在英文客户端下显示,同上,可以空着或去掉。

第5行Title-zhCN是插件的中文名称,会在简体中文客户端下显示,就是我们自己要看的啦,起个好听点的名字吧。

第6行Notes-zhCN是插件的中文说明,会在简体中文客户端下显示,随便写写就行。

第7行Author是插件的作者,就是你的名字啦,如果你不想让你的名字在英文客户端下成为鬼画符,就署上你的英文大名吧。

最后一行CapHide.lua是告诉客户端你插件都包含哪些代码文件,一行一个。在这里就只有CapHide.lua啦。

再来看CapHide.lua文件:

  • MainMenuBarLeftEndCap:Hide()MainMenuBarRightEndCap:Hide()

复制代码

没错,只有两行。我们甚至没有用到Lua语句,只调用了WOW API。将你的插件文件夹放到WOW插件文件夹(wowInterfaceAddOns)下,运行WOW,在插件选择界面(点击角色选择界面左下角按钮)中应该就能找到你的插件了。进到游戏里面,你会发现动作条两边的狮鹫不见了。是不是很酷呢?如果你在插件选择界面里找不到你的插件,请检查你文件的编码是否为UTF-8,文件所在文件夹的位置是否正确。

今天的插件知识就介绍到这里,改天我们再继续深入探讨。最后奉上两条小贴士,供喜欢锦上添花的朋友参考。

问:我在插件选择界面里看到有些插件名称的颜色是彩色的,我也想弄成彩色的,该怎么做?
答:以本文代码为例,可以在“隐藏动作条装饰”前面加上代码“|cff00CCFF”,在后面加上代码“|r”,其中“|cff”表示颜色标记开始,“|r”表示颜色标记结束,“00CCFF”就是颜色代码了,这样的代码对做网页和平面设计的朋友来说应该很熟悉。

问:我想让我的插件在插件选择界面里靠前显示,该怎么做?
答:应该不难发现,插件选择界面里的插件名称都是按字符顺序排列的。在字符排序里面,符号比字母优先,所以你想让你的插件名称靠前显示的话,可以在你的插件名前加上符号前缀,比如大脚和魔盒就是通过中括号“[]”来实现排序靠前的。
回复

使用道具 举报

发表于 2015-8-13 20:20:39 | 显示全部楼层
强帖先留个楼再慢慢看
回复 支持 反对

使用道具 举报

发表于 2015-8-13 20:33:38 | 显示全部楼层
从零开始学插件——牛牛爸的插件学习笔记(3)

在上两节,我们做了一个功能非常简陋的小插件。功能是在登录游戏或重载插件时,隐藏动作条两边的装饰,然后可以通过插件界面进行设定,让动作条两边的装饰再显示出来或再次隐藏。但插件无法记住我们的设定,也无法根据当前的状态来自动勾选复选框,显得有些笨拙。这一节,我们来解决这些问题。

首先,我们要引入“角色信息变量”这个概念,“角色信息变量”保存在toc文件中,它是一个或多个全局变量,它的值在退出游戏时自动保存,在登录游戏时自动读取,插件就是靠这个“角色信息变量”来保存插件设定的。现在我们在CapHide.toc中Author那行下面加一行:

    ## SavedVariables: Bill_CapHide

复制代码

Bill_CapHide就是我们声明的“角色信息变量”了,因为是全局变量,所以命名要复杂些,避免跟其它插件的全局变量冲突。除了用SavedVariables来保存“角色信息变量”外,我们还可以使用SavedVariablesPerCharacter,它们的区别是SavedVariables由一个帐号下所有角色共用,也就是说一个帐号下的所有角色使用相同的设置,而SavedVariablesPerCharacter是每个角色单独保存,也就是说一个帐号下的不同角色使用不同的设置。你可以根据你插件的功能来选择使用SavedVariables或SavedVariablesPerCharacter。

现在我们需要修改lua文件,让插件在适当的时候读取和保存Bill_CapHide变量。我们先来看修改后的CapHide.lua文件:

CapHide.lua

  • local f = CreateFrame("Frame")
  • f:RegisterEvent("PLAYER_LOGIN")
  • f:SetScript("OnEvent", function(self, event, ...)
  •         if event == "PLAYER_LOGIN" then
  •                 if Bill_CapHide then
  •                         MainMenuBarLeftEndCap:Hide()
  •                         MainMenuBarRightEndCap:Hide()
  •                 else
  •                         MainMenuBarLeftEndCap:Show()
  •                         MainMenuBarRightEndCap:Show()
  •                 end
  •         endend)

复制代码

上面代码的判断语句部分你应该会比较熟悉,我们上一节在Config.lua文件中写过类似的代码,只是判断条件由复选框是否选中改为了Bill_CapHide的值是否为true。我们在这里使用了简略的写法,将Bill_CapHide==ture写为Bill_CapHide,这在lua代码中是习惯用法,一上来可能看起来有些别扭,习惯了就好了。

在这里还要说下变量类型的问题,通过以上代码,我们基本可以确定Bill_CapHide是个布尔值型变量,其值无非为true、false、nil。当一个变量未赋值之前,值都是nil。而且lua规定nil等同于false。现在我们再回头看以上代码的判断语句部分,当这段代码在第一次执行时,Bill_CapHide并没有赋值,所以它的值是nil,所以判断语句判断到了else和end之间的部分,也就是说我们初次使用这个插件的时候,动作条两边的装饰是显示的。

现在我们再来看判断语句以外的部分。跟上一节Config.lua文件中的代码也有一些类似。新建了一个窗体,这个窗体没有名字也没有父窗体,所以它不会显示出来。我们建它的目的不是要作为界面,而是要注册一个事件。在这里我们注册了“PLAYER_LOGIN”事件,即角色登录事件。你也可以用别的事件,比如"ADDON_LOADED",但要注意不同事件参数的不同。我们这里用“PLAYER_LOGIN”一是因为它用起来简单,二是因为我希望让自己的插件尽量在别的插件加载完成后再加载。这段代码比较难理解的应该是第3行,而这第3行里比较难理解的应该是function小括号里的那些参数,尤其是那个“...”。“...”表示任意数量的参数,即事件有很多参数,在这里我们主要取第2个“event”,后面的我们就不需要了,所以用“...”带过。

有的同学可能会提出一个疑问,为什么要建窗体、注册事件这么复杂,只保留那个判断语句不行吗?答案是不行。因为如果不检测事件,你的代码将在游戏一开始加载时就执行,这时“角色信息变量”的值还未被读出来,也就是说即使你Bill_CapHide的值保存了true,它也读取不出来,仍是nil。所以我们至少要在ADDON_LOADED事件之后再来获取“角色信息变量”的值。而PLAYER_LOGIN事件发生在ADDON_LOADED事件之后,还有些什么事件?你可以自行Google。

我们再来看修改后的Config.lua文件:

Config.lua

  • local f = CreateFrame("Frame", nil, InterfaceOptionsFramePanelContainer)
  • f.name = "CapHide"
  • InterfaceOptions_AddCategory(f)

  • local l = f:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
  • l:SetPoint("TOP", 0, -15)
  • l:SetText("隐藏动作条装饰")

  • local c1 = CreateFrame("CheckButton", "x", f, "OptionsCheckButtonTemplate")
  • c1:SetPoint("TOPLEFT", 10, -45)
  • c1:SetScript("OnShow", function(c)
  •         c:SetChecked(Bill_CapHide)
  • end)
  • getglobal(c1:GetName().."Text"):SetText("隐藏动作条两边装饰")

  • f.okay = function()
  •         Bill_CapHide=c1:GetChecked()
  •         if Bill_CapHide then
  •                 MainMenuBarLeftEndCap:Hide()
  •                 MainMenuBarRightEndCap:Hide()
  •         else
  •                 MainMenuBarLeftEndCap:Show()
  •                 MainMenuBarRightEndCap:Show()
  •         endend

复制代码

前两段没有任何改变。第3段我们加上了三行,即从“c1:SetScript”到“end)”,这三行代码的意思是:当复选框显示的时候,读取Bill_CapHide的值,根据Bill_CapHide的值将复选框勾选或不勾选。这里比较容易产生困扰的是小括号里的那个c,c在这里作为参数的一个变量,实际上是将c1传了过来。也就是说我们可以省掉这个参数,直接在下一行将c写为c1,但是这样写就不严谨了。我们这里在触发事件后只执行了一项操作,如果执行了很多复杂的操作,不用参数传递变量的话,代码就不够灵活了。

再来看最后一段,加上了一行“Bill_CapHide=c1:GetChecked()”,即将复选框的选择状态(true或者false)赋值给了Bill_CapHide。还记得吗?Bill_CapHide是“角色信息变量”,这个变量的值在退出游戏时会自动保存的。假如我们现在选中了复选框,并点了“确定”按钮,Bill_CapHide的值就是true了,当下次登录游戏时,插件会读取Bill_CapHide的值,并根据CapHide.lua里的判断语句将动作条两边的装饰隐藏。既然复选框的选择状态已经赋值给Bill_CapHide了,那下面的判断语句也就不用判断复选框了,直接判断Bill_CapHide就可以了。

看到这里你会发现,CapHide.lua文件和Config.lua文件有7行相同的代码,也就是那个判断语句。在所有的编程语言中都有一个重要的指导思想,那就是避免代码重复。这既能让我们的代码变得简练,又能为以后的改进提供效率。如何在能避免代码重复?我们将在下一节再讨论。现在,你已经有了一个符合标准的插件,虽然它的代码还需要一些优化,但毫无疑问,它可以正常地工作了。它能在你进入游戏时自动加载,你可以通过设置界面改变它的设定,它可以保存你的设定。有句话怎么说的来着?麻雀虽小五脏俱全。是的,它是一只麻雀了。你想让它变成一只金丝雀吗?下一节,我们可以试一下。
回复 支持 反对

使用道具 举报

发表于 2015-8-13 20:33:56 | 显示全部楼层
从零开始学插件——牛牛爸的插件学习笔记(4)

在上小一节,我们的插件基本上已经达到了可以用的程度,但插件代码还需要一些优化,在使用上也可以更方便一些。这一节,就让我们来做一些锦上添花、画龙点睛的工作。

首先,我们要引入“自定义函数”的概念。所谓自定义函数,就是自己写一组代码来实现某个功能,当我们需要用这个功能的时候,可以直接调用这个函数,而不需要每次都写相同的代码。以我们的代码为例,CapHide.lua文件与Config.lua文件都有7行相同的判断语句,我们可以把这个判断语句写成一个函数,代码如下:


  • function BillTools_CapHide()
  •         if Bill_CapHide then
  •                 MainMenuBarLeftEndCap:Hide()
  •                 MainMenuBarRightEndCap:Hide()
  •         else
  •                 MainMenuBarLeftEndCap:Show()
  •                 MainMenuBarRightEndCap:Show()
  •         endend

复制代码

注意,我们function前面没有local,也就是说这是一个全局函数,因为我们CapHide.lua与Config.lua两个文件都将用到这个函数。作为全局函数,我们必须给它一个相对复杂的命名,并注意不要跟我们之前的变量名重名。我们可以把这段代码放到CapHide.lua文件中,原CapHide.lua文件中的判断语句从“if Bill_CapHide then”到“end”,可以改为“BillTools_CapHide()”了,代码如下:


  • local f = CreateFrame("Frame")
  • f:RegisterEvent("PLAYER_LOGIN")
  • f:SetScript("OnEvent", function(self, event, ...)
  •         if event == "PLAYER_LOGIN" then
  •                 BillTools_CapHide()
  •         endend)

复制代码

同样,我们可以替换Config.lua文件中的判断语句,Config.lua文件中最后一段,现在代码如下:


  • f.okay = function()
  •         Bill_CapHide=c1:GetChecked()
  •         BillTools_CapHide()end

复制代码

现在代码是不是简洁多了吧?但我们锦上添花的工作还没有结束。你会不会觉得每次调出游戏菜单,再点击“界面”按钮,再点击“插件”选项卡,再选择“CapHide”来设置我们的插件有些麻烦?可不可以通过一个命令直接调出我们插件的设置界面?答案当然是可以的。请在Config.lua文件加上以下代码:


  • SLASH_BILLTOOLS1 = "/billtools"
  • SLASH_BILLTOOLS2 = "/bt"
  • SlashCmdList["BILLTOOLS"] = function()
  •         -- 暴雪bug,首次调用需调用两次才能打开目标界面
  •         InterfaceOptionsFrame_OpenToCategory("CapHide")
  •         InterfaceOptionsFrame_OpenToCategory("CapHide")end

复制代码

我们从上往下看,SLASH_BILLTOOLS1和SLASH_BILLTOOLS2看似两个全局变量,但却不是普通的全局变量,它们在这里注册了两个命令,分别是"/billtools"和"/bt"。SlashCmdList是什么呢?你可以把它理解为一个接受命令的接口,当你在聊天栏输入一条命令时(以“/”开头),它会检索已注册的命令有没有符合条件的,如果有,它便会执行相关命令的代码。那我们这里要执行的是什么代码呢?首先,我们看到的是一行注释。我们的学习笔记进行到了第4节才提到注释,真是太不应该了。不过相信很多同学都已经知道如何在lua文件中使用注释了。注释下面那两行代码才是我们要执行的代码,至于为什么写了两行相同的代码,注释里已经写得很明白了。这也是我们在这里使用注释的原因,如果没有这行注释,日后你看到这两行相同的代码,还会以为你当时不小心多输了一行呢。因为我们经常会在Notepad++里按“Ctrl+S”的时候不小心按到“Ctrl+D”。

InterfaceOptionsFrame_OpenToCategory("CapHide")的意思应该比较好理解,就是打开插件界面里名字为“CapHide”的窗体。细心的同学可能会提出一个疑问,既然是打开“CapHide”窗体,注册命令的时候为什么不注册"/caphide"或"/ch"?因为我们的插件做到这里,隐藏动作条两边装饰的功能已经基本完结了,而我们的插件还将继续。换言之,CapHide将作为我们插件的一个功能存在,而我们的插件将换一个新的名字。既然是Bill的插件,里面是一些小工具的集合,就干脆叫“BillTools”吧。

保存你的插件,用“/rl”命令重新载入一遍插件,现在输入"/billtools"或"/bt"命令,是不是可以直接打开我们插件的设置界面了吧?最后,让我们再加上一行代码,让插件在每次在你进入游戏或重载插件时提示你插件加载成功,并告知可以使用"/bt"命令打开设置界面。代码如下:

    DEFAULT_CHAT_FRAME:AddMessage("|cff00CCFFBillTools加载成功!使用/bt命令打开设置界面!|r")

复制代码

这行代码加到哪里合适呢?当然是CapHide.lua文件的“BillTools_CapHide()”下面了。既然我们已经用“BillTools”给插件重新命名了,那就把toc文件和lua文件相关的名称也改一下吧。恰好我在写这篇学习笔记时,WOW已经从6.1升级到6.2,toc文件对应的版本号也得调整一下了。最终,调整后的插件文件代码如下:

BillTools.toc

  • ## Interface: 60200
  • ## Version: 0.2
  • ## Title: BillTools
  • ## Notes: Bill's Interface Package
  • ## Title-zhCN: [|cff00CCFFBillTools|r] Bill的工具箱
  • ## Notes-zhCN: Bill的小插件集合
  • ## Author: Bill
  • ## SavedVariables: Bill_CapHide

  • CapHide.luaConfig.lua

复制代码

CapHide.lua

  • function BillTools_CapHide()
  •         if Bill_CapHide then
  •                 MainMenuBarLeftEndCap:Hide()
  •                 MainMenuBarRightEndCap:Hide()
  •         else
  •                 MainMenuBarLeftEndCap:Show()
  •                 MainMenuBarRightEndCap:Show()
  •         end
  • end

  • local f = CreateFrame("Frame")
  • f:RegisterEvent("PLAYER_LOGIN")
  • f:SetScript("OnEvent", function(self, event, ...)
  •         if event == "PLAYER_LOGIN" then
  •                 BillTools_CapHide()
  •                 DEFAULT_CHAT_FRAME:AddMessage("|cff00CCFFBillTools加载成功!使用/bt命令打开设置界面!|r")
  •         endend)

复制代码

Config.lua

  • -- 注册、响应命令
  • SLASH_BILLTOOLS1 = "/billtools"
  • SLASH_BILLTOOLS2 = "/bt"
  • SlashCmdList["BILLTOOLS"] = function()
  •         -- 暴雪bug,首次调用需调用两次才能打开目标界面
  •         InterfaceOptionsFrame_OpenToCategory("BillTools")
  •         InterfaceOptionsFrame_OpenToCategory("BillTools")
  • end

  • -- 主界面
  • local f = CreateFrame("Frame", nil, InterfaceOptionsFramePanelContainer)
  • f.name = "BillTools"
  • InterfaceOptions_AddCategory(f)

  • -- 主标题
  • local r = f:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
  • r:SetPoint("TOP", 0, -15)
  • r:SetText("Bill的工具箱")

  • -- 标题
  • r = f:CreateFontString("r1", "ARTWORK", "GameFontNormalLarge")
  • r:SetPoint("TOPLEFT", 10, -45)
  • r:SetText("界面")

  • -- 横线
  • r = f:CreateTexture(nil, "BACKGROUND")
  • r:SetPoint("TOPLEFT", "r1",  "TOPLEFT", 0, -20)
  • r:SetSize(600,1)
  • r:SetTexture(1, 1, 1, 0.6)

  • -- 复选框
  • local c1 = CreateFrame("CheckButton", "x", f, "OptionsCheckButtonTemplate")
  • c1:SetPoint("TOPLEFT", "r1",  "TOPLEFT", 0, -25)
  • c1:SetScript("OnShow", function(c)
  •         c:SetChecked(Bill_CapHide)
  • end)
  • getglobal(c1:GetName().."Text"):SetText("隐藏动作条两边装饰")

  • -- 确定
  • f.okay = function()
  •         Bill_CapHide=c1:GetChecked()
  •         BillTools_CapHide()end

复制代码

改动的地方比较多,但如果你已经熟悉之前代码的话,一定会一眼看出哪些代码改动了,并能够理解这些改动的作用。这里要着重说下的是Config.lua文件,我们首次使用CreateTexture画了一条横线,也首次使用了SetPoint的相对定位。已经理解CreateFontString的你应该不难理解CreateTexture,已经理解绝对定位的你应该也不难理解相对定位,因为它们都是类似的。

现在,我们的插件已经初具雏形,尽管它的功能还很有限,但我们已经走过了编写一个简单插件的基本流程。剩下的工作就是对这个插件的功能进行扩展了。想必已经对Lua知识融会贯通的你已经想到下一步该加上什么功能了。是屏蔽聊天窗口那些烦人的广告?还是对绿色装备自动贪婪?对于那些能够举一反三的朋友,已经可以脱离我的学习笔记,依靠从相关网站查询到的相关知识来扩展自己的插件了。对于那些还想跟着我的学习笔记一路走下去的朋友,我们下小一节见!
回复 支持 反对

使用道具 举报

发表于 2015-8-13 20:40:32 | 显示全部楼层
MARK!教学贴啊~~~学习ing~~~
回复 支持 反对

使用道具 举报

发表于 2015-8-13 20:49:18 | 显示全部楼层
謝樓主用心且詳細的教學
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 加入我们

本版积分规则

小黑屋|手机版|Archiver|LUACN论坛

GMT+8, 2024-5-23 06:05 AM , Processed in 0.059170 second(s), 23 queries , Gzip On, Redis On.

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表