找回密码
 点一下
查看: 9322|回复: 21

[重现]为魔兽添加自己的本地函数 原翻译者:白银の游戏王

[复制链接]
发表于 2006-8-20 01:50:30 | 显示全部楼层 |阅读模式
GA的原帖已经丢失,这个帖子还原自飞雪殿的Blog上的备份
http://flyingsnow.myinfo.ws/
注意所有的相关文件都在本帖附件中





在这篇指南中,我将告诉你如何使用xttoc's jAPI来些你自己的War3本地函数。你必须先了解C/C++并且有一个你了解其调用协定的编译器。我将使用Borland's bcc 5.60。很不幸的,我无法得到任何免费的工具。

理论上使用这个技术我们可以做任何事,虽然可能需要大量的研究。但有一个严重的限制,那张地图无法在BNet上发布。我要明确的警告某些为地图写运行库的人,因为那将导致安全问题。不管怎么样,我们先开始。在C程序中数据结构特别细致。我们将继承两个:一个堆-只要动态分配数组即可。然后我们做一些更有想象里的事,一个迷宫的不相交集。


将附件中的API工具材料Copy到你的War3目录,除了jNatives文件夹外-你需要留下文件夹,但要清空其中的内容。你很可能需要为loader建立快捷方式,只需为LoaderWar3.exe添加标记一个window命令行来测试,如"D:\games\Warcraft III\LoaderWar3.exe" -window

我们需要做的流程概要:
-写本地函数
-将本地函数导入War3
-将本地函数的声明添加到common.j
-用本地函数写代码
-运行游戏

我们以经典的Hello World开始。以下是test.cpp



  1. /*
  2. The following routines are all exported by japi.dll:

  3. char* MemStrSearch(unsigned char *start, unsigned char *end, char *str);
  4. bool MemPatternCompare(unsigned char *address, unsigned char *pattern, unsigned char *mask, unsigned long length);
  5. unsigned char* MemPatternSearch(unsigned char *start, unsigned char *end, unsigned char *pattern, unsigned char *mask, unsigned long length);

  6. PIMAGE_SECTION_HEADER GetImageSectionHeaders(HMODULE hModule, WORD *count);
  7. PIMAGE_SECTION_HEADER GetImageSectionHeader(HMODULE hModule, unsigned char name[8]);

  8. void        jAPI jBindNative(void *routine, char *name, char *prototype);
  9. void        jAddNative(void *routine, char *name, char *prototype);
  10. jString jAPI jStrMap(char *str);
  11. char*        jAPI jStrGet(jString strid);
  12. */
  13. // - Andy Scott aka xttocs


  14. #include <windows.h>
  15. #include <stdio.h>

  16. #define jNATIVE        __stdcall
  17. #define jAPI        __msfastcall

  18. #define FloatAsInt(f) (*(long*)&f)
  19. #define IntAsFloat(i) (*(float*)&i)

  20. typedef long jString;
  21. typedef long jInt;
  22. typedef long jReal;

  23. typedef void        (jAPI *jpAddNative)(void *routine, char *name, char *prototype);
  24. typedef jString (jAPI *jpStrMap)        (char *str);
  25. typedef char *        (jAPI *jpStrGet)        (jString strid);

  26. jpAddNative                jAddNative;
  27. jpStrMap                jStrMap;
  28. jpStrGet                jStrGet;

  29. #pragma warning ( disable : 4996 )


  30. jString jNATIVE test(jString js, char *fnname)
  31. {
  32.        char *s;
  33.        s = jStrGet(js);
  34.        s[0] = 'A';
  35.        return jStrMap(s);
  36. }

  37. BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
  38. {
  39.        if (ul_reason_for_call == DLL_PROCESS_ATTACH)
  40.        {
  41.                DisableThreadLibraryCalls(hModule);

  42.                HMODULE hjApi = GetModuleHandle("japi.dll");

  43.                jAddNative        = (jpAddNative)GetProcAddress(hjApi, "jAddNative");
  44.                jStrMap                = (jpStrMap)*GetProcAddress(hjApi, "jStrMap");
  45.                jStrGet                = (jpStrGet)*GetProcAddress(hjApi, "jStrGet");

  46.                jAddNative(test, "test", "(S)S");
  47.        }
  48.    return TRUE;
  49. }
复制代码




在这里有两点很重要


  1. jString jNATIVE test(jString js, char *fnname)
  2. {
  3.        char *s;
  4.        s = jStrGet(js);
  5.        s[0] = 'A';
  6.        return jStrMap(s);
  7. }
复制代码






  1. jAddNative(test, "test", "(S)S");
复制代码




如果运行时崩溃,并且你没有使用Borland的编译器,试一下用__fasecall代替__msfastcall。

