首页
社区
课程
招聘
[原创]聊聊游戏辅助那些事上部第三篇——改造C++,让它更适合做辅助脚本。
发表于: 2017-4-13 23:14 11045

[原创]聊聊游戏辅助那些事上部第三篇——改造C++,让它更适合做辅助脚本。

2017-4-13 23:14
11045

聊聊游戏辅助那些事上部第三篇——改造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期)

收藏
免费 0
支持
分享
最新回复 (5)
雪    币: 768
活跃值: (530)
能力值: ( LV13,RANK:460 )
在线值:
发帖
回帖
粉丝
2
大牛,神器放出来吧。。
2017-4-14 03:39
0
雪    币: 193
活跃值: (1023)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
自己实现比挂LUA好在哪里?
2017-4-14 10:14
0
雪    币: 31
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
大牛,请问有写好的项目吗?
2017-4-14 17:41
0
雪    币: 12848
活跃值: (9147)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
5
写错代码炸整个进程系列
2017-4-14 22:46
0
雪    币: 3
活跃值: (14)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
牛B,实在牛。大牛牛
2018-2-6 21:31
0
游客
登录 | 注册 方可回帖
返回
//