Плагины

Генератор имеет свои плагины. Как правило, это готовый код, который нужно только использовать. Плагины доступны только для игры GTA San Andreas. Чтобы использовать их, нужно подключить пространство имён:

using GTA.SA.Plugins;

На текущий момент, доступны следующие плагины:

  1. Аудио-плеер
  2. Стартеры
  3. Видео-ролики
  4. Классы Sanny Builder

1. Аудио-плеер

Этот плагин позволит использовать проигрыватель аудио. Его цель в основном служит для диалогов. Но и для других целей его тоже можно использовать.

Плагин требует: 4 глобальных переменных, инициализацию только в потоках ( не в миссиях и не во внешних скриптах ). Пример использования плагина:

using GTA.SA.Plugins;

// --- --- ---

private static AudioPlayer audioPlayer; // здесь будем хранить ссылку на аудио-плеер

[Thread]
public void MAIN() {
    create_thread( AUDIO ); // запускаем поток с инициализацией плеера
    create_thread( TEST ); // использовать плагин нужно только после инициализации!
    end_thread();
}

[Thread]
public void AUDIO() {
    // в скобках перечислены индексы глобальных переменных:
    audioPlayer = new AudioPlayer( 2000, 2001, 2002, 2003 );
}

[Thread]
public void TEST() {

    // загружаем аудио-файлы. 1: с какого индекса начинать, 5: количество файлов
    audioPlayer.Load( 1, 5 ); // индекс должен быть больше нуля, а количество: меньше 26
	
    jf( 0, audioPlayer.IsReady ); // ожидаем загрузки всех файлов

    for( int i = 0; i < audioPlayer.Count; i++ ) {
        audioPlayer.PlayNext(); // воспроизводим следующий аудио-файл
		wait( audioPlayer.Lenght ); // ожидаем, пока звук закончит воспроизведение
    }
	
    audioPlayer.Unload(); // выгружаем все аудио-файлы
    end_thread();
}

Если проверить исходный код, то становится понятно, что все аудио-файлы должны находится в папке "Игра\sound". Расширение файла должно быть "mp3". А названия должны быть только числовыми. Метод загрузки получает первый индекс и количество файлов. Загрузка происходит в цикле, и в нашем примере будут загружены файлы с именами: 1, 2, 3, 4 и 5. Метод "PlayNext" сначала начнёт воспроизводить трек "1". При повторном вызове: "2", далее "3" и т.п. Всего можно загрузить не больше 25 звуков за раз. Если нужно больше, то загружать надо частями.

Вот так будет выглядеть наш код, прекрасно и кошмарно для новичков-скриптеров:

DEFINE OBJECTS 0

DEFINE MISSIONS 0

DEFINE EXTERNAL_SCRIPTS 0 // Use -1 in order not to compile AAA script

DEFINE UNKNOWN_EMPTY_SEGMENT 0

DEFINE UNKNOWN_THREADS_MEMORY 2048

//------------- MAIN ---------------

:MAIN
03A4: name_thread 'MAIN'
0180: set_on_mission_flag_to $409 // Note: your missions have to use the variable defined here
0004: $14 = 250 // $ = ? (int)
0111: set_wasted_busted_check 0
004F: create_thread @AUDIO
004F: create_thread @TEST
004E: end_thread

//------------- AUDIO ---------------

:AUDIO
03A4: name_thread 'AUDIO'
0006: 0@ = 0 // @ = ? (int)
0006: 31@ = 0 // @ = ? (int)

:AUDIO_AUTO_LABEL_0
0001: wait $14 ms
00D6: if
8038: not $2000 == 0 // $ == ? (int)
004D: jump_if_false @AUDIO_AUTO_LABEL_0

:AUDIO_LOOP_0
0001: wait 0 ms
0871: init_jump_table $2000 total_jumps 4 default_jump 0 @AUDIO_SWITCH_0_CASE_END jumps 0 @AUDIO_SWITCH_0_CASE_0 1 @AUDIO_SWITCH_0_CASE_1 2 @AUDIO_SWITCH_0_CASE_2 3 @AUDIO_SWITCH_0_CASE_3 -1 @AUDIO_SWITCH_0_CASE_END -1 @AUDIO_SWITCH_0_CASE_END -1 @AUDIO_SWITCH_0_CASE_END

:AUDIO_SWITCH_0_CASE_0
0002: jump @AUDIO_SWITCH_0_CASE_END

:AUDIO_SWITCH_0_CASE_1
int 31@
for 31@ = $2001 to $2002 step 1
    0AD3: 1@v = format "sound\%d.mp3" 31@
    00D6: if
    0AAB:     file_exists 1@v
    then
        00D6: if
        8AAC: not 5@(31@,25i) = load_audiostream 1@v
        then
        0006: 5@(31@,25i) = -1 // @ = ? (int)
        end
    else
    0006: 5@(31@,25i) = -1 // @ = ? (int)
    end
end
0004: $2000 = 2 // $ = ? (int)
0002: jump @AUDIO_SWITCH_0_CASE_END

