Создание плагина к ТОМ 2 на Delphi
Вступление
Несмотря на то, что я очень редко работаю с dll и очень мало знаю про них, всё-же опишу как создать плагин к ТОМ 2 в Delphi. Вдруг кому-то пригодится.
Работать мы будем в Embarcadero RAD Studio XE и будем во всю использовать юникод. Если вы программируете на более ранней версии, в которой ещё нет поддержки Unicode (Delphi 7, например), то отписывайте ниже, попробуем разобраться.
Подготовка проекта
Для начала запустим Delphi и создадим новый проект dll:
Выбираем в меню File -> New -> Other...
там ищем Dynamic-Link Library, выбираем и жмём Ok.
Перед нами открывается заготовка dll проекта:
library TestPlugin; { Important note about DLL memory management: ShareMem must be the first unit in your library's USES clause AND your project's (select Project-View Source) USES clause if your DLL exports any procedures or functions that pass strings as parameters or function results. This applies to all strings passed to and from your DLL--even those that are nested in records and classes. ShareMem is the interface unit to the BORLNDMM.DLL shared memory manager, which must be deployed along with your DLL. To avoid using BORLNDMM.DLL, pass string information using PChar or ShortString parameters. } uses SysUtils, Classes; {$R *.res} begin end.
В комментарии нас пугают сложностями работы со строками и с прочими вкусностями Delphi в dll. Плюём на это и удаляем этот комментарий
Сразу сохраним проект и назовём его, например TestPlugin.
Все плагины ТОМа должны иметь двойное расширение ".plg.dll". Что-бы не париться и каждый раз после компиляции не добавлять эту приставку ".plg", лезем в Project -> Options..., открываем раздел "Application" и в поле "LIB Suffix:" пишем ".plg" (без кавычек, конечно). Жмём OK. Теперь при компиляции к dll перед расширением будет добавляться этот суффикс.
Точка входа
Т.к. в делфи не всё так просто с точкой входа в dll как в Си, нам придётся немного пошаманить. Если вы не будете использовать в плагине глобальные объекты, которые надо выгружать по завершении работы плагина, то можно просто писать код между begin и end., но мы пойдём по чуть более сложному пути и выловим эти точки входа и выгрузки dll.
В разделе uses объявим модуль Windows.
Добавим функцию DLLEntryPoint:
//============== Точка входа в DLL ============================================= procedure DLLEntryPoint(Process: Integer); begin case Process of DLL_PROCESS_ATTACH: // При запуске dll begin // Здесь будет коннект к tom.dll и регистрация функций end; DLL_PROCESS_DETACH: // При выгрузке dll begin // Здесь выгружаем все глобальные объекты, которые насоздавали end; end; end;
Между основными begin ... end. напишем следующее:
begin DLLProc := @DLLEntryPoint; DLLEntryPoint(DLL_PROCESS_ATTACH); end.
Функции плагина
Для примера, наш плагин будет регистрировать две функции: getTime/время и getDate/дата, которые будут возвращать время и дату в виде строки. Понимаю, что такой плагин бесполезен, но для примера сойдёт.
Небольшая проблема кроется в том, что ТОМ не выгружает строки за нами. Мы должны делать это сами, причём после того как он заберёт результат этой функции. Чтобы не заморачиваться с этим, создадим глобальную переменную:
var ssStr: array of Char;
И сделаем функцию преобразования:
//============== Задание строки для вывода ===================================== procedure SetStr(const s: string); var i: Integer; begin SetLength(ssStr, Length(s) + 1); for i := 1 to Length(s) do ssStr[i-1] := s[i]; ssStr[Length(ssStr)-1] := #0; end;
Криво, но работает.
Теперь настало время описать наши две функции:
//================= Получение времени ========================================== function FnGetTime(const Login: PChar; const Text: PChar; const Dop: PChar): PChar; stdcall; begin SetStr(TimeToStr(Time)); Result := PChar(ssStr); end; //================= Получение даты ============================================= function FnGetDate(const Login: PChar; const Text: PChar; const Dop: PChar): PChar; stdcall; begin SetStr(DateToStr(Date)); Result := PChar(ssStr); end;
Все функции, которые надо подключать к ТОМу, надо объявлять именно так:
(const Login: PChar; const Text: PChar; const Dop: PChar): PChar; stdcall;
Подключение и регистрация функций
Итак, сначала объявим типы и константы, которые понадобятся для написания плагина:
const // Типы функций FnInput = 1; FnOutput = 2; FnOther = 3; FnCommand = 4; // функция доступна игроку из командной строки в игровом режиме type // Функция плагина // Login - ID сессии, которая вызывает функцию // Text - строковый аргумент функции // Dop - дополнительный строковый аргумент wcsFn = function (const Login: PChar; const Text: PChar; const Dop: PChar): PChar; stdcall; // Функция регистрации функции плагина // Name - имя функции // Func - указатель на функцию // Tp - тип регистрируемой функции // CntPar - Кол-во аргументов регистрируемой функции (от 0 до 2) wcsRegFn = function (const Name: PChar; Func: wcsFn; Tp: Integer; CntArg: Integer): Integer; stdcall;
Приведу описание типов функций:
FnOutput - функция что-нибудь выводит. Печатает текст на экран, воспроизводит звук, читает текст, рисует картинки и т.д.
При использовании эти функции не вызываются сразу, а накапливаются в пул. Это нужно для сохранения последовательности вывода текста, музыки, звуков и т.д. А также для пост-обработки накопленного выводимого текста целиком, а не обрывками.
FnInput - функция что-нибудь запрашивает. Например ввод командной строки или ответ на вопрос.
Перед выполнением такой функции сначала прокручивается и очищается пул функций FnOutput, а затем вызывается сама функция ввода.
FnOther - функция выполняется незаметно для пользователя. Ничего не выводит и ничего не просит. Выполняется сразу.
FnCommand - это тип функций, доступных игроку из командной строки в игровом режиме. Все остальные недоступны.
Функции хорошо прокомментированы, так-что объяснять не буду. То что не понятно, мы не будем использовать.
Директива stdcall нужна для совместимости dll с программами на Си, так-что не забываем.
Теперь поймаем tom.dll за шкирку и втюрим ему наши функции Для этого изменим код функции DLLEntryPoint:
//============== Точка входа в DLL ============================================= procedure DLLEntryPoint(Process: Integer); var EngineDll: THandle; RegFunc: wcsRegFn; begin case Process of DLL_PROCESS_ATTACH: // При запуске dll begin //получаем функции движка EngineDll := GetModuleHandle(PChar('tom.dll')); //движок уже загружен if EngineDll = 0 then Exit; @RegFunc := GetProcAddress(EngineDll, 'RegFunction'); if Assigned(@RegFunc) then begin RegFunc('getTime', FnGetTime, FnCommand, 0); RegFunc('время', FnGetTime, FnCommand, 0); RegFunc('getDate', FnGetDate, FnCommand, 0); RegFunc('дата', FnGetDate, FnCommand, 0); end; end; DLL_PROCESS_DETACH: // При выгрузке dll begin // Здесь выгружаем все глобальные объекты, которые насоздавали end; end; end;
Ну вот и всё, плагин готов. Компилируем, копируем в папку с ТОМом и проверяем из командной строки:
> время
14: 17: 56
> дата
24. 06. 2011
Заключение
Думаю, этих набросков хватит, чтобы разобраться с написанием плагинов к ТОМ 2 на Delphi.
Стоит отметить, что dll на делфи получаются очень пухлыми. Модуль SysUtils прибавляет к dll лишних 57 Кб, а модуль Classes и вовсе 223-275 Кб. Так-что используйте их только в случае крайней необходимости.
Полный исходник плагина:
library TestPlugin; uses Windows, SysUtils{, Classes}; {$R *.res} {$LIBSUFFIX '.plg'} const // Типы функций FnInput = 1; FnOutput = 2; FnOther = 3; FnCommand = 4; // функция доступна игроку из командной строки в игровом режиме type // Функция плагина // Login - ID сессии, которая вызывает функцию // Text - строковый аргумент функции // Dop - дополнительный строковый аргумент wcsFn = function (const Login: PChar; const Text: PChar; const Dop: PChar): PChar; stdcall; // Функция регистрации функции плагина // Name - имя функции // Func - указатель на функцию // Tp - тип регистрируемой функции // CntPar - Кол-во аргументов регистрируемой функции (от 0 до 2) wcsRegFn = function (const Name: PChar; Func: wcsFn; Tp: Integer; CntArg: Integer): Integer; stdcall; var ssStr: array of Char; //============== Задание строки для вывода ===================================== procedure SetStr(const s: string); var i: Integer; begin SetLength(ssStr, Length(s) + 1); for i := 1 to Length(s) do ssStr[i-1] := s[i]; ssStr[Length(ssStr)-1] := #0; end; //================= Получение времени ========================================== function FnGetTime(const Login: PChar; const Text: PChar; const Dop: PChar): PChar; stdcall; begin SetStr(TimeToStr(Time)); Result := PChar(ssStr); end; //================= Получение даты ============================================= function FnGetDate(const Login: PChar; const Text: PChar; const Dop: PChar): PChar; stdcall; begin SetStr(DateToStr(Date)); Result := PChar(ssStr); end; //============== Точка входа в DLL ============================================= procedure DLLEntryPoint(Process: Integer); var EngineDll: THandle; RegFunc: wcsRegFn; begin case Process of DLL_PROCESS_ATTACH: // При запуске dll begin //получаем функции движка EngineDll := GetModuleHandle(PChar('tom.dll')); //движок уже загружен if EngineDll = 0 then Exit; @RegFunc := GetProcAddress(EngineDll, 'RegFunction'); if Assigned(@RegFunc) then begin RegFunc('getTime', FnGetTime, FnCommand, 0); RegFunc('время', FnGetTime, FnCommand, 0); RegFunc('getDate', FnGetDate, FnCommand, 0); RegFunc('дата', FnGetDate, FnCommand, 0); end; end; DLL_PROCESS_DETACH: // При выгрузке dll begin // Здесь выгружаем все глобальные объекты, которые насоздавали end; end; end; begin DLLProc := @DLLEntryPoint; DLLEntryPoint(DLL_PROCESS_ATTACH); end.