我们的测试函数接收一个string的参数并返回一个string。它仅仅简单的用'A'代替string中的第一个字符,并且无法处理传递的空的string。此外,引擎会将调用的函数名传递给本地,所以我们要将函数名包括在内以确保当我们完成时堆栈被释放。

jAddNative函数的第三个参数给出了类型信息,括号内就是参数,如(ISI)代表integer x,string s,integer y。在括号后是返回类型,在我们的例子中是s,即string。

我们需要编译成一个DLL,用一个命令行:


  1. bcc32 -WD -e"test.xjp" test.cpp
复制代码




-WD表示build一个DLL,-e后是输出的文件,test.cpp是输入的源文件。文件扩展名是.xjp而不是.dll,因为这样才能被jAPI识别并链接如War3。现在将test.xjp复制到jAPI将要检查,也就是War3目录下的jNatives文件夹中(如果不复制则留空)。

接下来我们将本地函数添加到common.j。如果你没有它的一份副本,可以用mpq工具从war3patch.mpq中解出来。



[jass]native test takes string s returns string [/jass]



现在从jAPI包中启动LoaderWorldEditor.exe。如果什么也没有发生,可能是war3的注册信息问题,重装下War3试试。如果崩溃了,很可能是你设定的函数的调用约定出了问题,你可以将错误信息发送给我看看。好,现在运行WE,创建一张新地图,做任何您想做的事,将你修改过的common.j导入,并把它的路径从war3mapImported\改为Scripts\来确保它把默认的common.j覆盖。如果几次保存过后你仍然得到本地函数声明问题,检查一下你在common.j中设置的类型和在jAddNative中设置的类型是否匹配。

如果一切OK,写一些JASS来测试test函数。试一下游戏开始几秒后执行BJDebugMsg(test("Hello"))。保存地图。如果仍出现本地函数的声明问题,检查common.j的原型。如果保存无误,不要动“测试地图”按钮。运行jAPI LoaderWar3.exe代替,或者用命令行-window来运行会更合适。选择你的地图并开始、如果选中地图后没有显示玩家信息,很有可能你在.xjp文件中丢失了一些本地函数。所有你添加到common.j中的本地函数在你的自定义DLL中都要有入口。

如果游戏运行后显示"Aello",恭喜,你完成了你的第一个自定义本地函数。现在让我们做一些更有趣的事。动态分配内存的缺乏是JASS编程者的一个共同的遗憾。我们甚至不能传递数组参数!在C中,我们可以轻松的用malloc()来分配一大块内存。这有些危险,因为这些分配的内存并不会随着地图结束而释放。尽管我们需要手动释放,但它依然不失为一个好的实践。简单起见,并且我们有return bug来协调,我们仅仅需要用integer来实现数组。设想我们需要4个函数:



[jass]native ArrayAlloc takes integer length returns integer
native ArrayFree takes integer arr returns nothing
native ArrayGet takes integer arr, integer index returns integer
native ArraySet takes integer arr, integer index, integer val returns nothing[/jass]


将它们添加到common.j。
接下来复制test.cpp到array.cpp并且用上面的数组本地函数代替原来的test本地函数



  1. jInt jNATIVE ArrayAlloc(jInt len, char *fnname)
  2. {
  3.        return (jInt)malloc(sizeof(jInt)*len);
  4. }

  5. jInt jNATIVE ArraySet(jInt array,jInt index,jInt val, char *fnname)
  6. {
  7.        int *p = (int *)array;
  8.        p[index] = val;
  9.        return p[index];
  10. }

  11. jInt jNATIVE ArrayGet(jInt array, jInt index, char *fnname) {
  12.        int *p = (int *) array;
  13.        return p[index];
  14. }

  15. jInt jNATIVE ArrayFree(jInt array, char *fnname) {
  16.        int *p = (int *)array;
  17.        free(p);
  18.        return 0;
  19. }
复制代码



在所有的函数中我都返回了一些值,因为JASS似乎希望所有的函数都有返回值,即使函数什么也没有返回。
要给以上的数组本地函数添加注册信息,我们要写以下几行:



  1.                jAddNative(ArrayAlloc,"ArrayAlloc","(I)I");
  2.                jAddNative(ArraySet,"ArraySet","(III)I");
  3.                jAddNative(ArrayGet,"ArrayGet","(II)I");
  4.                jAddNative(ArrayFree,"ArrayFree","(I)I");
复制代码




用 bcc32 -WD -e "array.xjp" array.cpp 来编译,并用array.xjp代替jNatives文件夹下的test.xjp
测试一些简单的代码,如:



[jass]local integer heap = ArrayAlloc(16)
call ArraySet(heap,5,42)
call BJDebugMsg(I2S(ArrayGet(heap,5)))
call ArrayFree(heap)[/jass]