:AUDIO_SWITCH_0_CASE_2
008B: 0@ = $2001 // @ = $ (int)
000E: 0@ -= 1 // @ -= ? (int)
0004: $2003 = 0 // $ = ? (int)
00D6: if
8039: not 0@ == -1 // @ == ? (int)
then
0AAD: set_mp3 5@(0@,25i) perform_action 0
end
00D6: if
8039: not 5@($2001,25i) == -1 // @ == ? (int)
then
0AAD: set_mp3 5@($2001,25i) perform_action 1
0AAF: $2003 = get_mp3_length 5@($2001,25i)
end
0002: jump @AUDIO_SWITCH_0_CASE_END

:AUDIO_SWITCH_0_CASE_3
for 31@ = 0 to 25
    00D6: if
    8039: not 5@(31@,25i) == -1 // @ == ? (int)
    then
    0AAD: set_mp3 5@(31@,25i) perform_action 0
    0AAE: release_mp3 5@(31@,25i)
    0006: 5@(31@,25i) = -1 // @ = ? (int)
    end
end

0004: $2002 = -1 // $ = ? (int)
0004: $2003 = 0 // $ = ? (int)
0004: $2000 = 0 // $ = ? (int)
0002: jump @AUDIO_SWITCH_0_CASE_END

:AUDIO_SWITCH_0_CASE_END
00D6: if
0038:     $2000 == 0 // $ == ? (int)
then
0002: jump @AUDIO
end
0002: jump @AUDIO_LOOP_0

//------------- TEST ---------------

:TEST
03A4: name_thread 'TEST'
0004: $2001 = 0 // $ = ? (int)
0004: $2002 = 6 // $ = ? (int)
0004: $2000 = 1 // $ = ? (int)

:TEST_AUTO_LABEL_0
0001: wait 0 ms
00D6: if
0038:     $2000 == 2 // $ == ? (int)
004D: jump_if_false @TEST_AUTO_LABEL_0

0008: $2001 += 1 // $ += ? (int)
0004: $2000 = 3 // $ = ? (int)
0001: wait $2003 ms

0008: $2001 += 1 // $ += ? (int)
0004: $2000 = 3 // $ = ? (int)
0001: wait $2003 ms

0008: $2001 += 1 // $ += ? (int)
0004: $2000 = 3 // $ = ? (int)
0001: wait $2003 ms

0008: $2001 += 1 // $ += ? (int)
0004: $2000 = 3 // $ = ? (int)
0001: wait $2003 ms

0008: $2001 += 1 // $ += ? (int)
0004: $2000 = 3 // $ = ? (int)
0001: wait $2003 ms

0004: $2000 = 4 // $ = ? (int)
004E: end_thread

В примере поток "AUDIO" берёт на себя всю логику. Нам остаётся только указывать что и когда загружать/воспроизводить. Изменяя ту или иную переменную, мы даём потоку знать что делать дальше. Вот такой нехитрый способ упростить себе жизнь с аудио-файлами.

2. Стартеры

Плагины имеют несколько классов для работы со стартерами. Ниже приведён их список с описаниями:

  1. Простой стартер
  2. Стартер с продвинутыми настройками
  3. Многофункциональный стартер

2.1. Простой стартер

Этот вариант наиболее прост в освоении и реализации, так как не имеет никаких настроек. Конструктор принимает 7 обязательных параметров, остальные можно добавить когда нужно. Обязательно нужно указывать ID иконки радара, позицию маркера и два индекса глобальных переменных. После этого идёт перечень миссий, которые будут запускаться в стартере. Нужно указать минимум одну миссию!

Стартер имеет два свойства и один метод. "TotalMission" возвращает количество миссий, с которыми работает стартер. "MissionPassed" возвращает переменную, которая хранит количество пройденных миссий ( иногда в других потоках нужно проверить пройден ли какой-то этап ). Есть ещё метод "Recreate". Он создаёт новый поток стартера ( использовать нужно в миссиях, в коде провала и победы ). Принимает только один параметр, который указывает нужно ли увеличивать счётчик миссий.

Ниже приведён код использования простого стартера:

using GTA.SA.Plugins;

// --- --- ---

[Thread]
public void MAIN() {
    create_thread( STARTER );
    end_thread();
}
        
private static StarterEasy CJ_STARTER; // ссылка на стартер

[Thread]
public void STARTER() {
    // создаём новый стартер
    CJ_STARTER = new StarterEasy( RadarIconID.CJ, 0.0, 1.0, 2.0, 2004, 2005, MISS1, MISS2 );
}

[Mission]
public void MISS1() {
    Mission.OnPassed = delegate { CJ_STARTER.Recreate( true ); };
    Mission.OnFailed = delegate { CJ_STARTER.Recreate(); };
}

[Mission]
public void MISS2() {
    Mission.OnPassed = delegate { CJ_STARTER.Recreate( true ); };
    Mission.OnFailed = delegate { CJ_STARTER.Recreate(); };
}

В итоге мы получим такой код:

DEFINE OBJECTS 0

DEFINE MISSIONS 2
DEFINE MISSION 0 AT @MISS1 // 0
DEFINE MISSION 1 AT @MISS2 // 1

