Visual Studio 2008 и sproxy.exe

Понедельник, 21 Апрель, 2008

Проблема
There is no sproxy.exe in the visual studio folder “Program Files\Microsoft Visual Studio 9.0″ but there was in VS 2005 (C:\Program Files\Microsoft Visual Studio 8\VC\bin\sproxy.exe).

Решение
The ATL Server codebase is released as a shared source project on CodePlex and not installed as part of Visual Studio. As a tool supporting ATL Server project, sproxy.exe is not installed as part of Visual Studio too.

1. Скопировать себе архив с заголовочными файлами ATL Server.
2. Распаковать в произвольном месте.
3. Скопировать все файлы (подкаталоги необязательно? я не копировал) из получившегося каталога include в каталог заголовочных файлов “студии”: …\Microsoft Visual Studio 9.0\VC\atlmfc\include

Веб-сервис и приложение на C++

Пятница, 11 Январь, 2008

В моем случае, приложение написано с использованием MFC, web-service — на .NET C#.

disco.exe http://myserver:8080/WebService/MyWebService.asmx
rem след. строка для локального дебага
rem disco.exe http://localhost:1196/WebService/MyWebService.asmx
sproxy.exe results.discomap

На выходе генерируется “MyWebService.h”, который содержит все необходимое для работы с веб-сервисом “MyWebService”.

Черновой расчет загруженности процессора

Вторник, 8 Январь, 2008

CPU Load, CPU Usage, CPU Performance, загрузка процессора, да еще бы в процентах… Под Win9x-семейством — эти данные брались из ветки реестра HKEY_DYN, но с пришествием эры WinNT (2k, XP) что-то изменилось. Про Vista не буду, нет желания ее трогать.

Позавчера мне был нужен расчет загруженности процессора (для индикатора в ScreenDUO) и я его сделал. Вчерновую. Так как же оно работает?

Пробегаю по списку процессов (CreateToolhelp32Snapshot), узнаю сколько времени каждый процесс провел в kernel mode и сколько в user mode (GetProcessTimes), суммирую.

Вычитаю полученную сумму из предыдущего значения суммы времени. Беру время, прошедшее с момента получения последнего результата (таймер — вещь неточная, но мне хватает), перевожу его в наносекунды. Нахожу процентное соотношение времени, прошедшего с момента последнего тика таймера, и суммой времени, в течении которой процессы были заняты. Готово.

Метод неточный, но результаты выдает похожие на значение системного монитора. Работает под WinXP SP2, но непонятно, что будет выдавать на многоядерниках и при мультитрединге. Как обстоят дела с Win2k и Vista — я не знаю, под Win9x работать не будет.

#include “Tlhelp32.h”

const int updInterval = 10000; // msecs

double
 oldms = 0;
int GetDraftCpuUsage()
{
   
int n = 1;
   
double msecs = 0;
   HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
   
if (snap)      
   {
       
// walk through processes
       PROCESSENTRY32 pe;
       pe.dwSize =
sizeof(pe);
       Process32First(snap, &pe);
       
do 
       {
           FILETIME creation, exit, kernel, user;
           HANDLE ph = OpenProcess(PROCESS_QUERY_INFORMATION,
               
false, pe.th32ProcessID);
           
int r = GetProcessTimes(ph, &creation, &exit,
               &kernel, &user);
           CloseHandle(ph);

           if (r != 0)
           {
               
double tmp;

               tmp = kernel.dwLowDateTime;
               tmp += kernel.dwHighDateTime * (1 << 32);
               msecs += tmp;
               tmp = user.dwLowDateTime;
               tmp += user.dwHighDateTime * (1 << 32);    
               msecs += tmp;
           }

           n++;

       } while (Process32Next(snap, &pe) == TRUE);
   }

   if (!oldms)
       oldms = msecs;
   
   
int cpuUsage = (msecs - oldms) / (updInterval * 10000) * 100;
   oldms = msecs;
   
if (cpuUsage < 0) cpuUsage = 0;
   
if (cpuUsage > 100) cpuUsage = 100;
   
return cpuUsage;
}

По теме
- Почему поток захватывает 100% CPU?

Shogo и mouse-руль aka wheel

Понедельник, 10 Декабрь, 2007

Вероятно последняя статья про madding Shogo, т.к. больше там делать нечего. Камрад BLiTZ попросил сделать очередную доработку Shogo — приделать(binding) колесо от мышки, чтобы “скролировать” между видами вооружения.

