XGM Forum
Сайт - Статьи - Проекты - Ресурсы - Блоги

Форуме в режиме ТОЛЬКО ЧТЕНИЕ. Вы можете задать вопросы в Q/A на сайте, либо создать свой проект или ресурс.
Вернуться   XGM Forum > Warcraft> Академия: форум для вопросов> Желтая пресса: обучающие статьи
Ник
Пароль
Войти через VK в один клик
Сайт использует только имя.

 
Van Damm
wait... what?
offline
Опыт: 22,268
Активность: 0
Добавление собственных Native функций в Warcraft III
Часть статьи является вольным переводом руководства с WC3C авторства PipeDream

Часть 0: Вступление

Думаю, почти всем людям, занимающимся созданием карт для Warcraft 3 и использующим возможности скриптового языка JASS2 на полную, приходили в голову мысли вроде "А вот если бы вот здесь сделать как в C++ (подставить другой язык программирования по желанию)". Но это было невозможно до тех пор, пока xttocs не написал JAPI - подгрузчик собственных native-функций в запущенный процесс игры. Это случилось давно - почти три года назад (релиз japi 1.0 состоялся 24.01.2006), но до сих пор остается малоизвестным и редко используется. С помощью статьи я хочу распостранить эти знания среди русскоговорящих картостроителей.

Скажу сразу: эта статья не предназначена для новичков и людей, не понимающих в программировании.

В процессе написания статьи, я буду давать некоторые сведения, не обязательные к прочтению - они будут скрыты под катами.

Часть 1: Инструменты и знания

Итак, для начала определимся, что нам понадобится в этом нелегком деле:
  • Знание C/C++
  • блокнот для редактирования .cpp и .lua (рекомендую Notepad++)
  • Компилятор с известным вам соглашением вызова (здесь я использую Borland C++ 6)
  • Warcraft 3 версии 1.21b и ниже (как известно, Grimoire не работает с игрой версии 1.22)
  • Grimoire (скачть можно здесь, на XGM не последняя версия)
  • japi.dll (входит в состав grimoire)

Часть 2: Порядок действий и простейший пример

Итак, обозначим порядок наших действий:
  1. Написать нативку и скомпилировать dll
  2. Добавить объявление написанной нативки в common.j
  3. Написать код, использующий нативку
  4. Запустить игру

Для начала возьмем классический пример "Hello world": напишем функцию, которая не принимает параметров, а возвращает некоторую строку. Вот наш test.cpp
Код:
#include <windows.h>
#include <stdio.h>

#define jNATIVE __stdcall
#define jAPI      __cdecl

#define jString long
#define jInt     long
#define jReal   long

typedef void(jAPI *jpAddNative)(void *routine, char *name, char *prototype);
typedef jString(jAPI *jpStrMap)(const char *str);
typedef const char*(jAPI *jpStrGet)(jString strid);

jpAddNativejAddNative = NULL;
jpStrMapjStrMap = NULL;
jpStrGetjStrGet = NULL;

jString jNATIVE test()
{
   char *s = "ZOMG TEH FUNC!";
   return jStrMap(s);
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
   if (ul_reason_for_call != DLL_PROCESS_ATTACH) return TRUE;
   DisableThreadLibraryCalls(hModule);

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

   jAddNative = (jpAddNative)GetProcAddress(hjApi, "jAddNative");
   jStrMap= (jpStrMap)GetProcAddress(hjApi, "jStrMap");
   jStrGet= (jpStrGet)GetProcAddress(hjApi, "jStrGet");

   jAddNative((void *)test, "test", "()S");

   return TRUE;
}


Два важных места здесь - это:
Код:
jString jNATIVE test()
{
   char *s = "ZOMG TEH FUNC!";
   return jStrMap(s);
}
и
Код:
jAddNative((void *)test, "test", "()S");
Третий аргумент функции jAddNative определяет типы, передаваемые функции и возвращаемые ей.