DEFINE EXTERNAL_SCRIPTS 0 // Use -1 in order not to compile AAA script

DEFINE UNKNOWN_EMPTY_SEGMENT 0

DEFINE UNKNOWN_THREADS_MEMORY 2048

//------------- MAIN ---------------

:MAIN
03A4: name_thread 'MAIN'
0180: set_on_mission_flag_to $409 // Note: your missions have to use the variable defined here
0004: $14 = 250 // $ = ? (int)
0111: set_wasted_busted_check 0
004F: create_thread @STARTER
004E: end_thread

//------------- STARTER ---------------

:STARTER
03A4: name_thread 'STARTER'
00D6: if or
001A:     0 > $2005 // ? > $ (int)
0028:     $2005 >= 2 // $ >= ? (int)
then
0002: jump @STARTER_END
end
0111: set_wasted_busted_check 0
02A7: $2004 = create_icon_marker_and_sphere 15 at 0.0 1.0 2.0

:STARTER_AUTO_LABEL_0
0001: wait $14 ms
00D6: if
0038:     $409 == 0 // $ == ? (int)
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
0256:     player $2 defined
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
03EE:     player $2 controllable
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
0102:     actor $3 in_sphere 0.0 1.0 2.0 radius 1.5 1.5 1.5 sphere 0 stopped_on_foot
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
816B: not fading
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
8118: not actor $3 dead
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
8741: not actor $3 busted
004D: jump_if_false @STARTER_AUTO_LABEL_0
01F7: set_player $2 ignored_by_cops 1
03BF: set_player $2 ignored_by_everyone 1
02AB: set_actor $3 immunities BP 1 FP 1 EP 1 CP 1 MP 1
01B4: set_player $2 can_move 0
0164: disable_marker $2004
016A: fade 0 time 1000
00D6: if
0038:     $2005 == 0 // $ == ? (int)
then
0050: gosub @STARTER_0
end
00D6: if
0038:     $2005 == 1 // $ == ? (int)
then
0050: gosub @STARTER_1
end

:STARTER_END
004E: end_thread

:STARTER_0
0417: start_mission 0
0051: return

:STARTER_1
0417: start_mission 1
0051: return

//------------- Mission: MISS1 (0) ---------------

:MISS1
03A4: name_thread 'MISS1'
0050: gosub @MISS1_START
00D6: if
0112:     wasted_or_busted // mission only
004D: jump_if_false @MISS1_END
0050: gosub @MISS1_FAILED

:MISS1_END
0004: $409 = 0 // $ = ? (int)
00D8: mission_cleanup
004E: end_thread

:MISS1_START
0317: increment_mission_attempts
0004: $409 = 1 // $ = ? (int)

:MISS1_PASSED
0050: gosub @MISS1_CLEAR
0008: $2005 += 1 // $ += ? (int)
004F: create_thread @STARTER
0051: return

:MISS1_FAILED
0050: gosub @MISS1_CLEAR
004F: create_thread @STARTER
0051: return

:MISS1_CLEAR
0051: return

//------------- Mission: MISS2 (1) ---------------

:MISS2
03A4: name_thread 'MISS2'
0050: gosub @MISS2_START
00D6: if
0112:     wasted_or_busted // mission only
004D: jump_if_false @MISS2_END
0050: gosub @MISS2_FAILED

:MISS2_END
0004: $409 = 0 // $ = ? (int)
00D8: mission_cleanup
004E: end_thread

:MISS2_START
0317: increment_mission_attempts
0004: $409 = 1 // $ = ? (int)

:MISS2_PASSED
0050: gosub @MISS2_CLEAR
0008: $2005 += 1 // $ += ? (int)
004F: create_thread @STARTER
0051: return

:MISS2_FAILED
0050: gosub @MISS2_CLEAR
004F: create_thread @STARTER
0051: return

:MISS2_CLEAR
0051: return

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

2.2. Стартер с продвинутыми настройками

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

using GTA.SA.Plugins;

// --- --- ---

[Thread]
public void MAIN() {
    create_thread( STARTER );
    end_thread();
}
        
private static StarterMiddle CJ_STARTER;

[Thread]
public void STARTER() {
    CJ_STARTER = new StarterMiddle( RadarIconID.CJ, 0.0, 1.0, 2.0, 2004, 2005, 2006, 2007, 2008, 2009, 2010, MISS1, MISS2 );
}

[Mission]
public void MISS1() {
    Mission.OnPassed = delegate {
        CJ_STARTER.Move( 1440.0, 24.0, 13.0 ); // перемещение стартера
        CJ_STARTER.ChangeIcon( RadarIconID.SWEET ); // смена иконки стартера
        CJ_STARTER.Recreate( true );
    };
    Mission.OnFailed = delegate { CJ_STARTER.Recreate(); };
}

[Mission]
public void MISS2() {
    Mission.OnPassed = delegate { CJ_STARTER.Recreate( true ); };
    Mission.OnFailed = delegate { CJ_STARTER.Recreate(); };
}

Изменять позицию и иконку надо перед тем, как мы создадим стартер заново! Этот код будет иметь такой вид на выходе:

DEFINE OBJECTS 0

DEFINE MISSIONS 2
DEFINE MISSION 0 AT @MISS1 // 0
DEFINE MISSION 1 AT @MISS2 // 1

DEFINE EXTERNAL_SCRIPTS 0 // Use -1 in order not to compile AAA script

DEFINE UNKNOWN_EMPTY_SEGMENT 0

DEFINE UNKNOWN_THREADS_MEMORY 2048

//------------- MAIN ---------------

:MAIN
03A4: name_thread 'MAIN'
0180: set_on_mission_flag_to $409 // Note: your missions have to use the variable defined here
0004: $14 = 250 // $ = ? (int)
0111: set_wasted_busted_check 0
004F: create_thread @STARTER
004E: end_thread

//------------- STARTER ---------------

:STARTER
03A4: name_thread 'STARTER'
00D6: if or
001A:     0 > $2005 // ? > $ (int)
0028:     $2005 >= 2 // $ >= ? (int)
then
0002: jump @STARTER_END
end
0111: set_wasted_busted_check 0
00D6: if
0038:     $2010 == 0 // $ == ? (int)
then
0004: $2006 = 15 // $ = ? (int)
0005: $2007 = 0.0 // $ = ? (float)
0005: $2008 = 1.0 // $ = ? (float)
0005: $2009 = 2.0 // $ = ? (float)
0004: $2010 = 1 // $ = ? (int)
end
02A7: $2004 = create_icon_marker_and_sphere $2006 at $2007 $2008 $2009

:STARTER_AUTO_LABEL_0
0001: wait $14 ms
00D6: if
0038:     $409 == 0 // $ == ? (int)
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
0256:     player $2 defined
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
03EE:     player $2 controllable
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
0102:     actor $3 in_sphere $2007 $2008 $2009 radius 1.5 1.5 1.5 sphere 0 stopped_on_foot
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
816B: not fading
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
8118: not actor $3 dead
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
8741: not actor $3 busted
004D: jump_if_false @STARTER_AUTO_LABEL_0
01F7: set_player $2 ignored_by_cops 1
03BF: set_player $2 ignored_by_everyone 1
02AB: set_actor $3 immunities BP 1 FP 1 EP 1 CP 1 MP 1
01B4: set_player $2 can_move 0
0164: disable_marker $2004
016A: fade 0 time 1000
00D6: if
0038:     $2005 == 0 // $ == ? (int)
then
0050: gosub @STARTER_0
end
00D6: if
0038:     $2005 == 1 // $ == ? (int)
then
0050: gosub @STARTER_1
end

:STARTER_END
004E: end_thread

:STARTER_0
0417: start_mission 0
0051: return

:STARTER_1
0417: start_mission 1
0051: return

//------------- Mission: MISS1 (0) ---------------

:MISS1
03A4: name_thread 'MISS1'
0050: gosub @MISS1_START
00D6: if
0112:     wasted_or_busted // mission only
004D: jump_if_false @MISS1_END
0050: gosub @MISS1_FAILED

:MISS1_END
0004: $409 = 0 // $ = ? (int)
00D8: mission_cleanup
004E: end_thread

:MISS1_START
0317: increment_mission_attempts
0004: $409 = 1 // $ = ? (int)

:MISS1_PASSED
0050: gosub @MISS1_CLEAR
0005: $2007 = 1440.0 // $ = ? (float)
0005: $2008 = 24.0 // $ = ? (float)
0005: $2009 = 13.0 // $ = ? (float)
0004: $2006 = 38 // $ = ? (int)
0008: $2005 += 1 // $ += ? (int)
004F: create_thread @STARTER
0051: return

:MISS1_FAILED
0050: gosub @MISS1_CLEAR
004F: create_thread @STARTER
0051: return

:MISS1_CLEAR
0051: return

//------------- Mission: MISS2 (1) ---------------

:MISS2
03A4: name_thread 'MISS2'
0050: gosub @MISS2_START
00D6: if
0112:     wasted_or_busted // mission only
004D: jump_if_false @MISS2_END
0050: gosub @MISS2_FAILED

:MISS2_END
0004: $409 = 0 // $ = ? (int)
00D8: mission_cleanup
004E: end_thread

:MISS2_START
0317: increment_mission_attempts
0004: $409 = 1 // $ = ? (int)

:MISS2_PASSED
0050: gosub @MISS2_CLEAR
0008: $2005 += 1 // $ += ? (int)
004F: create_thread @STARTER
0051: return

:MISS2_FAILED
0050: gosub @MISS2_CLEAR
004F: create_thread @STARTER
0051: return

:MISS2_CLEAR
0051: return

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

2.3. Многофункциональный стартер

Этот тип стартера имеет немного другой принцип инициализации. Конструктор требует ещё 2 глобальные переменные, которые используются для радиуса сферы и хранения этапа миссии. Сами миссии пишутся в отдельном методе, а генерация кода происходит после использования метода "Write".

