Условия и циклы

Генератор предусматривает использование условий. Сами конструкции имеют весьма плохой синтаксис и это, наверное, самое больное место генератора. Sanny Builder позволяет нам использовать конструкции "IF-THEN-END" и "IF-THEN-ELSE-END". Для реализации условий, генератор имеет две команды: "and" и "or". Ниже приведён синтаксис написания условий:

[Thread]
public void TEST() {

    comment = "IF-THEN-END:";
    and( OnMission == 1, delegate {
        jump();
    } );

    comment = "IF_AND-THEN-END:";
    and( OnMission == 0, PlayerActor.is_dead(), delegate {
        jump();
    } );

    comment = "IF-THEN-END:";
    or( OnMission == 1, delegate {
        jump();
    } );

    comment = "IF_OR-THEN-END:";
    or( OnMission == 1, PlayerActor.is_dead(), delegate {
        jump();
    } );
	
    end_thread();
}

Если мы используем только одно условие, то команды "and" и "or" будут работать одинаково. На выходе мы получим такой код:

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

:TEST
03A4: name_thread 'TEST'

/* IF-THEN-END: */
00D6: if
0038:     $409 == 1 // $ == ? (int)
then
0002: jump @TEST
end

/* IF_AND-THEN-END: */
00D6: if and
0038:     $409 == 0 // $ == ? (int)
0118:     actor $3 dead
then
0002: jump @TEST
end

/* IF-THEN-END: */
00D6: if
0038:     $409 == 1 // $ == ? (int)
then
0002: jump @TEST
end

/* IF_OR-THEN-END: */
00D6: if or
0038:     $409 == 1 // $ == ? (int)
0118:     actor $3 dead
then
0002: jump @TEST
end

004E: end_thread

Если требуется разветвление ( ELSE ), то вот пример:

[Thread]
public void TEST() {

    comment = "IF_AND-THEN-ELSE-END:";
    and( OnMission == 1, PlayerChar.is_defined(), delegate {
        jump();
    }, delegate {
        PlayerChar.add_money( 2000 );
    } );

    comment = "IF_OR-THEN-ELSE-END:";
    or( OnMission == 1, PlayerActor.is_entering_vehicle(), delegate {
        jump();
    }, delegate {
        PlayerActor.set_max_health( 150 );
    } );
	
    end_thread();
}

Как видим, код не удобный и наличие делегатов немного путает. Результат этого кода:

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

:TEST
03A4: name_thread 'TEST'

/* IF_AND-THEN-ELSE-END: */
00D6: if and
0038:     $409 == 1 // $ == ? (int)
0256:     player $2 defined
then
0002: jump @TEST
else
0109: player $2 money += 2000
end

/* IF_OR-THEN-ELSE-END: */
00D6: if or
0038:     $409 == 1 // $ == ? (int)
09DE:     actor $3 entering_car
then
0002: jump @TEST
else
08AF: set_actor $3 max_health_to 150
end

004E: end_thread

Чтобы не писать каждый раз такие конструкции, я рекомендую сделать комментарий и в него поместить код условия и при необходимости скопировать его в нужное место. Для условия "IF-AND" есть альтернатива, но это сработает когда в нас нет вложенных условий с "IF-OR". Наш спасатель - это команда jf. Мы можем написать туда неограниченное количество условий и если хоть одно не сработает, то скрипт будет проверять их сначала.

Есть два варианта использования: с прыжком на первую метку когда условия не сработали и прыжок на автоматическую отметку с указанной задержкой. Второй способ - это уже полноценный цикл! Давайте рассмотрим оба способа:

[Thread]
public void SAVEGM() {
    wait( DefaultWaitTime );

    jf(
        PlayerChar.is_defined(),
        OnMission == 0,
        !PlayerActor.is_busted(),
        !PlayerActor.is_dead(),
        !PlayerActor.is_on_foot(),
        !is_fading(),
        is_key_pressed( VKKeys.F4 )
    );

    show_save_screen();
    jump();
}