如果有问题,可以参考上面test实例中的错误信息
练习:为数组添加边界检测和动态调整大小
以下是另一些本地函数,和他的迷宫生成地图,一个不相交集



  1. typedef struct {
  2.        int parent;
  3.        int rank;
  4. } node;

  5. jInt jNATIVE DJSNew(jInt len, char *fnname)
  6. {
  7.        node *set = (node *)malloc(sizeof(node)*len);
  8.        int i;
  9.        for(i=0;i<len;i++) {
  10.                set[ i ].parent = -1;
  11.                set[ i ].rank = 0;
  12.        }
  13.        return (int)set;
  14. }

  15. jInt jNATIVE DJSFind(jInt iset, jInt x, char *fnname) {
  16.        node *set = (node *)iset;
  17.        if (set[x].parent == -1) return x;
  18.        set[x].parent = DJSFind(iset,set[x].parent);
  19.        return set[x].parent;
  20. }

  21. jInt jNATIVE DJSUnion(jInt iset,jInt x,jInt y, char *fnname)
  22. {
  23.        node *set = (node *)iset;
  24.        int xr = DJSFind(iset,x);
  25.        int yr = DJSFind(iset,y);
  26.        if(set[xr].rank > set[yr].rank)
  27.                set[yr].parent = xr;
  28.        else if(set[xr].rank < set[yr].rank)
  29.                set[xr].parent = yr;
  30.        else {
  31.                set[yr].parent = xr;
  32.                set[xr].rank++;
  33.        }
  34.        return 0;
  35. }

  36. jInt jNATIVE DJSFree(jInt iset, char *fnname) {
  37.        free((node *)iset);
  38.        return 0;
  39. }
复制代码



一个比较常用的结构pair和配套函数



  1. typedef struct {
  2.        int car;
  3.        int cdr;
  4. } pair;

  5. pair *allocpair() {
  6.        return (pair *)malloc(sizeof(pair));
  7. }

  8. void freepair(pair *p) {
  9.        free(p);
  10. }

  11. jInt jNATIVE cons(jInt x, jInt y, char *fnname) {
  12.        pair *newpair = allocpair();
  13.        newpair->car = x;
  14.        newpair->cdr = y;
  15.        return (jInt)newpair;
  16. }

  17. jInt jNATIVE car(jInt ipair, char *fnname) {
  18.        pair *p = (pair *)ipair;
  19.        return p->car;
  20. }
  21. jInt jNATIVE cdr(jInt ipair, char *fnname) {
  22.        pair *p = (pair *)ipair;
  23.        return p->cdr;
  24. }
复制代码



原型字母表:



  1. Integer                I
  2. Real                R
  3. String                S
  4. Code                C
  5. Boolean                B
  6. Returns nothing        V
  7. Handles                H; (?)
  8. Handle ext.        H followed by ext followed by semicolon e.g. Hplayer;
复制代码




非常感谢xttocs所做的所有努力和你的阅读。如果你设法用其他不同的编译器做这些工作,或者想用Handle做一些事或者其他任何实际问题,请发送消息!祝你好运。
---------
一些好的编译器
Borland 5.60
Borland 5.5(免费)
---------
重要更新:每个本地函数的最后需要一个额外的"char * fnname"参数。War3会把调用的本地函数名传递进来,我们需要它来确保栈被释放。感谢xttocs。
如果你的stock loader有问题,试一下loader.rar中的那个。
发表于 2006-8-20 02:59:30 | 显示全部楼层
越来越BT老~~555
回复

使用道具 举报

发表于 2006-8-20 03:07:39 | 显示全部楼层
jInt jNATIVE ArrayFree(jInt array, char *fnname) {
       int *p = (int *)array;
       free(p);
       return 0;
}
看不懂这个是什么意思,*P指向array后又free?感觉有点多此一举,或者没什么用,希望解释一下。

jInt jNATIVE DJSFind(jInt iset, jInt x, char *fnname) {
       node *set = (node *)iset;
       if (set[x].parent == -1) return x;
       set[x].parent = DJSFind(iset,set[x].parent);
       return set[x].parent;
}

这里调用DJSFind不是有三个参数吗?怎么变成两个了?

[ 本帖最后由 SkyIsland 于 2006-8-20 03:11 编辑 ]
回复

使用道具 举报

发表于 2006-8-20 03:27:52 | 显示全部楼层
1、因为FREE的是数组内容。此函数就是用来清空数组的。
2、因为调用的时候传递的是三个参数。可是最后一个name暂时用不上。所以就不用了。
回复

使用道具 举报

发表于 2006-8-20 03:32:35 | 显示全部楼层
参数还可以不用。。。Jass真强
回复

使用道具 举报

发表于 2006-8-20 12:35:37 | 显示全部楼层
您错了。这个不是用参数的问题。
因为您要做的是与WAR3相接的函数。他调用的时候就是有这个参数。但是现在做的这个函数不需要用到此参数。所以。就不用。
回复