Теперь скомпилируем DLL. Для этого в командной строке напишем
Код:
bcc32 -WD -e"test.dll" test.cpp
-WD значит компилировать dll, -e определяет выходной файл и test.cpp - соответственно наш файл с исходным текстом.

Следующим шагом нам необходимо добавить эту функцию в common.j (если у вас его нет, всегда можно достать последнюю версию из war3patch.mpq):
Код:
native test takes nothing returns string


Теперь создадим карту, в которой будет вызвана наша функция. Назовем её test.w3m и сохраним в папку Maps директории варкрафта. В карте создадим триггер Test, выполняющийся при инициализации и в действия добавим:
Код:
call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60,test())
Не забудьте импортировать в карту измененный common.j с путем "scripts\common.j", иначе при сохранении будет выдана ошибка.


Пришло время запускать игру. Распакуйте Grimoire в какую-то директорию неподалеку и приготовьтесь печатать. Открываем startwar3.bat, пишем и сохраняем:
Код:
bin\exehack.exe -s war3.lua -loadfile "Maps\test.w3m"
Теперь редактируем ongameload.lua, вот всё, что должно в нем остаться:
Код:
loaddll("bin\\japi.dll")
loaddll("bin\\test.dll")
» Что такое луа?
Lua — интерпретируемый язык программирования, разработанный подразделением Tecgraf Католического университета Рио-де-Жанейро. Является свободно распространяемым, с открытыми исходными текстами на языке Си.

По возможностям, идеологии и реализации язык ближе всего к JavaScript, однако Lua отличается более мощными и гораздо более гибкими конструкциями, спроектирован с целью «не плодить сущности сверх необходимого». Хотя Lua не содержит понятия класса и объекта в явном виде, механизмы объектно-ориентированного программирования с поддержкой прототипов (включая множественное наследование) легко реализуются с использованием метатаблиц, которые также позволяют перегрузку операций и т. п. Реализуемая модель ООП (как и в JavaScript) — прототипная.

Lua в переводе с португальского значит «луна», поэтому неправильно писать «LUA» — так же, как и неправильно писать любое другое слово одними только прописными символами.

~Википедия


Предварительные ласки завершены. Теперь можно, скрепя сердце, запускать startwar3.bat, не забыв перед этим удостовериться что наша test.dll находится в папке grimoire\bin\. (да, и проверьте чтобы в папке с игрой не валялся scripts\common.j) Если вы все сделали правильно, игра запустится и сразу загрузит нашу тестовую карту, которая не преминет удивиться в ответ =)

Данный этап можно считать завершенным успешно.
» А что собственно происходит?
Перехватчиком вызовов jass-функций выступает библиотека japi.dll, которая внедряется в процесс варкрафта с помощью Grimoire.

Конкретно внедрением занимается exehack.exe, который принимает параметры для своей работы из файла script.lua, расположенного с ним в одной папке. При запуске с ключом -s, парсит переданный ему в качестве аргумента файл lua.

Мы аргументом передаем war3.lua, в котором сначала определяется путь к варкрафту, а затем запускается игра и в процесс внедряется ongameload.dll

ongameload.dll считывает ongameload.lua и внедряет в процесс вызовы библиотек, указанных в нем, после чего процесс размораживается и идет его нормальное выполнение.

Вот и вся схема подгрузки библиотек в нашем случае.


Часть 3: Пишем что-то полезное


Теперь перед вами раскрываются достаточно широкие горизонты. Вот лишь несколько примеров:

1) Быстрая конвертация строки типа "A001" в целое число и обратно:
Код:
jInt jNATIVE String2RawCode(jString input) 
{
   const char *str = jStrGet(input);
   if (strlen(str) == 1) 
      return str[0];
   else if (strlen(str) == 4) 
   {
      char newstr[4];
      newstr[0] = str[3];
      newstr[1] = str[2];
      newstr[2] = str[1];
      newstr[3] = str[0];
      return *((jInt*)newstr);
   }
   return 0;
}

