Найти тему
Adr Asd

пишем трейнер на Delphi_11 часть 1

Оглавление

Привет. Долго сомневался в нужности такой статьи, хоть и подробного материала в сети довольно мало, учитывая сложность выше минимума - я не уверен что статья будет пользоваться вниманием. Впрочем посмотрим. В процессе, в сети нашёл видео уроки создания трейнера на visual_Studio C++ где кодер использует объектно ориентированный код, моё мнение - классы нужны в более-менее сложном проекте , каковым трейнер всё таки не является и добавлять ему сложности в виде объектов не вижу смысла. Вполне хватает линейного кода и всяких структур данных. Но тут дело вкуса. Мы же создадим класс только для работы с потоками.

Я далеко не спец и программирую для удовольствия, так что строго не судите, нет - я писал разные программки на разных "языЦах" - но вот трейнер впервые. Трейнер будет для SCUM сам не знаю почему ;-), - просто в это время я его скачал и изучал карту, ну и хотелось преимуществ в виде патронов, бессмертия и прочего. Впрочем игра может быть любая. На момент начала написания статьи, версия SCUM была v0.85, теперь - 20.10.2023 SCUM.v0.9.113.75065

Будьте вежливы и оставляйте пжлст. ссылку на материал при копипасте и т.д. Предложения о корректировках и критика приветствуются, постараюсь учесть.

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

Трейнер будет патчить исполняемый код целевого процесса (игры) в памяти. Мы пишем приложение X64 и откуда у вас Delphi я понятия не имею ;-) - может Delphi Community Edition ?

-------------------
Ссылки на части:
_часть 2 _часть 3
_получаем патч-байты для SCUM.v0.9.113.75065
-------------------

Примерный план:

1) В первой части набросаем форму с каким нибудь скином для красоты. Для этого используем компонент - alphaControls. Кому не нужно - может и без него, а остальные могут посмотреть статью по "растриаливанию" этого компонента, так как он триальный. Получим необходимые для работы привилегии. Напишем функцию поиска PID нужного процесса, а так же функции ожидания появления нужного нам процесса с помощью таймера в случае его (процесса) отсутствия. В целях интереса - будет реализована работа и без таймера, с кнопкой активировать. Так же, для мониторинга целевого процесса, напишем ожидание его завершения с помощью функции WaitForSingleObject запущенной в новом потоке. (А можно и без неё, используя всё тот же таймер для мониторинга наличия процесса). И обработаем активацию/деактивацию слайдеров.

2) Во второй части напишем функции доступа к коду процесса в памяти, получения данных о модулях подключённых к процессу, получим данные PE-заголовка главного модуля и вычислим размеры секции кода, так как патчить будем именно в ней. Для хранения разных данных будем использовать в основном записи (record). Так как трейнер производит изменение кода в памяти по каким то адресам - логично предположить что их надо от чего то отсчитывать. Мне понравилась идея поиска сигнатур кода по маске, это и будет реализовано. То есть, мы находим адрес уникальной сигнатуры (последовательность байт) и отсчитываем смещение для патча от неё, но можно и от начала секции.

3) В третьей части - будем менять права на секцию кода, будет реализована функция пропатчивания, запускаемая с помощью слайдеров на форме. Создадим структуру (запись - record) содержащую все данные для патчей, а так же структуру для сохранения оригинальных байт перед патчем. Выяснять что и где нужно патчить будем с помощью отладчика x64Dbg , Cheat Engine и других утилит по ходу. ( Art Money тоже подойдёт ;-)

В процессе в числе прочего, будут созданы функции конвертеры: массив байт в строку, из hex-строки в массив байт и возможно прочие (в первой версии трейнера - наколбасил приличное кол-во конвертеров). Используемые в основном для тестового вывода значений, адресов и прочего.

Так как материала получается довольно много - возможно частей будет больше. Приводимый код лучше изучать в notepad++.

4) В дополнительной части получим данные для патчей из SCUM.

Часть 1

Запускаем Delphi, создаём новый Windows VCL Application. Если нужно оформление - кидаем на форму AlphaTools->TsSkinManager - в котором прописываем в SkinDirectory путь к папке со скинами, а в SkinName выбираем тему оформления. А чтобы тема стала встроенной - правой мышкой по sSkinManager->Internal skins... и в окне скинов добавить нужную тему. Кидаем на форму таймер, ну и прочие нужные контролы.

У меня так:

-2

Чекбокс "без таймера" и кнопка "активировать" - были добавлены ради эксперимента, для работы без таймера.