Опыт уже есть, куда лезть понятно — …\ClientShellDLL\MouseMenu.cpp. Первое, с чем я сталкиваюсь, это перечисление параметров и возможностей мышки:


#define ID_MOUSELOOK        0
#define ID_LOOKSPRING        1
#define ID_INVERTYAXIS        2
#define ID_SENSITIVITY        3
#define ID_INPUTRATE        4
#define ID_LEFTBUTTON        5
#define ID_RIGHTBUTTON        6
#define ID_MIDDLEBUTTON        7
#define ID_UPWHEEL            8
#define ID_DOWNWHEEL        9
#define ID_BACK                10

Добавляю ID_UPWHEEL и ID_DOWNWHEEL, делаю себе крестик в Notes, что нужно где-то использовать. Тут же наталкиваюсь на следующую порцию — это присвоение глобальным переменным в конструкторе. Разбавляю своим кодом:

CMouseMenu::CMouseMenu() : CBaseMenu()
{
   m_nSecondColumn = 0;

   m_nLeftButtonSelection = 0;
   m_nRightButtonSelection = 0;
   m_nMiddleButtonSelection = 0;
   m_nUpWheelSelection = 0;
   m_nDownWheelSelection = 0;
}

Очередная пометка насчет глобальных переменных (они, вероятно, где-то в .h файле). Не заморачиваясь иду по файлу сверху-вниз. Натыкаюсь на инициализатор и начинаю его подробный ревьюинг.