jString jNATIVE RawCode2String(jInt input) 
{
   char *str = (char*)&input;
   char newstr[5];
   if (input < 255) 
   {
      newstr[0] = str[0];
      newstr[1] = '\0';
   } 
   else 
   {
      newstr[0] = str[3];
      newstr[1] = str[2];
      newstr[2] = str[1];
      newstr[3] = str[0];
      newstr[4] = '\0';
   }
   return jStrMap(newstr);
}

2) Динамически аллоцируемый массив
Код:
jInt jNATIVE ArrayAlloc(jInt len)
{
   return (jInt)malloc(sizeof(jInt)*len);
}

jInt jNATIVE ArraySet(jInt array,jInt index,jInt val)
{
   int *p = (int *)array;
   p[index] = val;
   return p[index];
}

jInt jNATIVE ArrayGet(jInt array, jInt index) 
{
   int *p = (int *) array;
   return p[index];
}

jInt jNATIVE ArrayFree(jInt array) 
{
   int *p = (int *)array;
   free(p);
   return 0;
}
И так далее.

В дальнейшем поможет лишь анализ japi.dll и личные опыты. Также полезным может оказаться вот эта ссылка

Часть 4: Личные наблюдения

4.1 Достаточный минимум

Путем некоторых исследований, мной было установлено, что для простой подгрузки своих native-функций нет надобности возить за собой весь grimoire: достаточно минимального комплекта:
Код:
bin\<ваша библиотека>.dll
bin\japi.dll
bin\ongameload.dll
bin\exehack.exe
startwar3.bat
lua5.1.dll
findpath.lua
ongameload.lua
war3.lua

Его прилагаю к сему сообщению

4.2 Код базовой библиотеки для Delphi


Благодаря AiwaDoter, теперь доступен код базовой библиотеки для добавления native функций для Delphi. Вот он:
Код:
unit jAPIDLL;
{ library by AiwaDoter }

{$DEFINE DYNAMIC}  { закомментируйте эту директиву, чтобы реализовать статический импорт если он нужен }

interface

{$IFDEF DYNAMIC}
  { Объявления процедур
  для динамического импорта }
  procedure jStartAPI;
{$ELSE}
  { Объявления процедур для статического импорта }

{$ENDIF}

implementation

{$IFDEF DYNAMIC}
uses Windows; { библиотека Windows }

type

  { создание нестандартных  типов и процедур библиотеки jAPI }
  jString = longint;
  jInt    = longint;
  jReal   = longint;

  AddNative   = procedure(routine: Pointer; name: PChar; prototype: PChar); cdecl;
  StrMap       = function(const str: PChar): jString; cdecl;
  StrGet        = function(strid: jString): PChar; cdecl;

var
  { Логический номер модуля DLL }
  LibInstance : HMODULE;

  jAddNative: AddNative;
  jStrMap: StrMap;
  jStrGet: StrGet;

{ функция нативы которую хотим внедрить в процес }
function test: jString; stdcall;
begin
  Result:= jStrMap('ZOMG TEH FUNC! - DELPHI');
end;

procedure jStartAPI;
begin
  if(LibInstance = 0)then begin
    { если DLL еще не загружена,
    попытаемся загрузить }
    LibInstance := LoadLibrary('japi.dll');
    { Если LoadLibrary возвращает 0, произошла ошибка }
    if(LibInstance = 0)then begin
      MessageBox(0, 'Could not locate jAPI', 'Error',
        MB_ICONEXCLAMATION or MB_OK);
      exit;
    end;
    { DLL загружена, теперь попытаемся найти функции }
    jAddNative:= AddNative(GetProcAddress(LibInstance, 'jAddNative'));
    if(Not Assigned(jAddNative))then
    { Если GetProcAddress возвращает Nil, возникли проблемы }
      MessageBox(0, 'Could not find jAddNative', 'Error', MB_ICONEXCLAMATION or MB_OK);
      
    jStrMap:= StrMap(GetProcAddress(LibInstance, 'jStrMap'));
    if(Not Assigned(jStrMap))then
      MessageBox(0, 'Could not find jStrMap', 'Error', MB_ICONEXCLAMATION or MB_OK);
      
    jStrGet:= StrGet(GetProcAddress(LibInstance, 'jStrGet'));
    if(Not Assigned(jStrGet))then
      MessageBox(0, 'Could not find jStrGet', 'Error', MB_ICONEXCLAMATION or MB_OK);
  end;

  { здесь передаем адрес функции "test" задаем имя нативы и ее параметры }
  jAddNative(@test, 'test', '()S');
