Генератор предусматривает использование условий. Сами конструкции имеют весьма плохой синтаксис и это, наверное, самое больное место генератора. 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
Если команда у Вас не выводится, проверьте не является ли она условем. Такое решение было принято с теми соображениями, что некоторые опкоды можно использовать как условие и процедуру.
Теперь вы знаете базовый минимум для написания скриптов!