前言:
承接上篇教程,这次来讲讲如何为CE开发扩展的插件
上篇教程直达车:CE自写插件提供LUA接口实现pvz僵尸绘制之上篇
总教程分成两部分:
本下篇教程为Plugin的开发部分
事前准备
本人利用的工具:CE7.2汉化版(这次要注意版本 ,插件和CE版本相关,汉化与否不影响)
论坛有:CE7.2
VS2015 (可以兼容XP 的插件开发)
VS2015:https://my.visualstudio.com/Downloads?q=visual%20studio%202015&wt.mc_id=o~msft~vscom~older-downloads
CE7.2源码一份:https://github.com/cheat-engine/cheat-engine/archive/7.2.zip
教程内容
VS2015的安装
插件编译情况
CE插件的编译中 example-c里利用的情况是VS2015+WDK8.1( 可以或许兼容XP)
根据我上面提供的链接下载并安装VS2015 (貌似下载前要登陆Microsoft账号)
下载和安装VS2015的教程网上很多,这里不作重点,默认大家能安装好了
PS:安装好后用VS2015打开Plugin插件
Plugin 源码剖析
想知道如何开发一个插件,必不可少的就是去读一读别人的源码
下载并解压源码以后切换到\cheat-engine-7.2\Cheat Engine\plugin\example-c目录下
用VS2015打开.sln文件(如果是第一次打开会让你下载WDK8.1等相关的组件,点击确定让它主动下载安装即可)
源码结构
起首明白一点:我们编写出来的插件是dll格式,即是开发一个动态链接库程序 作为插件给CE调用
我们可以大致将这个例子的结构分为四层
结构包罗文件一个全被注释的头文件和一个空的源文件,留给我们拓展用的,可以删去bla.h bla.cppCE提供给我们的头文件和LUA提供给我们的头文件cepluginsdk.h lauxlib.h lua.h luaconf.h lualib.h主程序代码,即我们要编写插件的关键代码example-c界说我们要导出的函数example-c.def需要用到的静态库文件
我们看到了头文件里有LUA和CE提供的头文件,但源文件里并没有对应的源文件,于是我们知道是引入了.lib静态库文件
那么静态库文件在哪呢?
项目→属性→链接器→输入→附加依赖项
将上方的平台分别改为Win32和X64,可以看到附加依赖项分别显示为:
我们可以知道该项目利用了lua53-32.lib和lua53-64.lib
这两个lib文件就在cheat-engine-7.2\Cheat Engine\plugin目录下
我们可以将这两个.lib文件复制到cheat-engine-7.2\Cheat Engine\plugin\example-c中
下面简朴说一下.lib文件的来源,我们也可以自己生成 这两个.lib文件
编译lib静态库文件(非必须)
cheat-engine-7.2\Cheat Engine\lua53\lua53\vs2013\lua53目录下自己编译.lib静态库文件
生成lua53-64.lib
打开项目以后需要设置一下项目的属性
将lua53的平台工具集改为Visual Studio 2015 - Windows XP (v140_xp)
平台改为x64
然后打开配置管理器,将活动解决方案配置改为Release,活动解决方案平台改成x64
然后Ctrl+Shift+B大概生成→生成解决方案
可以看到我们所需要的静态库文件生成在了cheat-engine-7.2\Cheat Engine\bin下
我图中用红框框出来的就是我们刚刚生成的文件,此中我们只需要用到.lib文件
生成lua53-32.lib
将之前的属性页和配置管理器页中的平台改成Win32,再生成一份lua53-32
编译example-c插件
拿到源码以后最想干的事莫过于先让它跑起来,验证是否可以或许正确运行,于是我们先将插件编译出来再慢慢分析
将前面我们生成好的(大概直接拿上级目录里的)lua53-32.lib和lua53-64.lib复制到cheat-engine-7.2\Cheat Engine\plugin\example-c下
复制完以后打开example-c这个项目
然后确保将配置管理器改成 Release和x64
属性页设置为所有平台
设置完以后就可以Ctrl+Shift+B编译插件了
完善生成插件
在cheat-engine-7.2\Cheat Engine\plugin\example-c\x64\Release检察
我们试着用CE加载一下插件
打开CE→编辑→设置→插件→添加
源码剖析
导出函数说明
我们先看.def文件,检察导出的函数
有三个导出的函数:
分别为:CEPlugin_GetVersion CEPlugin_InitializePlugin CEPlugin_DisablePlugin
我们可以看看有关CE插件的文档:https://cheatengine.org/help/index.html
Cheat Engine comes with a plugin system so developers can add extra functions and features to ce without having to code in Delphi (Any language that can create standard DLL's with normal exports can make plugins for ce), or having to share their sourcecode.
CE 带来了插件体系 如许一来开发者们就可以添加额外的功能和特性到CE里,无需用Delphi 编程(任何语言都可以为CE创建正常导出的标准的DLL)
To make use of the plugin system you need to create a dll that exports 3 functions:
为了确保插件体系的正常运转,你需要创建一个带有以下三个导出功能的dll
GetVersion, DisablePlugin, and InitializePlugin
Note: Unless stated otherwise, all function calls use the stdcall calling mechanism.
注意:除非额外声明,所有的功能调用协定都应该利用stdcall
Cheat Engine comes with some example plugins. They generally make use of a common SDK file. Currently, there is a version for Delphi and a version for C available
CE带来了一些例子插件。它们通常利用一个公共的SDK文件,目前,有一个Delphi版本和一个C版本
所以我们知道为什么导出的函数里有这三个函数了
依次说说这三个函数
GetVersion
The GetVersion routine is a routine that will get called when the dll is queried for the first time.
GetVersion例程是第一次查询dll时将调用的例程
BOOL GetVersion(PPluginVersion pv, //pointer to structure you have to fill inint sizeofpluginversion //size of pluginversion);参数参数类型说明pvPPluginVersion指向PluginVersion的指针,必须填充sizeofpluginversionintpv参数中提供的PluginVersion结构的大小。您可以利用这个来确保您的插件版本与作弊引擎插件体系的当前实现兼容。目前,应该是8返回值返回值类型说明truebool返回值必须设置为1PluginVersion
The PluginVersion lets you specify for which plugin version the dll was developed and what the name of that plugin is.
PluginVersion允许你指定插件版本以及该插件的名称
typedef struct _PluginVersion{ unsigned int version; char *pluginname;} PluginVersion, *PPluginVersion;成员属性说明versionunsigned int(无符号整型)设置这个dll所期望的版本pluginnamechar *(字符串)CE会在initializePlugin上为你的插件提供一个兼容的指针/函数列表。DisablePlugin
BOOL DisablePlugin(void)The DisablePlugin routine is a routine that will get called when Cheat Engine closes or when the user deselects the plugin in settings.
DisablePlugin例程,当CE关闭或用户在设置中取消选择插件时,它将被调用。
返回值返回值类型说明truebool返回值必须设置为1InitializePlugin
The InitializePlugin routine is called when Cheat Engine is started and the plugin is enabled in the registry, or when the user enables it in settings and clicks ok.
当启动CE并在注册表中启用插件时,大概当用户在“设置”中启用插件并单击“确定”时,将调用InitializePlugin例程。
BOOL InitializePlugin(PExportedFunctions ef ,int pluginid);参数参数类型说明efPExportedFunctionsCE的ExportedFunctions结构的地址副本,在从InitializePlugin函数返回之前复制此结构pluginidint插件id,只要你的插件处于活动状态,它的pluginid就一直存在。此ID用于注册回调返回值返回值类型说明truebool返回值必须设置为1It is recommended to register the callback functions in this routine and save ExportedFunctions for later usage. For more information about the callbacks see the relevant help topics
建议在此例程中注册回调函数,并保存ExportedFunctions以备以后利用。有关回调的更多信息,请参阅相关的帮助主题
ExportedFunctions
The ExportedFunctions structure contains several pointers to usefull functions in CE, and pointers to the pointers of specific functions that might be worth hooking.
ExportedFunctions结构包罗几个指向CE中有用函数的指针,以及指向可能值得hook的特定函数的指针。
typedef struct _ExportedFunctions{ int sizeofExportedFunctions; CEP_SHOWMESSAGE ShowMessage; CEP_REGISTERFUNCTION RegisterFunction; CEP_UNREGISTERFUNCTION UnregisterFunction ; PULONG OpenedProcessID ; PHANDLE OpenedProcessHandle ; CEP_GETMAINWINDOWHANDLE GetMainWindowHandle; CEP_AUTOASSEMBLE AutoAssemble; CEP_ASSEMBLER Assembler; CEP_DISASSEMBLER Disassembler ; CEP_CHANGEREGATADDRESS ChangeRegistersAtAddress ; CEP_INJECTDLL InjectDLL ; CEP_FREEZEMEM FreezeMem; CEP_UNFREEZEMEM UnfreezeMem; CEP_FIXMEM FixMem; CEP_PROCESSLIST ProcessList; CEP_RELOADSETTINGS ReloadSettings; CEP_GETADDRESSFROMPOINTER GetAddressFromPointer ; //pointers to the address that contains the pointers to the functions //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! PVOID ReadProcessMemory; PVOID WriteProcessMemory; PVOID GetThreadContext; PVOID SetThreadContext; PVOID SuspendThread; PVOID ResumeThread; PVOID OpenProcess; PVOID WaitForDebugEvent; PVOID ContinueDebugEvent; PVOID DebugActiveProcess; PVOID StopDebugging; PVOID StopRegisterChange; PVOID VirtualProtect; PVOID VirtualProtectEx; PVOID VirtualQueryEx; PVOID VirtualAllocEx; PVOID CreateRemoteThread; PVOID OpenThread; PVOID GetPEProcess; PVOID GetPEThread; PVOID GetThreadsProcessOffset; PVOID GetThreadListEntryOffset; PVOID GetProcessnameOffset; PVOID GetDebugportOffset; PVOID GetPhysicalAddress; PVOID ProtectMe; PVOID GetCR4; PVOID GetCR3; PVOID SetCR3; PVOID GetSDT; PVOID GetSDTShadow; PVOID setAlternateDebugMethod; PVOID getAlternateDebugMethod; PVOID DebugProcess; PVOID ChangeRegOnBP; PVOID RetrieveDebugData; PVOID StartProcessWatch; PVOID WaitForProcessListData; PVOID GetProcessNameFromID; PVOID GetProcessNameFromPEProcess; PVOID KernelOpenProcess; PVOID KernelReadProcessMemory; PVOID KernelWriteProcessMemory; PVOID KernelVirtualAllocEx; PVOID IsValidHandle; PVOID GetIDTCurrentThread; PVOID GetIDTs; PVOID MakeWritable; PVOID GetLoadedState; PVOID DBKSuspendThread; PVOID DBKResumeThread; PVOID DBKSuspendProcess; PVOID DBKResumeProcess; PVOID KernelAlloc; PVOID GetKProcAddress; PVOID CreateToolhelp32Snapshot; PVOID Process32First; PVOID Process32Next; PVOID Thread32First; PVOID Thread32Next; PVOID Module32First; PVOID Module32Next; PVOID Heap32ListFirst; PVOID Heap32ListNext; //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PVOID mainform; PVOID memorybrowser; CEP_GENERATEAPIHOOKSCRIPT sym_nameToAddress; CEP_ADDRESSTONAME sym_addressToName ; CEP_NAMETOADDRESS sym_generateAPIHookScript; CEP_LOADDBK32 loadDBK32; CEP_LOADDBVMIFNEEDED loaddbvmifneeded; CEP_PREVIOUSOPCODE previousOpcode; CEP_NEXTOPCODE nextOpcode; CEP_DISASSEMBLEEX disassembleEx; CEP_LOADMODULE loadModule; CEP_AA_ADDCOMMAND aa_AddExtraCommand; CEP_AA_DELCOMMAND aa_RemoveExtraCommand; CEP_CREATETABLEENTRY createTableEntry; CEP_GETTABLEENTRY getTableEntry; CEP_MEMREC_SETDESCRIPTION memrec_setDescription; CEP_MEMREC_GETDESCRIPTION memrec_getDescription; CEP_MEMREC_GETADDRESS memrec_getAddress; CEP_MEMREC_SETADDRESS memrec_setAddress; CEP_MEMREC_GETTYPE memrec_getType; CEP_MEMREC_SETTYPE memrec_setType; CEP_MEMREC_GETVALUETYPE memrec_getValue; CEP_MEMREC_SETVALUETYPE memrec_setValue; CEP_MEMREC_GETSCRIPT memrec_getScript; CEP_MEMREC_SETSCRIPT memrec_setScript; CEP_MEMREC_ISFROZEN memrec_isfrozen; CEP_MEMREC_FREEZE memrec_freeze; CEP_MEMREC_UNFREEZE memrec_unfreeze; CEP_MEMREC_SETCOLOR memrec_setColor; CEP_MEMREC_APPENDTOENTRY memrec_appendtoentry; CEP_MEMREC_DELETE memrec_delete; CEP_GETPROCESSIDFROMPROCESSNAME getProcessIDFromProcessName; CEP_OPENPROCESS openProcessEx; CEP_DEBUGPROCESS debugProcessEx; CEP_PAUSE pause; CEP_UNPAUSE unpause; CEP_DEBUG_SETBREAKPOINT debug_setBreakpoint; CEP_DEBUG_REMOVEBREAKPOINT debug_removeBreakpoint; CEP_DEBUG_CONTINUEFROMBREAKPOINT debug_continueFromBreakpoint; CEP_CLOSECE closeCE; CEP_HIDEALLCEWINDOWS hideAllCEWindows; CEP_UNHIDEMAINCEWINDOW unhideMainCEwindow; CEP_CREATEFORM createForm; CEP_FORM_CENTERSCREEN form_centerScreen; CEP_FORM_HIDE form_hide; CEP_FORM_SHOW form_show; CEP_FORM_ONCLOSE form_onClose; CEP_CREATEPANEL createPanel; CEP_CREATEGROUPBOX createGroupBox; CEP_CREATEBUTTON createButton; CEP_CREATEIMAGE createImage; CEP_IMAGE_LOADIMAGEFROMFILE image_loadImageFromFile; CEP_IMAGE_TRANSPARENT image_transparent; CEP_IMAGE_STRETCH image_stretch; CEP_CREATELABEL createLabel; CEP_CREATEEDIT createEdit; CEP_CREATEMEMO createMemo; CEP_CREATETIMER createTimer; CEP_TIMER_SETINTERVAL timer_setInterval; CEP_TIMER_ONTIMER timer_onTimer; CEP_CONTROL_SETCAPTION control_setCaption; CEP_CONTROL_GETCAPTION control_getCaption; CEP_CONTROL_SETPOSITION control_setPosition; CEP_CONTROL_GETX control_getX; CEP_CONTROL_GETY control_getY; CEP_CONTROL_SETSIZE control_setSize; CEP_CONTROL_GETWIDTH control_getWidth; CEP_CONTROL_GETHEIGHT control_getHeight; CEP_CONTROL_SETALIGN control_setAlign; CEP_CONTROL_ONCLICK control_onClick; CEP_OBJECT_DESTROY object_destroy; CEP_MESSAGEDIALOG messageDialog; CEP_SPEEDHACK_SETSPEED speedhack_setSpeed; } ExportedFunctions, *PExportedFunctions;这个函数的成员太多了,具体的可以自己去看参考文档QAQ
导出函数具体分析
CEPlugin_GetVersion
BOOL __stdcall CEPlugin_GetVersion(PPluginVersion pv , int sizeofpluginversion){ pv->version=CESDK_VERSION; //这里的CESDK_VERSION为常量6,可以自行修改 pv->pluginname="C Example v1.3 (SDK version 4: 6.0+)"; //像如许的精确字符串是指向dll中的字符串的指针,因此是可行的 return TRUE;}CEPlugin_DisablePlugin
BOOL __stdcall CEPlugin_DisablePlugin(void){ //调用MessageBoxA方法弹窗,表示当前插件已关闭 MessageBoxA(0,"disabled plugin","Example C plugin", MB_OK); return TRUE;}CEPlugin_InitializePlugin
BOOL __stdcall CEPlugin_InitializePlugin(PExportedFunctions ef , int pluginid){ //中间省略的部分为在CE的窗口中添加自己的菜单等界面的功能,暂时没有用到,有兴趣的可以自己研究 //....省略// selfid=pluginid; //....省略// lua_State *lua_state=ef->GetLuaState(); //这里的lua_state固定写成如许就好 //注册lua的调用事件,这个是lua提供的函数 //这里的lua_pluginExample是我们按照LUA规范界说的C语言函数,提供给LUA调用,下面会具体讲解 lua_register(lua_state, "pluginExample", lua_pluginExample); Exported.ShowMessage("The \"Example C\" plugin got enabled"); return TRUE;}关于lua的函数可以参考Lua 函数索引,这里只讲解用到的
lua_register
参考lua_register
void lua_register (lua_State *L, const char *name, lua_CFunction f);把 C 函数 f 设到全局变量 name 中。 它通过一个宏界说:
#define lua_register(L,n,f) \ (lua_pushcfunction(L, f), lua_setglobal(L, n))这里可以知道实在lua_register实在是在调用lua_pushcfunction和lua_setglobal
lua_pushcfunction
void lua_pushcfunction (lua_State *L, lua_CFunction f);参考lua_pushcfunction
将一个 C 函数压栈。 这个函数接收一个 C 函数指针, 并将一个类型为 function 的 Lua 值压栈。 当这个栈顶的值被调用时,将触发对应的 C 函数。
注册到 Lua 中的任何函数都必须依照正确的协议来接收参数和返回值 (参见 lua_CFunction )
lua_CFunction
typedef int (*lua_CFunction) (lua_State *L);界说了要在LUA中调用的C函数的格式,下面看看example-c中的范例
lua_pluginExample
int lua_pluginExample(lua_State *L) //make sure this is cdecl 确保采取默认的cdecl调用协定{ Exported.ShowMessage("Called from lua");//CE中输出消息 lua_pushinteger(L, 123);//将123作为返回值压入到堆栈中 return 1;//返回 返回值的个数,这里只有一个返回值123}C 函数只需要把它们以正序压到堆栈上(第一个返回值开始压入), 然后返回这些返回值的个数
Lua和C/C++语言通信的主要方法是一个无处不在的虚拟栈
lua_setglobal
参考lua_setglobal
void lua_setglobal (lua_State *L, const char *name);从堆栈上弹出一个值,并将其设为全局变量 name 的新值
综上分析我们可以得知lua_register的信息 :
参数参数类型含义Llua_State*指向lua_State的指针,这里我们固定赋值为ef->GetLuaState()即可nameconst char *在lua中要调用时的函数名称flua_CFunction依照正确的lua协议的C函数返回值返回值类型说明numint返回前面压入到堆栈中的个数在CE LUA中调用插件提供的函数
上面给大家讲解了我们插件的源码以及插件中提供的LUA的接口,接下来演示在CE的LUA中调用函数
起首确保加载了example-c插件
可以看到后面的此时插件显示的面板为CEPlugin_GetVersion中我们设置的内容
然后打开LUA窗口:表单→显示CT表的Lua脚本
我们输入lua命令来调用CEPlugin_InitializePlugin中通过lua_register注册的函数
ret=pluginExample() --这里的pluginExample是在lua_register中注册的第二个参数print(ret) --这里的返回值是lua_pushinteger得来的实行以后可以看到:
调用了我们在C中界说好的函数
开发自己的CE Plugin
前面分析完了CE自带的插件例子,我们开始着手自己写一个插件吧
创建项目
打开VS2015 文件→新建→项目 (Ctrl+Shift+N)
点击确定以后出现Win32 应用程序向导
完成以后,一个空的DLL项目就出现了
设置项目属性
我们点击源文件,右键 添加→新建项 (Ctrl+Shift+A)
创建出一个空的main.cpp以后,我们设置一下项目的属性:项目→属性
确保配置为Release 平台为所有平台
然后打开配置管理器,同样修改配置和平台
导入CE相关文件
接下来别忘了导入CE提供的 .lib文件以及.h头文件
将cheat-engine-7.2\Cheat Engine\plugin下的以下文件复制到我们项目的目录下
然后回到VS2015,选中项目 右键 添加→现有项
将我们之前复制的文件全部添加进来
此时.h头文件已经添加进来可以利用了,但静态库.lib文件还需要添加一下引用
项目→属性→链接器→输入→附加依赖项→编辑
然后将lua53-32.lib和lua53-64.lib添加进来
添加完成后显示为
PS:也可以在代码的开头添加下列代码来引入静态库文件
#pragma comment(lib,"lua53-32.lib")#pragma comment(lib,"lua53-64.lib")添加DLL入口函数
这个是DLL函数必须的入口函数,可以从example-c那边拷贝过来,开发DLL的底子~~~
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: //MessageBox(0,"This plugin dll got loaded (This message comes from the dll)","C Plugin Example",MB_OK); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}添加.del导出配置
在源文件这里 右键 新建→新建项
在.def文件中写如以下内容
LIBRARY MyPluginEXPORTS CEPlugin_GetVersion @1 CEPlugin_InitializePlugin @2 CEPlugin_DisablePlugin @3表明以上三个函数为要的导出函数,原因之前已经说过是CE规范要求的
编写自己的功能
接着就在我们的程序中分别添加这三个函数即可
#include #include #include "cepluginsdk.h"//导入静态库#pragma comment(lib,"lua53-32.lib")#pragma comment(lib,"lua53-64.lib")//插件的idint selfid;//CE 提供的导出功能ExportedFunctions Exported;//入口函数BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: //MessageBox(0,"This plugin dll got loaded (This message comes from the dll)","C Plugin Example",MB_OK); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}BOOL __stdcall CEPlugin_GetVersion(PPluginVersion pv , int sizeofpluginversion){ pv->version=CESDK_VERSION; //这里的CESDK_VERSION为常量6,可以自行修改 pv->pluginname="MyPlugin"; //像如许的精确字符串是指向dll中的字符串的指针,因此是可行的 return TRUE;}//满意LUA规范的C函数,这里为调用WIN32的GetWindowRect函数并返回int __cdecl lua_GetWindowRect(lua_State *L) //make sure this is cdecl{ DWORD handle = luaL_checknumber(L, 1); //Exported.ShowMessage("Called from lua"); RECT rect; ZeroMemory(&rect, sizeof(RECT)); GetWindowRect((HWND)handle, &rect); //lua_pushinteger(L, handle); lua_pushinteger(L, rect.left); lua_pushinteger(L, rect.top); lua_pushinteger(L, rect.right); lua_pushinteger(L, rect.bottom); return 4;}BOOL __stdcall CEPlugin_InitializePlugin(PExportedFunctions ef, int pluginid){ selfid = pluginid; //copy the EF list to Exported Exported = *ef; //Exported is defined in the .h if (Exported.sizeofExportedFunctions != sizeof(Exported)) return FALSE; lua_State *lua_state = ef->GetLuaState(); //注册lua的调用事件,这个是lua提供的函数 //这里的lua_GetWindowRect是我们按照LUA规范界说的C语言函数,提供给LUA调用 lua_register(lua_state, "GetWindowRect", lua_GetWindowRect); Exported.ShowMessage("The \"MyPlugin\" plugin got enabled"); return TRUE;}BOOL __stdcall CEPlugin_DisablePlugin(void){ //clean up memory you might have allocated MessageBoxA(0, "disabled plugin", "MyPlugin", MB_OK); return TRUE;}测试自己的功能
然后我们Ctrl+Shift+B编译我们的插件
将生成好的插件用CE加载
测试完毕 !!!我们的函数可以正常调用,否则原本会显示
总结
总算是把这个教程给更新完了,CE的插件功能可以让我们用C来实现很多强大的功能,我只是简朴地带大家相识了一下有关CE的插件开发流程,还可以增加很多有用复杂的功能,我这里限于篇幅也不展开了。底下会附上我开发的插件的源码和插件
最后,渴望大家能一起变强,相互交流,相互进步,我也只是个勉强入门的萌新 ,本领不足,水平有限,渴望大家多多支持和鼓励一下。以上~~~~期待我们的下次再会
下载链接:MyPlugin
源码太大了,只能用自己的阿里云OSS了 QAQ
PS:本插件和上篇教程的插件是同一个插件,改了个名字而已
来源:http://www.12558.net
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有帐号?立即注册
x
楼主热帖