Надписи на форме - TsLabelFX , слайдеры - TsSlider, статусбар -TsStatusBar. Изображение PNG - добавлено с помощью TsImage. А Memo - для вывода тестовой информации - потом его можно убрать.

Слайдеры я настроил так: галочка Reversed - True - чтоб положение вкл было в право, SkinData->CustomColor - True - чтоб установить свои цвета для положений вкл/выкл. Цвета например такие: Color -$00483A25 и ColorOn $007BD098 для вкл. Подгоняем цвета, ширину и высоту окна, ровняем надписи, компилируем и проверяем как выглядит. Если всё устраивает кодим дальше.

Начнём постепенно писать функции и пополнять их в процессе полезным кодом.

Для начала нам нужно получить Debug привилегии для возможности чтения/записи в памяти процессов, далее получить список работающих процессов и выбрать из них нужный. Отлаживать будем на процессе Notepad.exe , а после уже переключимся на игру.

Объявим константу gameName сразу после подключения модулей uses:

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, sSkinManager, Vcl.ExtCtrls, acImage, acPNG, Vcl.StdCtrls, sLabel, Vcl.ComCtrls, sStatusBar, sPanel, acSlider, tlhelp32, sButton, sCheckBox;

const
gameName = 'notepad.exe';

и создадим приватные глобальные переменные для pid и имени процесса игры в блоке private юнита:

private
//хранят: pid и имя - процесса игры
progID:DWORD;
progName:string;

Сразу инициализируем их в событии FormCreate главной формы. Выбираем в инспекторе объектов (Object Inspector) нашу форму (Form1) и на вкладке Events создаём событие OnCreate (FormCreate), где инициализируем наши переменные:

procedure TForm1.FormCreate(Sender: TObject);
begin
progName:=''; progID:=0;
end;

Я обычно отделяю свои функции от создаваемых средой - блоками комментариев, и пишу их внутри. Напишем функцию получения привилегий, сразу за строками implementation {$R *.dfm} :

implementation
{$R *.dfm}

//////////////// функции ////////////////

function SetPrivilege(prochwnd: HWND; privilegeName: string; enable: boolean): boolean;
var
tpPrev, tp : TTokenPrivileges;
token : THandle;
dwRetLen : DWord;
begin
result := False;
try
if prochwnd = 0 then OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, token)
else OpenProcessToken(prochwnd, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, token);

tp.PrivilegeCount := 1;
if LookupPrivilegeValue(nil, pchar(privilegeName), tp.Privileges[0].LUID) then
begin
if enable then
tp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED
else
tp.Privileges[0].Attributes := 0;

dwRetLen := 0;
result := AdjustTokenPrivileges(token, False, tp, SizeOf(tpPrev), tpPrev, dwRetLen);
end;

finally
try
CloseHandle(token);
except
end;
end;
end;


////////// конец функций ////////////////

и выполним функцию
SetPrivilege при запуске программы в FormCreate:

procedure TForm1.FormCreate(Sender: TObject);
begin
progName:=''; progID:=0;

if
SetPrivilege(0, 'SeDebugPrivilege', true) then sStatusBar1.SimpleText:='привелегии получены'
else sStatusBar1.SimpleText:='привелегии не получены';
end;

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

-3

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

Добавим переменную флаг активации трейнера к private глобальным переменным:

sButton1ActivateTrainer:Boolean; //флаг статуса для ручной активации трейнера без таймера

а в FormCreate добавим её инициализацию: sButton1ActivateTrainer:=false;

sCheckBox1 будет делать кнопку не активной/активной при выборе работы с таймером или без. Напишем код для чекбокса. В FormCreate добавляем:

if not sCheckBox1.Checked then begin
//Timer1.Enabled := True;
//таймер пока закомментируем
sButton1.Enabled:=false;
sCheckBox1.Caption:='таймер включён';
end else begin
sButton1.Enabled:=true;
sCheckBox1.Caption:='без таймера';
end;

Далее создаём событие чекбокса OnClick для активации/деактивации кнопки, в инспекторе объектов выбираем sCheckBox1 и на вкладке Events двойной щелчок в OnClick и в sCheckBox1Click пишем код:

procedure TForm1.sCheckBox1Click(Sender: TObject);
begin
if sCheckBox1.Checked then begin
sButton1.Enabled:=true;
//Timer1.Enabled := false;
//таймер пока закомментируем
sCheckBox1.Caption:='без таймера';
end else begin
sButton1.Enabled:=false;
//Timer1.Enabled := true;
//таймер пока закомментируем
sCheckBox1.Caption:='таймер включён';
end;
end;