Миссии нужно добавлять отдельно, используя метод "AddMission". Туда нужно указать GXT-имя миссии, ссылку на миссию и, если нужно, указать дополнительные условия старта. Методы "SetStage" и "SetRadius" позволяют изменить этап миссии и радиус сферы соответсвенно. А функция "GetMissionGXTName" возвращает GXT-имя миссии, которое было указано в стратере.

Давайте рассмотрим пример реализации многофункционального стартера:

using GTA.SA.Plugins;

// --- --- ---

[Thread]
public void MAIN() {
    create_thread( STARTER );
    end_thread();
}
        
private static StarterHard CJ_STARTER; // сохраним ссылку на стартер

[Thread]
public void STARTER() {
    // сначала инициализируем стартер
    // далее добавив несколько миссий; в первой напишем ещё условий
    // потом генерируем код, используя метод "Write"
    CJ_STARTER = new StarterHard( RadarIconID.CJ, 144.0, -14.0, 13.4, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012 );
    CJ_STARTER.AddMission( "BASE_1", MISS1, PlayerActor.is_stopped(), PlayerActor.is_in_any_vehicle() );
    CJ_STARTER.AddMission( "BASE_2", MISS2 );
    CJ_STARTER.Write();
}

[Mission]
public void MISS1() {
    Mission.OnPassed = delegate {

        // вот так можно задать имя миссии в сейв-файле
		// нужно, что миссия MISS1 была добавлена в стартер
        set_latest_mission_passed( CJ_STARTER.GetMissionGXTName( MISS1 ) );

        var stage = CJ_STARTER.Stage; // получаем некий этап
        stage += 1; // изменяем этап

        // изменяем данные этапа, хотя в этом контексте писать его не нужно: он изменён выше в коде
        CJ_STARTER.SetStage( stage );

        and( CJ_STARTER.Stage > 2, delegate {
            CJ_STARTER.Move( 1440.0, 24.0, 13.0 );
            CJ_STARTER.ChangeIcon( RadarIconID.SWEET );
        } );

        CJ_STARTER.SetRadius( 6.0 ); // указываем новый радиус сферы стартера
        CJ_STARTER.Recreate( true );
    };
    Mission.OnFailed = delegate {
        CJ_STARTER.Recreate();
    };
}

[Mission]
public void MISS2() {
    Mission.OnPassed = delegate {
        CJ_STARTER.Recreate( true );
    };
    Mission.OnFailed = delegate {
        CJ_STARTER.Recreate();
    };
}

Этот небольшой код генерирует для Sanny Builder вот такой код:

DEFINE OBJECTS 0

DEFINE MISSIONS 2
DEFINE MISSION 0 AT @MISS1 // 0
DEFINE MISSION 1 AT @MISS2 // 1

DEFINE EXTERNAL_SCRIPTS 0 // Use -1 in order not to compile AAA script

DEFINE UNKNOWN_EMPTY_SEGMENT 0

DEFINE UNKNOWN_THREADS_MEMORY 2048

//------------- MAIN ---------------

:MAIN
03A4: name_thread 'MAIN'
0180: set_on_mission_flag_to $409 // Note: your missions have to use the variable defined here
0004: $14 = 250 // $ = ? (int)
0111: set_wasted_busted_check 0
004F: create_thread @STARTER
004E: end_thread

//------------- STARTER ---------------

:STARTER
03A4: name_thread 'STARTER'
00D6: if or
001A:     0 > $2005 // ? > $ (int)
0028:     $2005 >= 2 // $ >= ? (int)
then
0002: jump @STARTER_END
end
0111: set_wasted_busted_check 0
00D6: if
0038:     $2010 == 0 // $ == ? (int)
then
0004: $2006 = 15 // $ = ? (int)
0005: $2007 = 144.0 // $ = ? (float)
0005: $2008 = -14.0 // $ = ? (float)
0005: $2009 = 13.4 // $ = ? (float)
0005: $2012 = 1.5 // $ = ? (float)
0004: $2010 = 1 // $ = ? (int)
end
02A7: $2004 = create_icon_marker_and_sphere $2006 at $2007 $2008 $2009

:STARTER_CHECK
0001: wait $14 ms
00D6: if or
0038:     $409 == 1 // $ == ? (int)
016B:     fading
8256: not player $2 defined
then
0002: jump @STARTER_CHECK
end
00D6: if or
83EE: not player $2 controllable
0118:     actor $3 dead
0741:     actor $3 busted
then
0002: jump @STARTER_CHECK
end
00D6: if
00FE:     actor $3 sphere 0 in_sphere $2007 $2008 $2009 radius $2012 $2012 $2012
004D: jump_if_false @STARTER_CHECK
00D6: if
0038:     $2005 == 0 // $ == ? (int)
then
    00D6: if
    02A0:     actor $3 stopped
    004D: jump_if_false @STARTER_CHECK
    00D6: if
    00DF:     actor $3 driving
    004D: jump_if_false @STARTER_CHECK
0050: gosub @STARTER_0
0002: jump @STARTER_PLAY_MISSION
end
00D6: if
0038:     $2005 == 1 // $ == ? (int)
then
0050: gosub @STARTER_1
0002: jump @STARTER_PLAY_MISSION
end
0002: jump @STARTER_CHECK