end;

initialization
  LibInstance := 0;
  jAddNative:=Nil;
  jStrMap:=Nil;
  jStrGet:=Nil;

finalization
  { Если DLL была загружена, ее обязательно нужно выгрузить }
  if(LibInstance<>0)then begin
    FreeLibrary(LibInstance);
    LibInstance:= 0;
  end;

end.
{$ELSE}
end.

{$ENDIF}


4.3 Избавление от придирок антивирусов


Используя небольшую хитрость, можно добиться отказа от использования exehack.exe, что положительно повлияет на весь комплект: именно этот файл антивирусы чаще всего считают вредным. Также использование этой хитрости позволяет запускать варкрафт с добавленными native функциями в linux (естественно, из-под wine).
Итак, порядок действий:
  1. Скачать PEditor http://www.softpedia.com/get/Programming/File-Editors/PEditor.shtml
  2. Скопировать папку bin/ нашей сборки в PEditor/bin
  3. Скопировать lua5.1.dll в PEditor/
  4. Сделать резервную копию war3.exe
  5. PEditor -> открыть war3.exe -> directory -> imports, щелчок правой кнопкой add import: dll name = bin/ongameload.dll, func name = DllMain
  6. Нажать +, нажать OK, выйти из PEditor
Дело сделано! Теперь можно заменить оригинальный war3.exe в папке игры (не забудьте сделать резервную копию!) и наслаждаться. Этот метод позволяет исключить из списка возимых за собой файлов exehack.exe, startwar3.bat, findpath.lua и war3.lua.

Важно! При выполнении пункта 5, PEditor может выдать ошибку плана "Сan't find the "DllMain" function in the dll". Тогда вам нужно открыть ongameload.dll в любом просмотрщике ресурсов (рекомендую CFF Explorer — кстати в нем можно и импорт прописывать, даже удобнее: на PEditor антивирусы все еще могут ругнуться), найти таблицу экспорта и в ней посмотреть, имя какой функции содержит что-то, намекающее на загрузку ^_^ (в моем случае это было _Z16LoadLibraryAWrapPKc@4) — её и использовать в этом шаге.


Часть 5: Примечания


Внимание! Некоторые антивирусы могут ругаться на grimoire, а конкретно на файлы bin\exehack.exe, bin\ongameload.dll и bin\japi.dll, определяя в них троянов.Никакой опасности для вашего компьютера эти файлы не несут! Дело в том, что в них используется потенциально подозрительный код (для динамического изменения памяти процесса игры), на который и реагируют антивирусы. Просто добавьте их в исключения вашего антивируса.
Прикрепленные файлы
Тип файла: rar grimpack.rar (250.7 Кбайт, 145 просмотров )
________________
Тот, кто знает, когда он может сражаться, а когда не может, будет победителем.

Отредактировано Van Damm, 16.02.2009 в 23:44.
Старый 01.11.2008, 15:03
ADOLF

offline
Опыт: 108,165
Активность: 4820
Участник проектов:
-Литература
-Rock'n'roll мертв
очень мило, ток надо проверить)

ADOLF добавил:
dword наше все (ну в 32), остальные типы от лукавого, мну это всегда бесит
Старый 01.11.2008, 16:41
PlayerDark
Coraline
offline
Опыт: 10,569
Активность: 0
Теперь вопрос, как все это будет работать на чужом компьютере ?
Старый 01.11.2008, 20:38
Toadcop