Строки для таймера на будущее, пока их закомментируем. Компилируем проект и проверяем работу чекбокса.

Теперь реализуем код кнопки активации трейнера - Button1Click, где будут происходить подготовительные действия: поиск процесса игры, заполнение рабочих структур нужными данными и прочее. В инспекторе объектов создаём процедуру TForm1.sButton1Click нашей кнопки. И так как у нас есть флаг статуса для ручной активации трейнера - sButton1ActivateTrainer , создаём такой код:

procedure TForm1.sButton1Click(Sender: TObject);
begin

if not sButton1ActivateTrainer then begin
//активировать трейнер

sButton1.Caption:='деактивировать';
sButton1ActivateTrainer:=true;

end else begin
//деактивировать трейнер

sButton1.Caption:='активировать';
sButton1ActivateTrainer:=false;

end;

end
;

который будем пополнять кодом. Скомпилировали, проверили, колбасим дальше. Давайте ка докинем на форму Memo для проверки всяких переменных, данных и установим её ScrollBars в ssBoth для возможности прокрутки большого количества строк.

-4

Теперь напишем функцию поиска процесса игры, которая в положительном случае заполнит переменные progID и progName :

Добавляем в верху, в блок подключения модулей uses через запятую tlhelp32 , объявляем функцию в блоке type там где TForm1 = class(TForm) , function findProcessGame(prcssName: string):boolean; , добавим константу с именем искомого процесса gameName = 'notepad.exe' , добавим флаг processFindedFlag - отображающий статус нахождения процесса игры и глобальную переменную globali в блок public для визуализации работы таймера :

unit Unit1;
interface
uses
Winapi.Windows, ... ,
tlhelp32;
const
gameName = 'notepad.exe'; // 'SCUM.exe';
type
TForm1 = class(TForm)
sSkinManager1: TsSkinManager;
...
procedure sButton1Click(Sender: TObject);
function findProcessGame(prcssName: string):boolean;

private
//хранят: pid и имя - процесса игры
progID:DWORD;
progName:string;
//флаг статуса для ручной активации трейнера без таймера
sButton1ActivateTrainer:Boolean;
processFindedFlag:Boolean; //если флаг - true - процесс игры найден

public
globali: Integer; //счётчик для отладки

end;



Добавим инициализацию
processFindedFlag в FormCreate :

procedure TForm1.FormCreate(Sender: TObject);
begin
progName:=''; progID:=0;
sButton1ActivateTrainer:=false;
processFindedFlag:=False;
...