DBOOL CMouseMenu::Init (CClientDE* pClientDE, CRiotMenu* pRiotMenu, CBaseMenu* pParent, int nScreenWidth, int nScreenHeight)
{

Инициализация глобальных переменных кодом команды unassigned. Колесо будет работать как кнопка, поэтому основное внимание уделяю работе с кнопками мышки. Пытаюсь делать по аналогии.

   m_nLeftButtonSelection = NUM_COMMANDS - 1;        // unassigned
   m_nMiddleButtonSelection = NUM_COMMANDS - 1;
   m_nRightButtonSelection = NUM_COMMANDS - 1;
   m_nUpWheelSelection = NUM_COMMANDS - 1;
   m_nDownWheelSelection = NUM_COMMANDS - 1;

Просканировав дальнейший кусок кода, выясняю, что здесь происходит загрузка байндинга кнопок из конфига.

// get button object names
// load the current button bindings

Интерес представил дефайн CONTROLTYPE_BUTTON, идем посмотреть.

#define CONTROLTYPE_BUTTON        8            // a mouse button

А что это у нас про wheel написано? Ага! Оказывается колёсико — это у нас ось-Z.

#define CONTROLTYPE_ZAXIS    3 // z-axis, such as a wheel on a mouse or a throttle on a joystick

Оформляю так:

       if (pObj->m_ObjectType == CONTROLTYPE_BUTTON)
       {
           
if (nButton == 0) SAFE_STRCPY(strLeftButton, pObj->m_ObjectName);
           
if (nButton == 1) SAFE_STRCPY(strRightButton, pObj->m_ObjectName);
           
if (nButton == 2) SAFE_STRCPY(strMiddleButton, pObj->m_ObjectName);

           nButton++;
       }

       if (pObj->m_ObjectType == CONTROLTYPE_ZAXIS)
       {
           SAFE_STRCPY(strWheel, pObj->m_ObjectName);
       }

Практически без затруднений пробежав по этому куску, я притормозил и глубоко задумался: колесо, как кнопка — одно, а действия у него два: вперед-назад. Перекур.

Камрад BLiTZ подкидывает одно из решений в FAQ’е по Shogo:

rangebind “##mouse” “Wheel” 10.000000 120.000000 “NextWeapon” -120.000000 -10.000000 “PrevWeapon”

…и многое проясняется. Одно устройство, одна кнопка. На кнопку (в данном случае — колесо) навешивается два экшена: один из которых сработает при отрицательной координате (колесо на себя), другой при положительной (колесо от себя). Я возвращаюсь немного обратно и разбавляю код загрузки настроек конфига следующим куском:

       if (stricmp (pBinding->strTriggerName, strWheel) == 0 && pBinding->pActionHead)
       {
           
if (pBinding->pActionHead->nRangeLow < 0)
               m_nDownWheelSelection = CommandToArrayPos (pBinding->pActionHead->nActionCode);
           
else
               m_nUpWheelSelection = CommandToArrayPos (pBinding->pActionHead->nActionCode);

           GameAction* pAction = pBinding->pActionHead->pNext;

           if (pAction)
           {
               
if (pBinding->pActionHead->nRangeLow < 0)
                   m_nDownWheelSelection = CommandToArrayPos (pAction->nActionCode);
               
else
                   m_nUpWheelSelection = CommandToArrayPos (pAction->nActionCode);
           }
       }

Первый if — проверка наличия экшена в биндинге + проверка наш ли это биндинг. Далее:
- Блок-if — забирает первый экшен.
- Next получает второй экшен, если он есть, иначе NULL.
- Проверяет, есть ли второй экшен.
- Забираем второй экшен.

Коды команд присваиваются глобальным переменным m_nDownWheelSelection и m_nUpWheelSelection.

Немного балую себя и добавляю строчку:

pClientDE->CPrint(“ZOOM MOD v1.0rc2 — http://www.nikvoronin.com/shogo/”);

…себя не похвалишь… Заодно, будет видно — отработал мод или нет.

Меню настроек мышки в Shogo отличается по принципу работы от меню настроек клавиатуры. Здесь экшен выбирается из списка клавишами влево-вправо, т.е. приходится перебирать несколько экшенов, пока не доберешься до нужного. Перехожу к потрошению методов CMouseMenu::Left() и CMouseMenu::Right().

По аналогии с обработкой кнопок мыши добавляю обработчик колесо-вверх, колесо-вниз. BindButtonToCommand заменяю на заглушку BindWheelToCommand.

else if (m_nSelection == ID_MIDDLEBUTTON)
{
ChangeButtonSettingSurface (m_nSelection, &m_nMiddleButtonSelection, -1);
BindButtonToCommand (2, m_nMiddleButtonSelection);
}
else if (m_nSelection == ID_UPWHEEL)
{
ChangeButtonSettingSurface (m_nSelection, &m_nUpWheelSelection, -1);
BindWheelToCommand();
}
else if (m_nSelection == ID_DOWNWHEEL)
{
ChangeButtonSettingSurface (m_nSelection, &m_nDownWheelSelection, -1);
BindWheelToCommand();
}

CMouseMenu::Right() отличается от Left только значением параметра при вызове ChangeButtonSettingSurface.

Далее идут методы отрисовки меню (Draw), загрузки сурфэйсов (LoadSurface). Доделываю по аналогии с кнопками мышки и тут хлоп… ай-я-яй, как нехорошо! Использовать магические числа в движке. В циклах загрузки/отрисовки пунктов меню используется число 9, равное числу настраиваемых параметров мышки. После моих инъекций их стало 11. Ревьюю код и ввожу дефайн (9-ка встречается далеко не в одном месте):

for (int i = m_nTopItem; i < NUM_MOUSE_ID; i++)
{
if (m_MouseSettings[i].hMenuItem)
{

Это были мелочи, а теперь… я добрался до самого главного — метода:

DBOOL CMouseMenu::BindButtonToCommand (int nButton, int nSelection)

Копи-пастом добавляю BindWheelToCommand и глубоко задумываюсь. При назначении кнопок все просто: мы добавляем один биндинг для одной кнопки, иначе говоря — один пункт меню, одна кнопка, один биндинг в котором один экшен. В случае с колесом ситуация иная — два пункта меню, одна кнопка (колесо), один биндинг в котором два экшена.

Биндинги назначаются через исполнение команды консоли: m_pClientDE->RunConsoleString (str);. В случае с колесом, я не смогу сформировать строку консоли (следовательно, назначить биндинг) пока пользователь не задаст оба пункта меню. Хотя, с другой стороны, пользователь может захотеть забайндить только одно направление колеса мышки…

Несмотря на мутноватые размышление, в голове понемногу выстраивается последовательность вызовов и назначений. Я начинаю кодить.

Кажется все так. Билд. Копирование DLL. Запуск. Странности с биндингом: настройки загружаются нормально, если поменять ручками в конфиге — все равно нормально, если менять из меню — на оба направления мышки биндится один и тот же экшен. Ревьюю код. Час, другой… Хочется курить и ругаться матом. Что, впрочем, и делаю.

Засаду устроил метод CommandName из ClientUtilities.cpp, объявив переменную buffer как static. Оставляю “как есть” и добавляю в код SAFE_STRCPY.

   // remove old binding and set new binding

   char str[128];
   sprintf (str,
“rangebind \”%s\” \”%s\” 0 0 \”\” 0 0 \”\”", pObj->m_DeviceName, pObj->m_ObjectName);
   m_pClientDE->RunConsoleString (str);

   // create then set new binding

   char strUp[128] = {0};
   
char strDown[128] = {0};

   if (m_nDownWheelSelection != NUM_COMMANDS - 1 &&
       m_nUpWheelSelection != NUM_COMMANDS - 1 )
   {
       
       SAFE_STRCPY(strUp, CommandName (g_CommandArray[m_nUpWheelSelection].nCommandID));
       SAFE_STRCPY(strDown, CommandName (g_CommandArray[m_nDownWheelSelection].nCommandID));
       sprintf (
               str,
               
“rangebind \”%s\” \”%s\” 10.000000 120.000000 \”%s\” -120.000000 -10.000000 \”%s\”",
               pObj->m_DeviceName,
               pObj->m_ObjectName,
               strUp,
               strDown
               );
   }
   
else
       if (m_nDownWheelSelection != NUM_COMMANDS - 1)
       {
           SAFE_STRCPY(strDown, CommandName (g_CommandArray[m_nDownWheelSelection].nCommandID));
           sprintf (
                   str,
                   
“rangebind \”%s\” \”%s\” -120.000000 -10.000000 \”%s\”",
                   pObj->m_DeviceName,
                   pObj->m_ObjectName,
                   strDown
                   );
       }
       
else
           if (m_nUpWheelSelection != NUM_COMMANDS - 1)
           {
               SAFE_STRCPY(strUp, CommandName (g_CommandArray[m_nUpWheelSelection].nCommandID));
               sprintf (
                       str,
                       
“rangebind \”%s\” \”%s\” 120.000000 10.000000 \”%s\”",
                       pObj->m_DeviceName,
                       pObj->m_ObjectName,
                       strUp
                       );
           }

   m_pClientDE->RunConsoleString (str);

Очередная сборка. Копирование. Запуск. Работает.

Я доволен, камрад BLiTZ в аналогичном состоянии, идем курить.

Исходники Shogo Zoom RC2 выложены на Shogo Madding.

По теме
- Исходники Shogo Zoom
- Включаем zoom в Shogo: Mobile Armor Division
- Shogo Madding

Включаем zoom в Shogo: Mobile Armor Division

Четверг, 29 Ноябрь, 2007

Начало

Игра “Shogo: Mobile Armor Division”, она же — “Ярость. Восстание на Кронусе”. В Shogo существует два вида оружия, поддерживающие режим оптического прицела (zoom). Он включается повторным выбором того же оружия. Такое управление неудобно. Попробую сделать zoom по нажатию клавиши заданной из настроек клавиатуры (или мыши) игрового меню Shogo. Беру исходники и…


Сборка

Чтение readme.txt, направило в каталог “…\ClientShellDLL” который содержит клиентскую часть Shogo. Нужно заметить, что исходники построены на C++ в лучших традициях ООП. Открываю Файл проекта (.dsp). VS 2005 любезно конвертирует его в свой новый формат. Выбираю Release. Нажимаю Ctrl+Shift+B (Build Solution) и… получаю около 1500 варнингов и около 400 ошибок. WTF?

Ошибки в исходниках, попробую править. Оказалось, что не все так страшно: разработчики забыли о типе переменных при объявлении.

for ( i=0; i < nVectorsPerShot; i++)
{

…и в одном месте компилятор не понял как привести тип переменной. После правки, ClientShell благополучно собрался. Я получил на выходе (каталог “…\Shogo”) библиотеку CShell.dll, исходники которой будут в дальнейшем правиться.

We need weapon

Поиск по содержимому исходников на предмет наличия слова “weapon”. На глазок выделяю файл “…\ClientShellDLL\WeaponModel.cpp” и лезу в нутря. Оно!

// ———————————————————————– //
//
//ROUTINE:CWeaponModel::ChangeWeapon()
//
//PURPOSE:Change to a different weapon
//
// ———————————————————————– //

void CWeaponModel::ChangeWeapon(DBYTE nCommandId)
{

В методе CWeaponModel::ChangeWeapon() есть интересующий меня код:

if (nWeaponId == m_nWeaponId)
{
// Well, okay, some weapons have toggles if selected again…

if (CanWeaponZoom(nWeaponId))
{
bDeselectWeapon = DFALSE;
}
else
{
return;
}
}

// Handle deselection of current weapon…


if (bDeselectWeapon)
{
Deselect();

// Need to wait for deselection animation to finish…Save the
// new weapon id…

m_nRequestedWeaponId = nWeaponId;
}
else
{
HandleInternalWeaponChange(nWeaponId);
}

Обращаю внимание на nWeaponId и nCommandId! Похоже, что с помощью метода DBYTE nWeaponId = GetWeaponId(nCommandId, nPlayerMode); (его нет в листинге выше) происходит стыковка id команды и id оружия. Запомню на будущее.

Непосредственно в куске кода идет проверка поддержки оружием zoom. Если поддерживает, то сброс текущего оружия отменяется. Далее вызывается метод непосредственно выбора оружия HandleInternalWeaponChange(nWeaponId); Смотрю этот метод.

// ———————————————————————– //
//
//    ROUTINE:    CWeaponModel::HandleInternalWeaponChange()
//
//    PURPOSE:    Change to a different weapon
//
// ———————————————————————– //

void CWeaponModel::HandleInternalWeaponChange(DBYTE nWeaponId)
{
   
if (!g_pRiotClientShell) return;
   
if (g_pRiotClientShell->IsPlayerDead() || g_pRiotClientShell->IsSpectatorMode()) return;

   // Check to see if we are already on this weapon…

   if (nWeaponId == m_nWeaponId)
   {
       
// If the weapon can be zoomed, toggle the zoom modes…

       if (CanWeaponZoom(nWeaponId))
       {
           
if (g_pRiotClientShell->IsChaseView())
           {
               m_bZoomView = DFALSE;
           }
           
else
           {
               m_bZoomView = !m_bZoomView;
           }
       }
       
else
       {
           
return;
       }
   }
   
else
   {
       m_bZoomView = DFALSE;
   }

   // Change to the weapon…

   DoWeaponChange(nWeaponId);
}

Вижу следующую цепочку проверок:
- Если оружие меняется на то же самое (m_nWeaponId - хранит id текущего оружия), то…
- Поддерживает ли оружие zoom? Если да, то…
- Меняет состояние режима zoom на противоположное: m_bZoomView = !m_bZoomView;
- Переключает оружие: DoWeaponChange(nWeaponId);

Как переключается zoom стало понятно. Теперь сделаю свой собственные метод. За основу возьму CWeaponModel::HandleInternalWeaponChange(DBYTE nWeaponId).

Zoom будет включаться на текущем оружии, поэтому стыковать nWeaponId и nCommandId не нужно. Id текущего оружия возьму из поля m_nWeaponId. Получилось следующее:

// ———————————————————————– //
//
//    ROUTINE:    CWeaponModel::ToggleZoom()
//
//    PURPOSE:    Toggle zoom on a current weapon
//
// ———————————————————————– //

void CWeaponModel::ToggleZoom()
{
   
if (!g_pRiotClientShell) return;

   if (CanWeaponZoom(m_nWeaponId))
   {
       
if (g_pRiotClientShell->IsChaseView())
       {
           m_bZoomView = DFALSE;
       }
       
else
       {
           m_bZoomView = !m_bZoomView;
       }
   }
   
else
   {
       
return;
   }
   
   DoWeaponChange(m_nWeaponId);
}

- Проверяю оружие на способность к zoom’у.
- Проверяю поддерживает ли камера zoom.
- Меняю состояние zoom на противоположное: m_bZoomView = !m_bZoomView;
- Переключаю оружие: DoWeaponChange(nWeaponId);

Пришло время вызвать метод.

Система команд

Кто-то вызывает метод CWeaponModel::ChangeWeapon(DBYTE nCommandId) и передает некую команду nCommandId. Ищу с помощью “Find All References”. Вижу, что есть еще один файл в котором происходит вызов: “…\ClientShellDLL\RiotClientShell.cpp”.

case COMMAND_ID_WEAPON_10 :
{
   m_weaponModel.ChangeWeapon(command);
}
break;

Сделаю по аналогии:

case COMMAND_ID_TOGGLE_ZOOM :
{
   m_weaponModel.ToggleZoom();
}
break;

Теперь метод вызывается, но у меня на руках никому неизвестный COMMAND_ID_TOGGLE_ZOOM. Поищу-ка, где объявляется COMMAND_ID_WEAPON_10 и добавлю туда же. “Find All References” для COMMAND_ID_WEAPON_10 и нахожу объявление в “…\shared\riotcommandids.h” Выбираю id из незанятых номеров и делаю по аналогии.

// Zoom extension…

#define COMMAND_ID_TOGGLE_ZOOM        80

Здесь я попадаю в небольшой тупик — непонятно что делать дальше: метод есть, id прописан, но о нем никто не знает. Попробую искать ссылки на другие COMMAND_ID_. Точно! Вот оно! Поиск по COMMAND_ID_FORWARD привел меня к файлу “…\ClientShellDLL\ClientUtilities.cpp”

Меню команд

В первую очередь обращаю внимание на NUM_COMMANDS из CommandID g_CommandArray[NUM_COMMANDS] = …. Число команд (NUM_COMMANDS) ограничено и прописано в заголовочном файле (.h). Добавлю сюда еще одну, следовательно наращиваем NUM_COMMANDS с 28 до 29. Добавляю новую команду в массив команд:

{ IDS_CONTROL_TOGGLEZOOM,COMMAND_ID_TOGGLE_ZOOM },
{ IDS_CONTROL_UNASSIGNED,COMMAND_ID_UNASSIGNED }// this control must always remain as the last one in the array

Обращаю внимание на комментарий “// this control must always remain as the last one in the array” и добавляю команду, перед COMMAND_ID_UNASSIGNED.

Я сделал нечто и получил на руки новый id IDS_CONTROL_TOGGLEZOOM. Припоминаю из своего опыта, что IDS_ означает строковый ресурс, который где-то должен быть прописан. Оставлю на потом, а пока продолжу исследовать ClientUtilities.cpp.

int CommandToArrayPos(int nCommand)

Добавлю сюда команду IDS_CONTROL_TOGGLEZOOM. Посчитав пальцем по экрану, получаю

case COMMAND_ID_TOGGLE_ZOOM:        return 27;
case COMMAND_ID_UNASSIGNED:            return 28;

Кстати, этот метод преобразует id команды в номер позиции массива команд. В дальнейшем, из массива можно будет достать связку id-команды — ids-ресурса-команды. Забегая вперед, ids-ресурса-команды — это скорее всего название команды.

И, похоже, последнее, что придется менять в этом файле:

//——————————————————————————————-
// CommandName
//
// Retrieves the command name from a command number
// Arguments:
//        nCommand - command number
// Return:
//        String containing the name of the action
//——————————————————————————————-
char* CommandName(int nCommand)

Теперь уже понятно, что здесь получение имени команды по ее id (это видно из комментария). Дорабатываю:

case COMMAND_ID_TOGGLE_ZOOM:            nStringID = IDS_ACTIONSTRING_TOGGLEZOOM;         break;
case COMMAND_ID_UNASSIGNED:                nStringID = IDS_ACTIONSTRING_UNASSIGNED;         break;

Получил еще один неоформленный id — IDS_ACTIONSTRING_UNASSIGNED. Разбираюсь с IDS_ ресурсами.

Ресурсы

Где-то на просторах readme.txt я приметил, что ресурсы вынесены в отдельный проект. Буду посмотреть “…\ClientRes\ClientRes.dsp”. Загружаю, смотрю в .rc файл. IDS_ обнаружены!

Добавляю по аналогии с другими командами.

IDS_ACTIONSTRING_TOGGLEZOOM = “Zoom”
IDS_CONTROL_TOGGLEZOOM = “zoom”

Запуск

Cобираю проект. Теперь у меня есть два DLL-файла в каталоге …\Shogo.

Создаю в каталоге Shogo дочерний каталог NiK. Переписываю полученные от сборки DLL’и. Запускаю лончер. В Shogo Advanced Launcher, Command-Line прописываю: -rez NiK. Запускаю Shogo.

В игровом меню настроек клавиатуры и мыши появился новый пункт Zoom. Ура! Играю! Zoom нихрена не работает. В консоле появляется надпись, что-то вроде “action is not binded”. WTF?

autoexec.cfg

В autoexec.cfg есть целый ряд команд AddAction. Может мне сюда? Попробую.

AddAction Zoom 80

Zoom - это название команды. 80 - это id команды (#define  COMMAND_ID_TOGGLE_ZOOM  80).

Повторный запуск, повторная настройка клавиатуры и мыши… барабанная дробь…

Работает!

Еще немного…

Я так и не понял причину, по которой мне пришлось прописывать AddAction в autoexec.cfg. То ли это баг, то ли так и должно быть, но если удалить из autoexec.cfg другие AddAction, то другие действия перестают работать. Неужели так задумано?

Исходники и библиотеки можно посмотреть/забрать в статье — “Исходники Shogo Zoom“.