[Thread]
public void STARTER() {
    var marker = RadarMarker.global( 2000 ).create_long_range( RadarIconID.CJ, 1400.0, 400.0, 13.0 );

    jf( DefaultWaitTime, // <-- время задержки
        OnMission == 0,
        PlayerChar.is_defined(),
        !PlayerChar.is_on_jetpack(),
        PlayerActor.is_near_point_3d_on_foot( false, 1400.0, 400.0, 13.0, 0.0, 0.0, 0.0 )
    );

    marker.disable();
    end_thread();
}

Как видим, разница в синтаксисе между двумя способами только в наличии времени задежки. Обратите внимание ещё на символ ! возле условий. Если написать его перед условием, то будет осуществлена проверка на "ложь", а не на "истину", этакий удобный аналог опкода с ключевым словом not в Sanny Builder. Вот результат работы:

//------------- SAVEGM ---------------

:SAVEGM
03A4: name_thread 'SAVEGM'
0001: wait $14 ms
00D6: if
0256:     player $2 defined
004D: jump_if_false @SAVEGM
00D6: if
0038:     $409 == 0 // $ == ? (int)
004D: jump_if_false @SAVEGM
00D6: if
8741: not actor $3 busted
004D: jump_if_false @SAVEGM
00D6: if
8118: not actor $3 dead
004D: jump_if_false @SAVEGM
00D6: if
844B: not actor $3 on_foot
004D: jump_if_false @SAVEGM
00D6: if
816B: not fading
004D: jump_if_false @SAVEGM
00D6: if
0AB0:     key_pressed 115
004D: jump_if_false @SAVEGM
03D8: show_save_screen
0002: jump @SAVEGM

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

:STARTER
03A4: name_thread 'STARTER'
02A7: $2000 = create_icon_marker_and_sphere 15 at 1400.0 400.0 13.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
8A0C: not player $2 on_jetpack
004D: jump_if_false @STARTER_AUTO_LABEL_0
00D6: if
00FE:     actor $3 sphere 0 in_sphere 1400.0 400.0 13.0 radius 0.0 0.0 0.0
004D: jump_if_false @STARTER_AUTO_LABEL_0

0164: disable_marker $2000
004E: end_thread

Раз мы перешли к циклам, то давайте рассмотрим их виды в генераторе. Sanny Builder предоставляет нам 3 вида циклов: "for", "while" и "repeat", плюс ещё цикл на основе меток. Все эти циклы доступны и в генераторе. Цикл "for" я не смог реализовать нормально, поэтому я разделил его на две отдельные команды:

[Thread]
public void TEST() {

    var index = Int.local( 0 ); // 0@
    var indexFloat = Float.local( 0 ); // 0@

    to( index, 0, 10, delegate { // for 0@ = 0 to 10
        load_model( index );
    } );
    load_requested_models();

    downto( indexFloat, 4.0, 0.0, delegate { // for 0@ = 4.0 downto 0.0 step 0.25
        PlayerActor.set_position( 0.0, indexFloat, 13.0 );
    }, 0.25 );

    end_thread();
}

Обе команды принимают на вход переменную-счётчик и требуют два параметра: начало и конец. В конце команды можно ещё написать шаг цикла. Третьим параметром требуется указать функцию, которая будет выполняться в цикле. Я использовал анонимый метод через делегат. Если мы скомпилируем этот код, то получим следующее:

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

:TEST
03A4: name_thread 'TEST'

int 0@ // нужно, если в качестве параметров цикла будут переменные
for 0@ = 0 to 10
0247: load_model 0@
end

038B: load_requested_models

float 0@ // нужно, если в качестве параметров цикла будут переменные
for 0@ = 4.0 downto 0.0 step 0.25
00A1: put_actor $3 at 0.0 0@ 13.0
end

004E: end_thread

Что касается циклов "while" и "repeat", то генератор имеет всего одну команду - "loop". Какой из циклов будет сгенерирован зависит от места, где написано условие. Если нам нужен "бесконечный цикл", то условие не пишем:

[Thread]
public void TEST() {

    loop( PlayerChar.is_defined(), delegate {
        wait( 0 );
        comment = "while";
    } );

    loop( delegate {
        wait( 0 );
        comment = "repeat";
    }, !PlayerChar.is_defined() );

    loop( delegate {
        wait( 0 );
        comment = "while true";
    } );

    end_thread();
}

Здесь кроме условия мы должны написать анонимную функцию через делегат. Если условие будет вначале, то будет использоваться "while". Если в конце - "repeat". Без условия мы получим "while true". Этот код буде иметь такой вид на выходе:

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

:TEST
03A4: name_thread 'TEST'

while 0256:     player $2 defined
0001: wait 0 ms
/* while */
end

repeat
0001: wait 0 ms
/* repeat */
until 8256: not player $2 defined

while true
0001: wait 0 ms
/* while true */
end

004E: end_thread

Иногда нам нужно использовать команды "break" и "continue" в циклах. В таких случаях слово "delegate" нужно заменить на лямбда-выражение. Это даст нам возможность использовать результат цикла и вызвать требуемые команды:

[Thread]
public void TEST() {

    loop( anyName => {
        wait( 0 );
        and( !PlayerChar.is_defined(), delegate {
            anyName.Break();
        } );
        anyName.Continue();
        comment = "break and continue";
    } );

    end_thread();
}

Результат:

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

:TEST
03A4: name_thread 'TEST'

while true
0001: wait 0 ms

00D6: if
8256: not player $2 defined
then
break
end

continue
/* break and continue */
end

004E: end_thread

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

У нас есть возможность также сделать цикл, который похож на "jf", только без условий. Так называемый цикл на метках:

[Thread]
public void TEST() {

    cycle( delegate {
        wait( 0 );
        and( PlayerChar.is_defined(), delegate {
            jump( 2 );
        } );
        comment = "label loop";
    } );

    label = 2;
    end_thread();
}

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

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

:TEST
03A4: name_thread 'TEST'

:TEST_LOOP_0
0001: wait 0 ms
00D6: if
0256:     player $2 defined
then
0002: jump @TEST_2
end
/* label loop */
0002: jump @TEST_LOOP_0

:TEST_2
004E: end_thread

Кроме этого, мы можем использовать циклы C#, чтобы сгенерировать код для MAIN.SCM:

[Thread]
public void TEST() {

    // создаём  C#-массив, который будем перебирать через циклы C#
    VehicleModel[] ids = new VehicleModel[ 3 ] { VehicleModel.ADMIRAL, VehicleModel.NEBULA, VehicleModel.PEREN };

    foreach( var item in ids ) {
        load_model( item );
    }
    load_requested_models();

    wait( DefaultWaitTime );

    for( int i = 0; i < ids.Length; i++ ) {
        destroy_model( ids[ i ] );
    }

    end_thread();
}

Этот приём не создаст никаких циклов и будет генерировать всё последовательно. Его лучше использовать когда у нас есть данные, которые нельзя както упорядочить. Вот результат:

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

:TEST
03A4: name_thread 'TEST'
0247: load_model 445
0247: load_model 516
0247: load_model 404
038B: load_requested_models
0001: wait $14 ms
0249: release_model 445
0249: release_model 516
0249: release_model 404
004E: end_thread

Если возвращаться к условиям, то все условия возвращают тип "Condition". Проверить какая команда является условием можно через подсказку Visual Studio:

Если написать команду вне блока условий или цикла, то генератор ничего не напишет. Например, это будет проигнорировано:

[Thread]
public void TEST() {
    ini_write_int( 25, "CLEO/INI", "SECTION", "KEY" );
    end_thread();
}

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

[Thread]
public void TEST() {
    ini_write_int( 25, "CLEO/INI", "SECTION", "KEY" ).Write();
    ( !PlayerChar.is_defined() ).Write();
    end_thread();
}

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

:TEST
03A4: name_thread 'TEST'
0AF1: write_int 25 to_ini_file "CLEO/INI" section "SECTION" key "KEY" // IF and SET
8256: not player $2 defined
004E: end_thread

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

Теперь вы знаете базовый минимум для написания скриптов!