и пишем функцию поиска процесса ( внутри блока отделённого комментарием // функции // )

function TForm1.findProcessGame(prcssName: string):boolean;
var
hSnapShot: THandle; ProcInfo: TProcessEntry32; findFlag:Boolean;
prcssName2:string;
begin
findFlag:=False; hSnapShot:=0;
hSnapShot := CreateToolHelp32Snapshot(TH32CS_SNAPPROCESS, 0);
prcssName2:=LowerCase(prcssName);

if (hSnapShot <> THandle(-1)) then
begin
ProcInfo.dwSize := SizeOf(ProcInfo);
if (Process32First(hSnapshot, ProcInfo)) then
begin
if LowerCase(ProcInfo.szExeFile) = prcssName2 then
begin
//если нашли - заполним глобальные переменные искомого процесса
progName:=ProcInfo.szExeFile; progID:=ProcInfo.th32ProcessID;
if progID > 0 then begin
findFlag:=true;
CloseHandle(hSnapShot); hSnapShot:=0;
//Result:=true;
end;
end else begin
//если не найден на первом проходе - повторяем поиск
while (Process32Next(hSnapShot, ProcInfo)) do
if LowerCase(ProcInfo.szExeFile) = prcssName2 then
begin
//если нашли - заполним глобальные переменные искомого процесса
progName:=ProcInfo.szExeFile; progID:=ProcInfo.th32ProcessID;

Memo1.Lines.Add(LowerCase(ProcInfo.szExeFile) + ' || ' + prcssName2);
//для тестового вывода

if progID > 0 then begin
findFlag:=true;
CloseHandle(hSnapShot); hSnapShot:=0;
//Result:=true;
end;
end;
end;
end;
end;

if hSnapShot > 0 then CloseHandle(hSnapShot);
if findFlag then Result:=true else Result:=False;

end;

Реализуем вызов findProcessGame по клику на кнопке sButton1Click:

procedure TForm1.sButton1Click(Sender: TObject);
begin
if not sButton1ActivateTrainer then begin
//активировать трейнер
if findProcessGame(gameName) then begin
//заполняется progID
SCheckBox1.Enabled:=false;
sLabelFX1.Caption:='pid: ' + inttostr(progID);
sLabelFX2.Caption:='имя: '+ progName;
processFindedFlag:=true;
//установка флага наличия процесса
sStatusBar1.SimpleText:='готов к работе';
//активируем слайдеры
activDeactivSliders('X', clGrayText, True);

sButton1.Caption:='деактивировать';
sButton1ActivateTrainer:=true;

end else sStatusBar1.SimpleText:='процесс '+ gameName + ' не найден.';

end else begin
//деактивировать трейнер
SCheckBox1.Enabled:=true;
activDeactivSliders('', $483A25, False);
sStatusBar1.SimpleText:='процесс ' + gameName + ' не найден ';

if processFindedFlag then
//был найден до этого (сброс всех полей - заполненных ранее)
begin
progName:=''; progID:=0;
sLabelFX1.Caption:='pid:';
sLabelFX2.Caption:='имя:';

processFindedFlag:=false;
//сброс флага наличия процесса
end;

sButton1.Caption:='активировать';
sButton1ActivateTrainer:=false;
end;
end;

Компилируем и проверяем предварительно запустив notepad.exe. Запускаем Диспетчер задач и сравниваем PID процесса (ИД процесса) Блокнот (notepad.exe) в диспетчере и трейнере. Если столбец ИД процесса в диспетчере отсутствует, правой мышкой в области названий столбцов и поставить нужную галочку.

-5

Теперь реализуем работу с таймером, он будет запускать функцию поиска процесса при его отсутствии или пропаже. Как вы заметили что таймер у нас будет работать при деактивированном чекбоксе ( без таймера ). Для визуализации работы таймер я добавил глобальную переменную globali в раздел public нашего юнита, чтоб можно было видеть количество тактов таймера в любой функции. Установим таймеру свойства Enable в False, а interval тика таймера в 1000 ( 1 секунда ). У него всего одно событие OnTimer, создаём его:

procedure TForm1.Timer1Timer(Sender: TObject);
begin
inc(globali);
//+1 к переменной глобального счётчика ( декоративный )
if findProcessGame(gameName) then begin
//процесс игры найден , так же заполняет progID
if not processFindedFlag then
begin
if progID > 0 then begin

sLabelFX1.Caption:='pid: ' + inttostr(progID);
sLabelFX2.Caption:='имя: '+ progName;

//для кнопки
sButton1.Caption:='деактивировать';
sButton1ActivateTrainer:=true;

timer1.Interval:=5000;
//увеличим интервал до 5 секунд
processFindedFlag:=true;
//процесс игры найден
sStatusBar1.SimpleText:='готов к работе';
end;
end;

end else begin
//процесс игры не найдена

sStatusBar1.SimpleText:='процесс ' + gameName + ' не найден ';

//для кнопки
sButton1.Caption:='активировать';
sButton1ActivateTrainer:=false;

if processFindedFlag then
//был найден до этого
begin
progName:=''; progID:=0;
sLabelFX1.Caption:='pid:'; sLabelFX2.Caption:='имя:';
timer1.Interval:=2000;
//меняем интервал до 2 секунд

//тестовый вывод в Memo
Form1.Memo1.Lines.Add(' ***** искомый процесс не найден ***** ' );

processFindedFlag:=false;
//сброс флага наличия процесса
end;

//тестовый вывод в Memo
Memo1.Lines.Add(inttostr(globali) + ' ожидаем процесса игры..');

end;
end;

и раскомментируем в процедурах TForm1.FormCreate и TForm1.sCheckBox1Click строки, активации и деактивации таймера Timer1.Enabled := false; и Timer1.Enabled := true; Компилируем, смотрим в лог сообщений дельфы нет ли ошибок и запускаем. Так как таймер работает сразу после запуска ( при снятом чекбоксе ), нужно запустить notepad и найденный процесс отобразится в Memo. Если установить галку в чекбоксе - то таймер перестанет работать и сообщений в Memo не будет. При закрытии блокнота - таймер продолжит поиск нужного процесса. Ну и добавим событие FormClose где на всякий случай деактивируем таймер: ( возможно это и лишнее ;-)

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
timer1.Enabled:=False;
end;

Мне стало интересно реализовать мониторинг процесса с помощью функции WaitForSingleObject запущенной в отдельном потоке - ожидающей сигнала ( в нашем случае сигнал закрытия ) от наблюдаемого процесса. Таймер выключаем при появлении процесса, мониторинг происходит с помощью WaitForSingleObject , при исчезновении процесса снова включаем таймер. Кодим:

Для начала создадим флаг статуса потока ожидания flagExecuteThread в разделе private переменных нашего юнита:

flagExecuteThread: boolean; //флаг статуса потока ожидания

и инициализируем его в FormCreate flagExecuteThread:=false; , а использовать его мы будем в TForm1.Timer1Timer.

Теперь опишем класс потока для WaitForSingleObject, напишем его перед блоком var с объявлением формы var Form1: TForm1; , а сразу после Form1: TForm1; объявим переменную для этого класса - waitThread :

//класс отдельного потока для WaitForSingleObject
TWaitThread = class(TThread)
private
protected
procedure Execute; override;
end;


var
Form1: TForm1;
waitThread: TWaitThread;

не обращайте внимания на красное подчёркивание в procedure Execute; - мы его просто ещё не написали, о чём дэльфа и сигнализирует. Давайте напишем процедуру Execute для нашего класса потока TWaitThread:

///////////////////////////// TWaitThread ///////////////////////////
procedure TWaitThread.Execute;
var
retWait:Cardinal; prHandle: THandle;
begin
prHandle:=OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_ALL_ACCESS or PROCESS_VM_OPERATION, False, Form1.progID);
if prHandle > 0 then begin
retWait := WaitForSingleObject (prHandle, 1000);
Form1.Memo1.Lines.Add('поток запущен');
//создаём цикл для возможности прерывания WaitForSingleObject
while retWait > 0 do begin
retWait := WaitForSingleObject (prHandle, 2000);