offline
Опыт: 53,756
Активность: 771
PlayerDark нужен лоудер и всё (набор файлов и запуск вара через спец ехе)... и что бы у него патчилась вся контора ясно что =)
Старый 01.11.2008, 20:59
Van Damm
wait... what?
offline
Опыт: 22,268
Активность: 0
PlayerDark того, что есть в прилагаемом архиве, вполне достаточно

Van Damm добавил:
ну еще моск дабы сделать так, чтоб игрок запускал карту именно лоадером )
________________
Сто раз сразиться и сто раз победить — это не лучшее из лучшего; лучшее из лучшего — покорить чужую армию, не сражаясь.
Старый 01.11.2008, 21:34
ScorpioT1000
Только вперёд!
offline
Опыт: отключен
это насилование варика =)
вармейкинг недостоин таких извратов, гораздо разумнее писать что-то более полезное
Старый 02.11.2008, 17:31
ADOLF

offline
Опыт: 108,165
Активность: 4820
Участник проектов:
-Литература
-Rock'n'roll мертв
смотря для чего, какой то мего мод как раз и может использовать что то подобное
________________
war3 modmaking - FAQ :: terrain FAQ :: jass FAQ :: modeling FAQ
Старый 02.11.2008, 19:54
NETRAT

offline
Опыт: 83,812
Активность: 0
Участник проектов:
-The Elder Scrolls
понятно, все взаимодействие возможно через приведение к типу HANDLE (jINT), значит теоретически возможно пристроить сишный класс матриц (который, пожалуй, все программисты пишут на 1 курсе ВУЗа) или класс рациональных чисел так что бы он мог создаваться из jass и делать все необходимые преобразования/подсчеты
меня интересует насколько реально добавление чистых классов (обьектов типа класс) в jass? то есть реально ли перенести интерфейсы с хандлей на типизированные ссылки (ссылки на обьекты типа класс, что впринципе одно и то же, но более юзабельно)
вполне очевидно, что скрипт работает через хандли, и это видно из строки return jStrMap(s)
________________
"Герои и инвентарь будет! Плясать есть от чего. А остальное наработками, наработками..." © Зевс
Старый 05.11.2008, 11:24
NETRAT

offline
Опыт: 83,812
Активность: 0
Участник проектов:
-The Elder Scrolls
о, блин, заработало!
________________
"Герои и инвентарь будет! Плясать есть от чего. А остальное наработками, наработками..." © Зевс
Старый 05.11.2008, 16:43
Van Damm
wait... what?
offline
Опыт: 22,268
Активность: 0
с почином вас =)
________________
Сто раз сразиться и сто раз победить — это не лучшее из лучшего; лучшее из лучшего — покорить чужую армию, не сражаясь.
Старый 05.11.2008, 16:53
NETRAT

offline
Опыт: 83,812
Активность: 0
Участник проектов:
-The Elder Scrolls
успешно добавил stl::map<int,int>
________________
"Когда уже трусы и носки в стиле XGM будут?" © Trok-Ferot
Старый 05.11.2008, 17:59
NETRAT

offline
Опыт: 83,812
Активность: 0
Участник проектов:
-The Elder Scrolls
провел познавательный тест на лимит операций
Код:
set udg_ittest = 0
    loop
        set udg_ittest = udg_ittest + 1
    endloop

В переменной 42856
Код:
local integer i = 0
    loop
        set udg_itest = i
        set i = i + 1
    endloop

В переменной 33332 (красивое число =)
Код:
local integer i = 0
    loop
        call xIIMapSet(1,i)
        set i = i + 1
    endloop

Внешний(DLL) обьект типа STL::map<int,int> 24999
Код:
local integer i = 0
    loop
        call xISMapSet(1,I2S(i))
        set i = i + 1
    endloop