使用道具 举报

发表于 2006-8-21 15:22:37 | 显示全部楼层
原帖由 amp34 于 2006-8-20 12:35 发表
您错了。这个不是用参数的问题。
因为您要做的是与WAR3相接的函数。他调用的时候就是有这个参数。但是现在做的这个函数不需要用到此参数。所以。就不用。

正解,魔兽的引擎会自动把这个参数传过来,为了保证堆栈平衡,我们需要设置这个参数,但是未必需要用到他。
回复

使用道具 举报

发表于 2006-8-21 18:24:23 | 显示全部楼层
原帖由 amp34 于 2006-8-20 12:35 发表
您错了。这个不是用参数的问题。
因为您要做的是与WAR3相接的函数。他调用的时候就是有这个参数。但是现在做的这个函数不需要用到此参数。所以。就不用。

说得太深奥了,还是不懂丫,呵呵。
回复

使用道具 举报

发表于 2006-8-21 18:24:48 | 显示全部楼层
原帖由 FlyingSnow 于 2006-8-21 15:22 发表

正解,魔兽的引擎会自动把这个参数传过来,为了保证堆栈平衡,我们需要设置这个参数,但是未必需要用到他。

跟函数重载有区别吗?你这种说法我不敢苟同。

因为当你写一个函数的时候,可能会没有参数,可能只有一个参数,也可能会有N个参数。设置这些参数的目的是什么?我认为其目的就是当你需要实现某个功能时,会让一些变量进行一些逻辑运算来返回一个值。这些变量都是实际需要的。
比如:如果需要有三个变量进行逻辑运算来得到一个值,我们就用三个变量,如果只需要两个,我完全可以去掉一个。而不必还保留那一个没用的。如果有多种情况,可能会用到,也可能不会用到,我们一般用函数重载来解决。
我觉得跟堆栈平衡没有什么关系,只是为了实际功能的需要而定。

[ 本帖最后由 SkyIsland 于 2006-8-21 18:32 编辑 ]
回复

使用道具 举报

发表于 2006-8-21 18:28:14 | 显示全部楼层
你可以创建你自己的新函数,没有任何名字的限制。
回复

使用道具 举报

发表于 2006-8-21 22:21:19 | 显示全部楼层
加一组读表的函数还不错~~
回复

使用道具 举报

发表于 2006-8-21 22:24:37 | 显示全部楼层
原帖由 SkyIsland 于 2006-8-21 18:24 发表

跟函数重载有区别吗?你这种说法我不敢苟同。

因为当你写一个函数的时候,可能会没有参数,可能只有一个参数,也可能会有N个参数。设置这些参数的目的是什么?我认为其目的就是当你需要实现某个功能时,会让 ...

你错了,你说的是写一个你需要的函数,而在这里,我们写的是一个魔兽引擎“需要”的函数,它有它的规范,如果你写过matlab的函数你就知道,为了遵守规范,有时我们需要使用无用的参数。
回复

使用道具 举报

发表于 2006-8-21 22:30:38 | 显示全部楼层
难道它有固定的接口吗?
回复

使用道具 举报

发表于 2006-8-21 22:52:23 | 显示全部楼层
最后一个参数虽然我们不需要
但是它是魔兽引擎需要滴固定滴参数...

在实际使用中我发现不加最后一个参数貌似不影响使用
虽然没有具体验证,但很可能因此导致内存泄漏...
回复

使用道具 举报

发表于 2006-8-21 22:56:52 | 显示全部楼层
没有申请内存空间,怎么会泄露呢?
回复

使用道具 举报

发表于 2006-8-21 23:08:05 | 显示全部楼层
函数本身也占用内存恩...
回复

使用道具 举报

发表于 2006-8-21 23:13:26 | 显示全部楼层
函数的参数变量是从外部调用进来的啊,如果没有调用任何变量,也会申请该变量内存空间吗?

这就好象一个人没有买房子,但房产公司送他一套。。。。

糊涂了。
回复

使用道具 举报

发表于 2006-8-21 23:28:00 | 显示全部楼层
并不是说参数占用内存...

魔兽滴引擎可能在调用机制下会将这个函数导入内存或者类似
然后通过函数名来释放该函数占用的内存...
回复

使用道具 举报

发表于 2006-8-21 23:41:07 | 显示全部楼层
这个函数在申请内存空间的时候,难道不会申请参数所要占用的内存吗?
回复

使用道具 举报

发表于 2006-8-21 23:43:50 | 显示全部楼层
由于不清楚魔兽滴引擎具体滴调用原理
所以无法回答LS滴问题...
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 点一下

本版积分规则

Archiver|移动端|小黑屋|地精研究院

GMT+8, 2024-5-2 15:18 , Processed in 0.127244 second(s), 19 queries .

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

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