Form1.Memo1.Lines.Add('_мониторим наличие процесса_');

if waitThread.Terminated then begin
break;
end;
end;
CloseHandle(prHandle);
if (retWait = 0) or (waitThread.Terminated) then begin
Form1.flagExecuteThread:=false;
if not Form1.sCheckBox1.Checked then begin
Form1.Timer1.Enabled:=true;
//запустим таймер
Form1.Memo1.Lines.Add(' таймер активирован.');
end;
Form1.Memo1.Lines.Add('поток ожидания остановлен');
Form1.Memo1.Lines.Add(' приложение ' + gameName +' закрыто.');
end;
end;
end;

///////////////// TWaitThread END////////////////////////////

я ограничил её комментарием для большей наглядности в коде - это на выбор. При исчезновении наблюдаемого процесса - WaitForSingleObject вернёт ноль, даже при падении или некорректном его прерывании и наш наблюдающий поток прекратит выполнение.

Напишем запуск нашего потока из procedure TForm1.Timer1Timer , добавляем код обработки потока в блок if progID > 0 then begin ... и его else ветку:

procedure TForm1.Timer1Timer(Sender: TObject);
begin
inc(globali);
//инкремент глобального счётчика

if findProcessGame(gameName) then begin
//процесс игры найден , так же заполняет progID
if not processFindedFlag then
begin
if progID > 0 then begin

sLabelFX1.Caption:='pid: ' + inttostr(progID);
sLabelFX2.Caption:='имя: '+ progName;

//для кнопки
sButton1.Caption:='деактивировать';
sButton1ActivateTrainer:=true;

timer1.Interval:=5000;
//увеличим интервал до 5 секунд
processFindedFlag:=true;
//процесс игры найден
sStatusBar1.SimpleText:='готов к работе';

//запускаем ожидание закрытия наблюдаемого процесса
if not flagExecuteThread then begin
waitThread := TWaitThread.Create(False);
waitThread.FreeOnTerminate:=true;
waitThread.Priority := tpLower;
flagExecuteThread:=true;
Memo1.Lines.Add('Запуск потока ожидания завершения ' + gameName);
//выключаем таймер за ненадобностью, так как процесс отслеживает WaitForSingleObject в функции Execute Класса TWaitThread
//обратно - включение происходит в функции Execute Класса TWaitThread после уничтожения потока
Timer1.Enabled:=false;
Memo1.Lines.Add('Таймер деактивирован');
end;

end;
end;

end else begin
//процесс игры не найдена

sStatusBar1.SimpleText:='процесс ' + gameName + ' не найден ';