:STARTER_PLAY_MISSION
0164: disable_marker $2004
01F7: set_player $2 ignored_by_cops 1
03BF: set_player $2 ignored_by_everyone 1
02AB: set_actor $3 immunities BP 1 FP 1 EP 1 CP 1 MP 1
01B4: set_player $2 can_move 0
016A: fade 0 time 1000

:STARTER_END
004E: end_thread

:STARTER_0
00BA: show_text_styled GXT 'BASE_1' time 1000 style 2
0417: start_mission 0
0051: return

:STARTER_1
00BA: show_text_styled GXT 'BASE_2' time 1000 style 2
0417: start_mission 1
0051: return

//------------- Mission: MISS1 (0) ---------------

:MISS1
03A4: name_thread 'MISS1'
0050: gosub @MISS1_START
00D6: if
0112:     wasted_or_busted // mission only
004D: jump_if_false @MISS1_END
0050: gosub @MISS1_FAILED

:MISS1_END
0004: $409 = 0 // $ = ? (int)
00D8: mission_cleanup
004E: end_thread

:MISS1_START
0317: increment_mission_attempts
0004: $409 = 1 // $ = ? (int)

:MISS1_PASSED
0050: gosub @MISS1_CLEAR
0318: set_latest_mission_passed 'BASE_1'
0008: $2011 += 1 // $ += ? (int)
0084: $2011 = $2011 // $ = $ (int)
00D6: if
0018:     $2011 > 2 // $ > ? (int)
then
0005: $2007 = 1440.0 // $ = ? (float)
0005: $2008 = 24.0 // $ = ? (float)
0005: $2009 = 13.0 // $ = ? (float)
0004: $2006 = 38 // $ = ? (int)
end
0005: $2012 = 6.0 // $ = ? (float)
0008: $2005 += 1 // $ += ? (int)
004F: create_thread @STARTER
0051: return

:MISS1_FAILED
0050: gosub @MISS1_CLEAR
004F: create_thread @STARTER
0051: return

:MISS1_CLEAR
0051: return

//------------- Mission: MISS2 (1) ---------------

:MISS2
03A4: name_thread 'MISS2'
0050: gosub @MISS2_START
00D6: if
0112:     wasted_or_busted // mission only
004D: jump_if_false @MISS2_END
0050: gosub @MISS2_FAILED

:MISS2_END
0004: $409 = 0 // $ = ? (int)
00D8: mission_cleanup
004E: end_thread

:MISS2_START
0317: increment_mission_attempts
0004: $409 = 1 // $ = ? (int)

:MISS2_PASSED
0050: gosub @MISS2_CLEAR
0008: $2005 += 1 // $ += ? (int)
004F: create_thread @STARTER
0051: return

:MISS2_FAILED
0050: gosub @MISS2_CLEAR
004F: create_thread @STARTER
0051: return

:MISS2_CLEAR
0051: return

Конечно, Вы можете писать собственные стартеры вручную, если нужны экзотические способы запуска :)

3. Видео-ролики

Мы можем создавать собственные видео-ролики с помощью скриптового языка. Плагин "CutScene" написан с целью упростить работу с ними. Класс основан на событиях. Всего их 3: перед сценой, после сцены и действие сцены.

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

Класс имеет несколько методов, которые помогут настроить некие параметры. Вот таблица этих методов:

КомандаОписание
ClearEventsУдаляет все события, чтобы можно было названичить их ещё раз в дальнейшем.
ChangeFadeColorИзменяет цвет затемнения и осветления экрана ( по умолчанию: 0x000000 ( чёрный цвет ) ).
ChangeFadeTimeУстанавливает время задержки при затемнении и осветлении экрана ( по умолчанию: 500 ).
SetAutoInterfaceВключает или отключает автоматическое прятанье элементов интерфейса, таких как радар и прочее.
PutPlayerActorBeforeУстанавливает позицию, куда будет перемещён игрок перед началом сцены.
PutPlayerActorAfterУстанавливает позицию, куда будет перемещён игрок после конца сцены.
ClearPutПозволяет указывать позицию перемещения игрока при следующем использовании.
WriteГенерирует код сцены ( вызывать нужно после установки событий ).

Указать цвет и время задержки можно в конструкторе, но не обязательно ( опциональные параметры ). Рассмотрим простой пример сцены:

[Thread]
public void SCENE() {

    var index = Int.local( 0 ); // 0@
    var cutSceneActors = Array<Actor>.local( 1, 3 ); // 1@ 2@ 3@

    var scene = new CutScene();

    scene.Before = delegate {
        comment = "Before";
        PlayerActor.set_immunities( 1, 1, 1, 1, 1 ).lock_position( true );
        PlayerChar.can_move( false );
        clear_area( 1, 1400.0, 400.0, 13.0, 30.0 );
        refresh_game_renderer( 1400.0, 400.0 );
        load_scene( 1400.0, 400.0, 13.0 ); // Camera.SetAtPos
        set_camera_behind_player();
        load_model( PedModel.ARMY, PedModel.BALLAS1 );
        load_requested_models();
        cutSceneActors[ 0 ].create( PedType.MISSION1, PedModel.BALLAS1, 1400.0, 400.0, 13.0 );
        cutSceneActors[ 1 ].create( PedType.MISSION1, PedModel.BALLAS1, 1403.0, 400.0, 13.0 );
        cutSceneActors[ 2 ].create( PedType.MISSION1, PedModel.ARMY, 1405.0, 400.0, 13.0 );
        set_camera_position( 1395.0, 405.0, 13.0, 0.0, 0.0, 0.0 );
        set_camera_point_at( 1400.0, 400.0, 13.0, 2 );
        cutSceneActors[ 1 ].task.look_at_actor( cutSceneActors[ 0 ], -1 );
        cutSceneActors[ 2 ].task.look_at_actor( cutSceneActors[ 0 ], -1 );
    };

    scene.Action = delegate {
        comment = "Action";
        cutSceneActors[ 0 ].start_facial_talk( 3000 );
        cutSceneActors[ 0 ].task.perform_animation( "ANIM", "PED", 4.0, 0, 0, 0, 0, 3000 );
        show_text_highpriority( FXT.Add( "GXTKEY1", "Это текст диалога" ), 3000, 0 );
        wait( 3000 );
        cutSceneActors[ 0 ].stop_facial_talk();
    };

    scene.After = delegate {
        comment = "After";
        destroy_model( PedModel.ARMY, PedModel.BALLAS1 );
        cutSceneActors.each( index, actor => {
            actor.remove_references().destroy();
        } );
        restore_camera_with_jumpcut();
        set_camera_behind_player();
        PlayerActor.set_immunities( 0, 0, 0, 0, 0 ).lock_position( false );
        PlayerChar.can_move( 1 );
    };

    scene.Write();
    // scene.ClearEvents(); // <- если события "Before", "Action" и "After" нужно установить значение null
    end_thread();
}

Всё это будет иметь такой вид:

//------------- SCENE ---------------

:SCENE
03A4: name_thread 'SCENE'
06AB: set_actor $3 all_weapons_hidden 1
0826: enable_hud 0
0581: enable_radar 0
02A3: enable_widescreen 1
/* Before */
02AB: set_actor $3 immunities BP 1 FP 1 EP 1 CP 1 MP 1
04D7: lock_actor $3 in_current_position 1
01B4: set_player $2 can_move 0
0395: clear_area 1 at 1400.0 400.0 13.0 radius 30.0
04E4: refresh_game_renderer_at 1400.0 400.0
03CB: set_rendering_origin_at 1400.0 400.0 13.0
0373: set_camera_directly_behind_player
0247: load_model 287
0247: load_model 102
038B: load_requested_models
009A: 1@ = create_actor_pedtype 24 model 102 at 1400.0 400.0 13.0
009A: 2@ = create_actor_pedtype 24 model 102 at 1403.0 400.0 13.0
009A: 3@ = create_actor_pedtype 24 model 287 at 1405.0 400.0 13.0
015F: set_camera_position 1395.0 405.0 13.0 rotation 0.0 0.0 0.0
0160: set_camera_point_at 1400.0 400.0 13.0 switchstyle 2
05BF: AS_actor 2@ look_at_actor 1@ -1 ms
05BF: AS_actor 3@ look_at_actor 1@ -1 ms
0169: set_fade_color_RGB 0 0 0
016A: fade 1 time 500
0707: start_scene_skip_to @SCENE_AUTO_SCENE_0
/* Action */
0967: actor 1@ move_mouth 3000 ms
0605: AS_actor 1@ perform_animation "ANIM" IFP "PED" framedelta 4.0 loopA 0 lockX 0 lockY 0 lockF 0 time 3000 ms // version A
00BC: show_text_highpriority GXT 'GXTKEY1' time 3000 flag 0
0001: wait 3000 ms
0968: actor 1@ stop_mouth

:SCENE_AUTO_SCENE_0
0169: set_fade_color_RGB 0 0 0
016A: fade 0 time 500
0701: end_scene_skip
0001: wait 500 ms
02A3: enable_widescreen 0
0826: enable_hud 1
0581: enable_radar 1
06AB: set_actor $3 all_weapons_hidden 0
/* After */
0249: release_model 287
0249: release_model 102

for 0@ = 0 to 3
01C2: remove_references_to_actor 1@(0@,3i) // Like turning an actor into a random pedestrian
009B: destroy_actor 1@(0@,3i)
end

02EB: restore_camera_with_jumpcut
0373: set_camera_directly_behind_player
02AB: set_actor $3 immunities BP 0 FP 0 EP 0 CP 0 MP 0
04D7: lock_actor $3 in_current_position 0
01B4: set_player $2 can_move 1
0169: set_fade_color_RGB 0 0 0
016A: fade 1 time 500
0001: wait 500 ms
004E: end_thread

Мы видим, что у нас сцена начинается и заканчивается с осветления экрана. Мы можем повлиять на это, передав в метод "Write" опциональные параметры. Также рассмотрим ситуацию, когда в начале и вконце сцены нужно перемещать игрока:

[Thread]
public void SCENE() {
    
    var scene = new CutScene();

    // указываем куда перемещать игрока до сцены
    scene.PutPlayerActorBefore( 1400.0, 25.0, 13.0, 180.0 );

    // указываем куда перемещать игрока после сцены
    scene.PutPlayerActorAfter( 1300.0, 125.0, 13.0, 0.0 );

    scene.Action = delegate {
        comment = "Action";
    };

    scene.Write( true, false ); // в начале сцены будет осветление, а в конце - нет

    // если в дальнейшем не надо перемещать игрока или нужно перемещать в другие позиции
    scene.ClearPut();

    end_thread();
}

Опциональные параметры метода "Write" позволяют контролировать наличие осветления и затемнения. В итоге будет это:

//------------- SCENE ---------------

:SCENE
03A4: name_thread 'SCENE'
0169: set_fade_color_RGB 0 0 0

// если указать первым параметром "true" в методе "Write", то будут эти две строчки
016A: fade 0 time 500
0001: wait 500 ms

// если вызвать метод "PutPlayerActorBefore"
00A1: put_actor $3 at 1400.0 25.0 14.0
0173: set_actor $3 z_angle_to 180.0

06AB: set_actor $3 all_weapons_hidden 1
0826: enable_hud 0
0581: enable_radar 0
02A3: enable_widescreen 1
0169: set_fade_color_RGB 0 0 0
016A: fade 1 time 500
0707: start_scene_skip_to @SCENE_AUTO_SCENE_0
/* Action */

:SCENE_AUTO_SCENE_0
0169: set_fade_color_RGB 0 0 0
016A: fade 0 time 500
0701: end_scene_skip
0001: wait 500 ms
02A3: enable_widescreen 0
0826: enable_hud 1
0581: enable_radar 1
06AB: set_actor $3 all_weapons_hidden 0

/* если указать вторым параметром "false" в методе "Write", то следующего кода не будет:
0169: set_fade_color_RGB 0 0 0
016A: fade 1 time 500
0001: wait 500 ms
*/

// если вызвать метод "PutPlayerActorAfter"
00A1: put_actor $3 at 1300.0 125.0 14.0
0173: set_actor $3 z_angle_to 0.0

004E: end_thread

Используя один класс, мы можем генерировать несколько сцен подряд или использовать в нескольких потоках. Давайте рассмотрим пример, когда такая ситуация есть:

[Thread]
public void SCENE() {
    
    var scene = new CutScene();
    scene.SetAutoInterface( false ); // не прячем радар, HUD
    
    scene.Action = delegate { comment = "Action #1"; };
    scene.Write( false, false );
    
    comment = "--- --- ---";
    comment = "--- --- ---";
    comment = "--- --- ---";
    
    scene.Action = delegate { comment = "Action #2"; };
    scene.Write( false, true );
    
    end_thread();

}

Нам не обязательно указывать все события сцены. Нужно указать метод хотя бы для "Action". После метода "Write" можно переопределить действие и генерировать другое действие сцены. Метод "SetAutoInterface" позволяет немного сократить код. Вот такой результат будет у нас:

//------------- SCENE ---------------

:SCENE
03A4: name_thread 'SCENE'
0169: set_fade_color_RGB 0 0 0
016A: fade 1 time 500
0707: start_scene_skip_to @SCENE_AUTO_SCENE_0
/* Action #1 */

:SCENE_AUTO_SCENE_0
0169: set_fade_color_RGB 0 0 0
016A: fade 0 time 500
0701: end_scene_skip
0001: wait 500 ms
/* --- --- --- */
/* --- --- --- */
/* --- --- --- */
0169: set_fade_color_RGB 0 0 0
016A: fade 1 time 500
0707: start_scene_skip_to @SCENE_AUTO_SCENE_1
/* Action #2 */

:SCENE_AUTO_SCENE_1
0169: set_fade_color_RGB 0 0 0
016A: fade 0 time 500
0701: end_scene_skip
0001: wait 500 ms
0169: set_fade_color_RGB 0 0 0
016A: fade 1 time 500
0001: wait 500 ms
004E: end_thread

Конечно, можно обойтись и без плагина, так как реализация сцен не такая сложная.

4. Классы Sanny Builder

Подключение плагинов добавляет некоторые классы, которые имеют туже цель, что и классы в Sanny Builder. К некоторым вещам настолько привыкаешь, что больше без них не можешь обойтись :) Я добавил классы "Model", "Garage" и "Camera". Все они статические. Другие нет смысла добавлять, так как уже есть готовые объекты генератора.

Использование очень простое:

[Thread]
public void TEST() {
    Model.Load( PedModel.ARMY );
    load_requested_models();
    
    jf( DefaultWaitTime, Model.Available( PedModel.ARMY ) );
    
    Model.Destroy( PedModel.ARMY );
    
    Camera.RestoreWithJumpcut();
    Camera.BehindPlayer();
    
    Garage.Activate( "Name" );
    
    end_thread();
}

Думаю, Вы поймёте что будет в итоге :) На текущий момент это все плагины и дополнения.