聊聊游戏辅助那些事上部第三篇——改造C++,让它更适合做辅助脚本。
有很久没有写这个系列了,一个原因是当前的脚本因为沿用了最初的指令式,我采用了定时执行的模式,当采用指令式脚本时,因为语法简单,定时执行并没有多大问题,但当改为c++后,定时执行的弊端就体现出来,需要自己维护堆栈,不知不觉,主解析函数竟已多达5000行代码,实在太过庞大,我一直想改成线程式,以便把解析函分解成多个函数。第二篇已经预告了,又不能不写,所以暂时对于脚本只能先写点边缘内容,在以后有时间再来详细说说如何解析C++语法了。
一、关于脚本的几种形式: 对于脚本我走过几个阶段,最早的脚本是指令式,每一行就一个指令,一个指令包含一个复杂的操作,比如 Goto 是跑路指令,Wait是等待指令。实现的伪代码:
首先定义模板类实例,把指令解析函数与指令名称对应起来。使用 atl 的影射模板。
BOOL (*PARSEPROC)(LPCSTR);
CAltMap<CString, PARSEPROC> map; map["goto"] = Goto; map["wait"] = Wait;
然后是主解析主函数:
BOOL DisaptchTask(LPCSTR lpText) { char code[16] strtok_s(lpText, ":", code, 16); tolower(code); auto* pair = map.Lookup(code); if(pair) { return pair->m_value(lpText); } return FALSE; }
优点是简单高效,主要的执行单元都由挰序内部完成,容错性强。缺点是脚本不够灵活,基本功能都必须由内部实现,没有变量空间的概念,不能定义局部变量。只能使用有限的几个全局变量。
然后我转向 C++, C++的灵活性无需多说,很多脚本语言都源自C++,比如javascript, php等。用 C++ 优点是很多不常用不追求效率的功能可以直接由脚本实现,程序只需要导出游戏的基本功能,比如按钮单击,技能使用,道具使用,自动寻路等,其它的可以交给脚本来实现,而缺点是要实完整的 C++ 语法实在是项大工程,非一朝一夕可以完成。
二、c++语法的几点改造: 作为游戏辅助的脚本语言却有几个特别之处,不能完全依循C++的语法。
1、灵活的错误处理。 作为辅助脚本,错误有时是常态,所以一个好的错误处理可以让脚本更强壮。在脚本除了支持try{}catch(){}语法之外,另外加入了更灵活的错误处理方法,摈弃了try,改为只需要单独的catch来处理错误:
catch(普通错误){错误处理函数();} 任务过程();
上面的脚本中,正常执行是不会执行花括号中的 错误处理函数(),只有当捕获到一个普通错误时才会转去执行错误处理函数(),处理完错误后继续下面的 任务过程();一个catch语句可以捕获在它之后的所有错误,包括子函数、子脚本。也可以捕获多种错误。
catch(普通错误,严重错误){错误处理函数();} 任务过程();
或者有多个catch,总是最后的catch先检查可以捕获的错误,没有则递交给它前面的catch语句,如果最后都没有catch语句捕获错误,则会让脚本中止。 catch(严重错误){严重错误处理函数();} 准备过程();
catch(普通错误){普通错误处理函数();} 任务过程1();
catch(普通错误){普通错误处理函数();} 任务过程2();
2、改造后的switch语句。可以用字符串作为case分支,方便任务派遣。使用switch语句的好处是,脚本可以乱序执行。
switch(获取任务名()) { case "东山再起": 东山再起(); break;
case "赤壁战火": 赤壁战火(); break;
。。。 }
3、改造后的 goto 语句。用一个 //-> 来定义一个标号。在 C++ 中是注释语句。
goto 获取任务名();
//->东山再起: 东山再起();
//->赤壁战火: 赤壁战火();
。。。
4、中文支持。C++本身就支持中文,而且使用 C11 新特性,支持中文就更方便了。比如:
const auto 创建窗口=CreateWindowEx;
或者:
#define 创建窗口 CreateWindowEx
这样所有用到 CreateWindowEx 都可以用 创建窗口 来代替。
5、插件支持:
普通的dll;
申明一个dll的导出函数: BOOL(*GetWindowRect)(HWND hWnd,LPRECT lpRect) = GetProcAddress(GetModuleHandle("User32.dll"),"GetWindowRect");
专用插件导出函数、变量、常量、类型、模板等,动态或静态加载插件: #import "xxx.dll"
定义一个函数结构,统一插件函数与脚本函数,或者是重载函数头。
typedef struct TFUNCTION
{
BYTE m_functionType; // 类型,区分插件函数,接口函数,脚本函数,重载头等
BYTE m_functionParam; // 参数表类型,固定参数表,不定长参数表,需要传递参数个数的不定长参数表,需要传递参数类型的不定长参数表
WORD m_functionStyle; // 标志符,可设置为 FS_USERETTEXT 返回值类型, (1 << NT_XXXXXFUNCTION) 可当作某类函数使用
union
{
FARPROC m_proc; // 插件或接口函数指向函数地址
int m_vindex; // 虚函数索引
LPCTSTR m_body; // 脚本函数指向函数代码
TFUNCTION** m_functionlist; // 重载函数头指向函数表
};
union
{
struct
{
WORD m_rettypextra; // 额外的返回类型,任务,脚本,异步脚本,终止脚本,异常
WORD m_paramextra; // 附加参数
};
class CInterpreter* m_pScript; // 脚本函数的函数所在的脚本,用于查找全局变量
};
short m_paramleast; // 必要参数个数,不保括默认参数。
union
{
short m_paramcount; // 固定参数个数,脚本函数该值 大于等于 0 表标函数参数已分析 m_paramlist 有效
short m_functioncount; // 重载函数头为重载函数个数
};
union
{
LPCTSTR m_rettext; // 返回值类型
UINT m_rettype;
};
LPCTSTR m_name; // 函数名
LPCTSTR m_param; // 参数表
union
{
LPCTSTR m_paramlist_notes; // 字符串表述的参数类型表,紧接类型表为注释
LPDWORD m_paramlist; // 参数类型表
};
}const FAR* LPCTFUNCTION;
然后只需要定义一个数组
TFUNCTION DllInterfaceList[] =
{
{FT_PLUGIN, FIXEDCOUNT, FS_USERETTEXT, (FARPROC)GetModuleHandle,
RVT_NORMAL, NEEDEMPTY, 1, 1, _T("HMODULE"), _T("GetModuleHandle"), _T("(LPCTSTR lpModuleName)"), VS_P_T CI_I2_T }
, {FT_PLUGIN, FIXEDCOUNT, FS_USERETTEXT, (FARPROC)GetProcAddressW,
RVT_NORMAL, NEEDEMPTY, 2, 2, _T("FARPROC"), _T("GetProcAddress"), _T("(HMODULE hModule, LPCTSTR lpProcName)"), VS_P_T CI_V_T VS_P_T CI_I2_T }
, {FT_PLUGIN, FIXEDCOUNT, FS_USERETTEXT, (FARPROC)LoadLibrary,
RVT_NORMAL, NEEDEMPTY, 1, 1, _T("HMODULE"), _T("LoadLibrary"), _T("(LPCTPATH lpLibFileName)"), VS_C_P_T CI_PATH_T }
, {FT_PLUGIN, FIXEDCOUNT, FS_USERETTEXT, (FARPROC)FreeLibrary,
RVT_NORMAL, NEEDEMPTY, 1, 1, _T("BOOL"), _T("FreeLibrary"), _T("(HMODULE hLibModule)"), VS_P_T CI_V_T }
}; 最后添加到添加到变量空间中,插件函数是函数类型的变量。
void AddVariant(LPCTFUNCTION pfl, int nCount) { for (int i = 0; i < nCount; i++, pfl++) { ATLASSERT(Lookup(pfl->m_name) == NULL); new(&(*this)[pfl->m_name]) TEVARIANT(EVT_FUNCTION, pfl); } }
然后整个插件被加载为 C++ 变量空间,与变量空间有公共的生成期。
这一篇写得比较乱,下一篇上干货; 聊聊游戏辅助那些事上部第四篇——优化到极致的 A*路径搜索 算法。是不是吹牛呢?到时候见真章。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)