//для кнопки
sButton1.Caption:='активировать';
sButton1ActivateTrainer:=false;

if processFindedFlag then
//был найден до этого
begin
progName:=''; progID:=0;
sLabelFX1.Caption:='pid:'; sLabelFX2.Caption:='имя:';
timer1.Interval:=2000;
//меняем интервал до 2 секунд
Form1.Memo1.Lines.Add(' ***** искомый процесс не найден ***** ' );
processFindedFlag:=false;
//сброс флага наличия процесса
end;

//сброс флага работы потока ожидания
if flagExecuteThread then begin
flagExecuteThread:=false;
Memo1.Lines.Add('Остановка потока ожидания завершения ' + gameName );
end;

//тестовый вывод в Memo
Memo1.Lines.Add(inttostr(globali) + ' ожидаем процесса игры..');

end;
end;

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

Теперь научим слайдеры активироваться/деактивироваться в зависимости от наличия нужного процесса и создадим одну функцию обрабатывающую клик на всех слайдерах кроме первого (sSlider1) , а первый слайдер будет включать/выключать все разом. Начнём с первого, создадим для sSlider1 событие OnSliderSChange и заполним кодом:

procedure TForm1.sSlider1SliderChange(Sender: TObject);
var
sldrClickName:TsSlider; flagOn:boolean;
begin
sldrClickName :=TsSlider(Sender); flagOn:=false;
if sldrClickName.Name = 'sSlider1' then begin
//sLabelFX3 - подпись к sSlider1 (у вас может быть другая)
if sldrClickName.SliderOn then begin
sLabelFX3.Caption:='выключить все';
flagOn:=true;
end else begin
sLabelFX3.Caption:='включить все';
flagOn:=false;
end;

if not flagOn = sSlider2.SliderOn then sSlider2.Click;
if not flagOn = sSlider3.SliderOn then sSlider3.Click;
if not flagOn = sSlider4.SliderOn then sSlider4.Click;
if not flagOn = sSlider5.SliderOn then sSlider5.Click;
if not flagOn = sSlider6.SliderOn then sSlider6.Click;
end;
end;

Теперь напишем процедуру активации/деактивации слайдеров, объявим её в блоке type ( там где TForm1 = class(TForm) ) нашего юнита :

procedure activDeactivSliders(sldrCaption:string; sldrColor:TColor; enabl:boolean);

и пишем реализацию:

procedure TForm1.activDeactivSliders(sldrCaption:string; sldrColor:TColor; enabl:boolean);
var i:Integer; tmpSldr:TsSlider;
begin
for i:=0 to Form1.ControlCount-1 do
begin
//ищем слайдеры на форме
if Form1.Controls[i] is TsSlider then begin
tmpSldr:=nil;
tmpSldr:=(Form1.Controls[i] as TsSlider);
tmpSldr.Color:= sldrColor;
//clGrayText;
tmpSldr.SliderCaptionOff:=sldrCaption;
tmpSldr.ShowCaption := enabl;
if not enabl then tmpSldr.SliderOn:= False;
//выключаем если disable
Form1.Controls[i].Enabled:=enabl;
// активация/деактивация
tmpSldr:=nil;
end;
end;
end;

а результат этой процедуры у нас зависит от наличия процесса игры, то есть в Timer1Timer, в блоке if progID > 0 then begin после строки sStatusBar1.SimpleText:='готов к работе'; пишем:

//активируем слайдеры
activDeactivSliders('X', clGrayText, True);

а в блоке else ( относящемуся к этому же if progID > 0 then ) - end else begin //не найдена ещё один вызов с деактивацией слайдеров (можно в начале блока):

//деактивируем слайдеры
activDeactivSliders('', $483A25, False); //$483A25 - цвет

Можно скомпилировать и проверить работу. При появлении процесса блокнота - слайдеры становятся активны и меняют цвет на посветлее, состояние ВКЛ отображается зелёным. Когда процесс блокнота пропадает - слайдеры выключаются и деактивируются.

Теперь сделаем то же для кнопки активировать в процедуре TForm1.sButton1Click в блок if findProcessGame(gameName) then begin после sStatusBar1.SimpleText:='готов к работе'; добавляем код:
//активируем слайдеры
activDeactivSliders('X', clGrayText, True);
а в его
else часть:
activDeactivSliders('', $483A25, False);
Собираем, проверяем, исправляем ;-)
Для первой части достаточно - вторая следует.
Полный код Unit1.pas для первой части.
Критика и поправки приветствуются. Всем удачи.

-6