Внешний(DLL) обьект типа STL::map<int,string> 21427 (в этом случае добавляется приведение типа ссылка jString к const char * и, понятное дело, строка копируется и полностью хранится в памяти отведенной внешней dll
Код:
local integer i = 0
    loop
        call StoreInteger(udg_gc, "test", "test", i)
        set i = i + 1
    endloop

В кеше 18749

NETRAT добавил:
Рофл
Код:
loop
       call test_inc()
    endloop
где test_inc - внешняя функция, увеличивающая внешнюю переменную, добивает ее до 150000

NETRAT добавил:
отсюда вывод - физику нужно писать внешнюю =)
________________
"Когда уже трусы и носки в стиле XGM будут?" © Trok-Ferot
Старый 06.11.2008, 14:27
Van Damm
wait... what?
offline
Опыт: 22,268
Активность: 0
я догадывался =) в принципе, если делать вообще_все по уму, то вар будет использоваться только для вывода картинки
Старый 06.11.2008, 14:55
NETRAT

offline
Опыт: 83,812
Активность: 0
Участник проектов:
-The Elder Scrolls
мелькнула мыслишка приспособить шахматный ai с интерфейсом варкрафта =)

NETRAT добавил:
варкрафт в качестве i/o
________________
"Когда уже трусы и носки в стиле XGM будут?" © Trok-Ferot
Старый 06.11.2008, 16:34
J
expert
offline
Опыт: 50,947
Активность: 0
может вам просто написать игру с нуля и использовать в ней ресурсы варика?
Старый 06.11.2008, 17:17
NETRAT

offline
Опыт: 83,812
Активность: 0
Участник проектов:
-The Elder Scrolls
эх, жаль не удается оверрайдить функции работы с кешем... видать japi на такое не способен...

Jon это не спортивно, где дух компетишна?!

рофл
по лимиту операций стандартный кеш не отличается от внешнего обьекта типа map<string,*> (подозреваю что hashmap будет то же самое)

NETRAT добавил:
почитал сишные файлеги гримойра и rtc... молодцы ребята
________________
"Герои и инвентарь будет! Плясать есть от чего. А остальное наработками, наработками..." © Зевс

Отредактировано NETRAT, 06.11.2008 в 17:37.
Старый 06.11.2008, 18:52
agentex

offline
Опыт: 34,381
Активность: 413
Цитата:
В кеше 18749

то есть как integer в кеше можно сохранить макс. 18749 число?
Старый 06.11.2008, 19:00
NETRAT

offline
Опыт: 83,812
Активность: 0
Участник проектов:
-The Elder Scrolls
нет, это значит что в одном потоке можно вызвать не более 18749 (+10 возможно) операций сохранения в кеш, после этого будет превышение лимита и поток принудительно завершится
________________
"Герои и инвентарь будет! Плясать есть от чего. А остальное наработками, наработками..." © Зевс
Старый 06.11.2008, 19:11
Toadcop

offline
Опыт: 53,756
Активность: 771
Цитата:
отсюда вывод - физику нужно писать внешнюю =)
это я где то год назад хотел СДЕЛАТЬ =) но чё то забил =( и вызывать в джассе тока действия апдейт с периодом. =)

ну реально что тормознутое это... функции, для щастя надо бы было напрямую в юнита писать X\Y фейсинг и высоту полёта ^_^
Старый 06.11.2008, 20:47
NETRAT

offline
Опыт: 83,812
Активность: 0
Участник проектов:
-The Elder Scrolls
угу, в системке обрабатывать и каждые 0.04 секунды перерисовывать
агейю туда впихнуть ^^
________________
"Когда уже трусы и носки в стиле XGM будут?" © Trok-Ferot
Старый 06.11.2008, 21:34

Опции темы Поиск в этой теме
Поиск в этой теме:

Расширенный поиск

Ваши права в разделе
Вы не можете создавать темы
Вы не можете отвечать на сообщения
Вы не можете прикреплять файлы
Вы можете скачивать файлы

BB-коды Вкл.
[IMG] код Вкл.
HTML код Выкл.
Быстрый переход



Часовой пояс GMT +3, время: 06:28.