| 06-18-2006, 08:28 PM | #1 |
In this tutorial I'm going to show how to use xttoc's jAPI to write your own warcraft natives. You'll need to know C/C++ and have a compiler whose calling convention you know (IE, can get lucky with). I'll be using borland's bcc 5.60. Unfortunately I couldn't get anything free to work. Theoretically with this technique we'll be able to do anything at all, although it may take quite a bit of research. Even then there'll be a serious limitation in that maps won't be bnet distributable. I'd like to specifically warn against someone writing a general library that maps could call, as this would be begging for security problems. In any case, let's get started. One thing that would be particularly nice to have is C routines for data structures. We'll implement two: One, a heap- just dynamically allocated arrays. Then we'll do something a little fancier, a disjoint set for mazes. First off grab jAPI Tool from this thread:http://www.wc3campaigns.net/showthread.php?t=79652. Stuff all the files into your warcraft3 directory except for the jNatives folder-keep the folder but toss its contents. You'll probably want shortcuts for both the loaders, in particular, add the -window flag to the LoaderWar3.exe for testing, as in target: "D:\games\Warcraft III\LoaderWar3.exe" -window The outline of what needs to happen is: - Write a native - Load native into warcraft - Get custom declaration of native into common.j - Write code that uses native - Run game We'll start with just a hello world. Here's our test.cpp: Code:
/*
The following routines are all exported by japi.dll:
char* MemStrSearch(unsigned char *start, unsigned char *end, char *str);
bool MemPatternCompare(unsigned char *address, unsigned char *pattern, unsigned char *mask, unsigned long length);
unsigned char* MemPatternSearch(unsigned char *start, unsigned char *end, unsigned char *pattern, unsigned char *mask, unsigned long length);
PIMAGE_SECTION_HEADER GetImageSectionHeaders(HMODULE hModule, WORD *count);
PIMAGE_SECTION_HEADER GetImageSectionHeader(HMODULE hModule, unsigned char name[8]);
void jAPI jBindNative(void *routine, char *name, char *prototype);
void jAddNative(void *routine, char *name, char *prototype);
jString jAPI jStrMap(char *str);
char* jAPI jStrGet(jString strid);
*/
// - Andy Scott aka xttocs
#include <windows.h>
#include <stdio.h>
#define jNATIVE __stdcall
#define jAPI __msfastcall
#define FloatAsInt(f) (*(long*)&f)
#define IntAsFloat(i) (*(float*)&i)
typedef long jString;
typedef long jInt;
typedef long jReal;
typedef void (jAPI *jpAddNative)(void *routine, char *name, char *prototype);
typedef jString (jAPI *jpStrMap) (char *str);
typedef char * (jAPI *jpStrGet) (jString strid);
jpAddNative jAddNative;
jpStrMap jStrMap;
jpStrGet jStrGet;
#pragma warning ( disable : 4996 )
jString jNATIVE test(jString js, char *fnname)
{
char *s;
s = jStrGet(js);
s[0] = 'A';
return jStrMap(s);
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);
HMODULE hjApi = GetModuleHandle("japi.dll");
jAddNative = (jpAddNative)GetProcAddress(hjApi, "jAddNative");
jStrMap = (jpStrMap)*GetProcAddress(hjApi, "jStrMap");
jStrGet = (jpStrGet)*GetProcAddress(hjApi, "jStrGet");
jAddNative(test, "test", "(S)S");
}
return TRUE;
}The two important bits here are: Code:
jString jNATIVE test(jString js, char *fnname)
{
char *s;
s = jStrGet(js);
s[0] = 'A';
return jStrMap(s);
}Code:
jAddNative(test, "test", "(S)S"); Our test function takes a string and returns a string. It simply substitutes the first character in the string for the letter 'A', hoping that it wasn't passed a string of zero length. The engine also passes the name of the called function to the native so we include that fnname to make sure the stack gets cleaned up when we're done. That third argument of jAddNative gives the type information. In parentheses comes the arguments. For example, (ISI) would mean takes integer x, string s, integer y. After the parenthesis comes the return type, in our case, S for string. We need to compile a DLL. In a command prompt: Code:
bcc32 -WD -e"test.xjp" test.cpp Next we need to add the native to common.j. If you don't have a copy, extract it with your favorite mpq archiver from war3patch.mpq. JASS:native test takes string s returns string If everything is still going OK, write some JASS to try out your new test function. Try running call BJDebugMsg(test("Hello")) a few seconds into the game. Save your map. If the native doesn't seem to be declared, check that your common.j has the prototype line. If it saves ok, don't bother with the test map button. Instead run the jAPI LoaderWar3.exe, preferably through a shortcut with the -window flag. Head over to your map and start it up. If, upon clicking the map in the map selection screen, no player slots appear, you're probably missing some natives in your .xjp file. All the natives you added in common.j must have an entry in your custom dll. If the game runs and displays "Aello", congratulations, you've written your first custom native. Now let's do something more interesting. One common lament of JASS programmers is the lack of dynamically allocated memory. We can't even pass arrays! In C, however, we can easily malloc() a new chunk of memory. This is a little dangerous, since it won't be cleaned up when the map finishes. We'll have to do it manually for now, but this is good practice anyway. For simplicity, and since we have the return bug for flexibility, we'll just write arrays of integers. So we're going to want four functions: 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 Next copy test.cpp to array.cpp and replace the test native with some array natives: Code:
jInt jNATIVE ArrayAlloc(jInt len, char *fnname)
{
return (jInt)malloc(sizeof(jInt)*len);
}
jInt jNATIVE ArraySet(jInt array,jInt index,jInt val, char *fnname)
{
int *p = (int *)array;
p[index] = val;
return p[index];
}
jInt jNATIVE ArrayGet(jInt array, jInt index, char *fnname) {
int *p = (int *) array;
return p[index];
}
jInt jNATIVE ArrayFree(jInt array, char *fnname) {
int *p = (int *)array;
free(p);
return 0;
}To register them, we'll need these lines in DllMain: Code:
jAddNative(ArrayAlloc,"ArrayAlloc","(I)I"); jAddNative(ArraySet,"ArraySet","(III)I"); jAddNative(ArrayGet,"ArrayGet","(II)I"); jAddNative(ArrayFree,"ArrayFree","(I)I"); Compile this with bcc32 -WD -e"array.xjp" array.cpp Toss out your test.xjp and replace it in the jNatives folder with array.xjp. Try some simple test code, perhaps: JASS:local integer heap = ArrayAlloc(16) call ArraySet(heap,5,42) call BJDebugMsg(I2S(ArrayGet(heap,5))) call ArrayFree(heap) Exercise: Add bounds checking and dynamic resizing to your array. Here's another native, this one's for blu and his maze generation map, a disjoint set: Code:
typedef struct {
int parent;
int rank;
} node;
jInt jNATIVE DJSNew(jInt len, char *fnname)
{
node *set = (node *)malloc(sizeof(node)*len);
int i;
for(i=0;i<len;i++) {
set[i].parent = -1;
set[i].rank = 0;
}
return (int)set;
}
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;
}
jInt jNATIVE DJSUnion(jInt iset,jInt x,jInt y, char *fnname)
{
node *set = (node *)iset;
int xr = DJSFind(iset,x);
int yr = DJSFind(iset,y);
if(set[xr].rank > set[yr].rank)
set[yr].parent = xr;
else if(set[xr].rank < set[yr].rank)
set[xr].parent = yr;
else {
set[yr].parent = xr;
set[xr].rank++;
}
return 0;
}
jInt jNATIVE DJSFree(jInt iset, char *fnname) {
free((node *)iset);
return 0;
}Or some lists for weaaddar: Code:
typedef struct {
int car;
int cdr;
} pair;
pair *allocpair() {
return (pair *)malloc(sizeof(pair));
}
void freepair(pair *p) {
free(p);
}
jInt jNATIVE cons(jInt x, jInt y, char *fnname) {
pair *newpair = allocpair();
newpair->car = x;
newpair->cdr = y;
return (jInt)newpair;
}
jInt jNATIVE car(jInt ipair, char *fnname) {
pair *p = (pair *)ipair;
return p->car;
}
jInt jNATIVE cdr(jInt ipair, char *fnname) {
pair *p = (pair *)ipair;
return p->cdr;
}Prototype letters: Code:
Integer I Real R String S Code C Boolean B Returns nothing V Handles H; (?) Handle ext. H followed by ext followed by semicolon e.g. Hplayer; Big thanks to xttocs for doing all the hard work and thanks for reading. If you manage to get this working with different compilers or how to work with handles or anything really, please post what/how! Good luck. -------- Known good compiler list Borland 5.60 Borland 5.5 (free) -------- Important update: Each native needs an additional "char *fnname" argument at the end. Warcraft passes the name of the called native in so we need to make sure the stack is cleaned up. Thanks xttocs. If you have problems with the stock loader, try the one in loader.rar |
| 06-18-2006, 10:29 PM | #2 |
Tested, and approved (Not that my approval means much, but tested it, and i know it works) |
| 06-19-2006, 01:39 AM | #3 | |
I got stuck here: Quote:
|
| 06-19-2006, 01:55 AM | #4 |
I think that you should also have that common.j in a scripts subfolder of war3's folder. And make sure you didn't make syntax errors when adding the natives to common.j. I am talking out of my ass right now, but what if you try to add a: type superarray extends handle and then make those natives use superarray as argument / return value ? hmnn I should probably take my mingw32 and try to make this work there and then test the monster myself. -- I renamed the title, also I moved it to misc tutorials. It is not really a JASS tutorial so it wouldn't fit there |
| 06-19-2006, 03:53 AM | #5 |
Getting a little type safety on the arrays would be nice. However I'm pretty happy with integers since we don't have to return bug. I think if we want type safety it should come from a language on top of jass. Speaking of the return bug, you can write proper typecast natives that do the typecasting in C. This'll compile to just move input to output, so zero cost and way faster than the return bug. We can also do it with out understanding handles. Apparently borland has a free as in beer compiler. Let me know if it requires any tricks. - attached a test map with all the ingredients to help folks narrow down the source of problems. - Added prototype information |
| 06-19-2006, 04:27 AM | #6 |
figured out gcc wouldn't work. I don't want a free as in beer compiler, I don't want to compile things with propietary compilers, I also had some unfun experiences with borland's vision of c++ but well maybe one of these days I'll try. I wouldn't have any problem in using integers, but I am corious as if using the type define in common.j would actually work. |
| 06-19-2006, 02:40 PM | #7 |
I am not familiar with c++, so it may take some time till I actually get all the constant keys, but anyways. Great tut! +Rep |
| 06-19-2006, 03:31 PM | #8 | |
Quote:
:D I'll have a look at this stuff when I get back from holiday, but I'm not sure this will ever be useful for general modding, due to the lack of portability (unfortunately :/). |
| 06-19-2006, 10:10 PM | #9 |
I can't compile this even changing __fastcall instead of __msfastcall. I am using Dev-C++ that is a free program, i am having problems with the command prompt, because i don't know what is that and i don't know if Dev-C++ have it. Please help me. PD: You think that with borland c++ 5 this work? |
| 06-20-2006, 01:47 AM | #10 |
Can't understand why would anyone use dev-c++ instead of code::blocks . Anyways it is surelly configured to use gcc (mingw32) as compiler by default, you need to get the borland compiler and then configure dev-c++ to use it instead |
| 06-20-2006, 04:31 AM | #11 | |
Quote:
Why you say that? (reply this, please) Anyway thanks for the advice, but how i can change the compiler if i have the borland one?. |
| 06-20-2006, 07:10 AM | #12 |
You don't have to compile from the IDE if you don't know how to configure it. You can compile from command line. Cool tutorial btw. |
| 06-20-2006, 08:59 AM | #13 | |||
Quote:
ex: common.j JASS://Custom junk type arr extends handle //Dynamic memory native ArrayAlloc takes integer len returns arr native ArraySet takes arr heap, integer index, integer value returns integer native ArrayGet takes arr heap, integer index returns integer native ArrayFree takes arr heap returns integer JASS:function Trig_trynative_Actions takes nothing returns nothing local arr heap call TriggerSleepAction(1.0) set heap = ArrayAlloc(16) call BJDebugMsg(I2S(HtoI(heap))) call ArraySet(heap,0,5) call BJDebugMsg(I2S(ArrayGet(heap,0))) call ArrayFree(heap) endfunction Code:
jAddNative(ArrayAlloc,"ArrayAlloc","(I)Harr;");
jAddNative(ArraySet,"ArraySet","(Harr;II)I");
jAddNative(ArrayGet,"ArrayGet","(Harr;I)I");
jAddNative(ArrayFree,"ArrayFree","(Harr;)I");Quote:
Quote:
Code:
long addnativewrap(void *routine, char *name, char *prototype) {
__asm {
mov ecx,routine
mov edx,name
push prototype
mov eax,jAddNative
jmp eax //Some compilers don't like jumping to things that aren't labels
}
} |
| 06-21-2006, 05:41 PM | #14 | |
Quote:
How i can use a command prompt?, how i can acceses to it?, somebody can explain me please? |
| 06-21-2006, 06:08 PM | #15 |
I assume you use windows. Start->Run->command. |
