0008. Условия и циклы - wmysterio/gta-script-generator GitHub Wiki
Генератор предусматривает использование условий. Сами конструкции имеют весьма плохой синтаксис и это, наверное, самое больное место генератора.
Условия IF-THEN-END и IF-THEN-ELSE-END
Sanny Builder позволяет нам использовать конструкции IF-THEN-END и IF-THEN-ELSE-END. Для реализации условий, генератор имеет две команды — and
и or
. Ниже приведён синтаксис написания условий:
public class TEST : Thread {
public override void START( LabelJump label ) {
wait( 0 );
Comment = "IF-THEN-END:";
and( OnMission == 1, delegate {
jump( START );
} );
Comment = "IF_AND-THEN-END:";
and( OnMission == 0, PlayerActor.is_dead(), delegate {
jump( START );
} );
Comment = "IF-THEN-END:";
or( OnMission == 1, delegate {
jump( START );
} );
Comment = "IF_OR-THEN-END:";
or( OnMission == 1, PlayerActor.is_dead(), delegate {
jump( START );
} );
end_thread();
}
}
На выходе мы получим такой код:
//------------- THREAD TEST ---------------
:TEST
03A4: name_thread 'TEST'
0001: wait 0 ms
// 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
Если требуется разветвление (else), то вот пример:
public class TEST : Thread {
public override void START( LabelJump label ) {
wait( 0 );
Comment = "IF_AND-THEN-ELSE-END:";
and( OnMission == 1, PlayerChar.is_defined(), delegate {
jump( START );
}, delegate {
PlayerChar.add_money( 2000 );
} );
Comment = "IF_OR-THEN-ELSE-END:";
or( OnMission == 1, PlayerActor.is_entering_vehicle(), delegate {
jump( START );
}, delegate {
PlayerActor.set_max_health( 150 );
} );
end_thread();
}
}
Результат этого кода:
------------- THREAD TEST ---------------
:TEST
03A4: name_thread 'TEST'
0001: wait 0 ms
// 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
Как видим, код не удобный и наличие делегатов немного путает. Чтобы не писать каждый раз такие конструкции, я рекомендую использовать сниппеты или код условий перемещать в окно Панель элементов Visual Studio (CTRL
+W
,X
; доступно также в меню Вид->Панель элементов).
Условия JF
Генератор поддерживает инструкцию JF, которая делает переход к метке, если условия не выполнились. Это позволит писать код на более низком уровне в случае необходимости. Для этого используется метод jf
. Особенностью этого подхода является то, что метод не имеет ограничений на количество условий. Рассмотрим пример использования:
public class TEST : Thread {
public override void START( LabelJump label ) {
jf( END_SCRIPT, PlayerChar.is_defined(), !PlayerActor.is_dead(), !PlayerActor.is_busted() );
PlayerChar.add_money( 2000 );
Jump += END_SCRIPT;
}
private void END_SCRIPT( LabelJump label ) {
end_thread();
}
}
Метод jf принимает первым параметром метку, куда будет осуществлён переход, если условия не выполнились. Если игрок найден, не мёртв и не арестован, то игроку дадут 2000$. В итоге будет вот такой код:
//------------- THREAD TEST ---------------
:TEST
03A4: name_thread 'TEST'
00D6: if
0256: player $2 defined
004D: jump_if_false @TEST_LABEL_0
00D6: if
8118: not actor $3 dead
004D: jump_if_false @TEST_LABEL_0
00D6: if
8741: not actor $3 busted
004D: jump_if_false @TEST_LABEL_0
0109: player $2 money += 2000
0002: jump @TEST_LABEL_0
:TEST_LABEL_0
004E: end_thread
Недостаток такого подхода в том, что все условия проверяются по очереди, когда можно было некоторые из них объединить. Этот факт я тоже учёл. Метод and и or имеют перегрузки, которые принимают метку в качестве первого параметра, но исключают возможность использовать блоков then и else. Рассмотрим следующий пример:
public class TEST : Thread {
public override void START( LabelJump label ) {
jf( END_SCRIPT, PlayerChar.is_defined() );
and( END_SCRIPT, !PlayerActor.is_dead(), !PlayerActor.is_busted() );
PlayerChar.add_money( 2000 );
Jump += END_SCRIPT;
}
private void END_SCRIPT( LabelJump label ) {
end_thread();
}
}
В этом случае сначала будет идти проверка только на существование игрока. Далее уже идут проверки на выполнение всех условий:
//------------- THREAD TEST ---------------
:TEST
03A4: name_thread 'TEST'
00D6: if
0256: player $2 defined
004D: jump_if_false @TEST_LABEL_0
00D6: if and
8118: not actor $3 dead
8741: not actor $3 busted
004D: jump_if_false @TEST_LABEL_0
0109: player $2 money += 2000
0002: jump @TEST_LABEL_0
:TEST_LABEL_0
004E: end_thread
На этом возможности jf не заканчиваются! Мы можем использовать метод wait
, где дополнительно указывается набор условий, выполнение которых также нужно ждать:
public class TEST : Thread {
static RadarMarker marker;
public override void START( LabelJump label ) {
marker.create_long_range( RadarIconID.CJ, 1400.0, 400.0, 13.0 );
wait( 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();
wait( PlayerActor.is_dead() ); // <-- время задержки отсутствует
end_thread();
}
}
Если мы посмотрим результат, то увидим что у нас генерируются автоматические метки для конструкций JF:
//------------- THREAD TEST ---------------
:TEST
03A4: name_thread 'TEST'
02A7: $2000 = create_icon_marker_and_sphere 15 at 1400.0 400.0 13.0
:TEST_JF_0
0001: wait $14 ms
00D6: if
0038: $409 == 0 // $ == ? (int)
004D: jump_if_false @TEST_JF_0
00D6: if
0256: player $2 defined
004D: jump_if_false @TEST_JF_0
00D6: if
8A0C: not player $2 on_jetpack
004D: jump_if_false @TEST_JF_0
00D6: if
00FF: actor $3 sphere 0 in_sphere 1400.0 400.0 13.0 radius 0.0 0.0 0.0 on_foot
004D: jump_if_false @TEST_JF_0
0164: disable_marker $2000
:TEST_JF_1
0001: wait 0 ms
00D6: if
0118: actor $3 dead
004D: jump_if_false @TEST_JF_1
004E: end_thread
Обратите внимание ещё на символ !
возле условий. Если написать его перед условием, то будет осуществлена проверка на ложь, а не на истину. Этакий удобный аналог реверса опкода, который начинается с числа 8
и имеет ключевое слово not
в описании.
Циклы
Раз мы перешли к циклам, то давайте рассмотрим их виды. Sanny Builder предоставляет нам 3 вида: for, while и repeat. Все они доступны и в генераторе. Цикл for я не смог реализовать нормально, поэтому я разделил его на две отдельные команды to
и downto
:
public class TEST : Thread {
Int index = local( 0 ); // [email protected]
Float indexFloat = local( 0 ); // [email protected]
public override void START( LabelJump label ) {
// for [email protected] = 0 to 10
to( index, 0, 10, delegate {
load_model( index );
} );
load_requested_models();
// for [email protected] = 4.0 downto 0.0 step 0.25
downto( indexFloat, 4.0, 0.0, delegate {
PlayerActor.set_position( 0.0, indexFloat, 13.0 );
}, 0.25 ); // <- 0.25 - шаг цикла, опциональный параметр
end_thread();
}
}
Обе команды принимают на вход переменную-счётчик и требуют два параметра: начало и конец. В конце команды можно ещё написать шаг цикла. Третьим параметром требуется указать функцию, которая будет выполняться в цикле. Я использовал там анонимный метод. Если мы скомпилируем этот код, то получим следующее:
//------------- THREAD TEST ---------------
:TEST
03A4: name_thread 'TEST'
int [email protected] // нужно, если в качестве параметров цикла будут переменные
for [email protected] = 0 to 10
0247: load_model [email protected]
end
038B: load_requested_models
float [email protected] // нужно, если в качестве параметров цикла будут переменные
for [email protected] = 4.0 downto 0.0 step 0.25
00A1: put_actor $3 at 0.0 [email protected] 13.0
end
004E: end_thread
Что касается циклов while и repeat, то генератор имеет всего одну команду — loop
. Какой из циклов будет генерироваться зависит от места, где написано условие. Если нам нужен "бесконечный цикл", то условие не пишем вовсе:
public class TEST : Thread {
public override void START( LabelJump label ) {
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. Этот код буде иметь такой вид на выходе:
//------------- THREAD 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 нужно заменить на лямбда выражение. Это даст нам возможность вызывать требуемые команды:
public class TEST : Thread {
public override void START( LabelJump label ) {
loop( l => { // лямбда выражение
wait( 0 );
and( !PlayerChar.is_defined(), delegate {
[email protected]();
} );
[email protected]();
Comment = "break and continue";
} );
end_thread();
}
}
Результат:
//------------- 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, только без условий (так называемый цикл на метках). Он реализуется с помощью события Cycle
. Он не делает никаких прыжков и печатает его сразу после команды, которая была до него:
public class TEST : Thread {
public override void START( LabelJump label ) {
Cycle += TEST_LABEL_LOOP;
}
private void TEST_LABEL_LOOP() {
wait( 0 );
and( PlayerChar.is_defined(), delegate {
Jump += END_SCRIPT;
} );
Comment = "LOOP FOR LABEL";
}
private void END_SCRIPT( LabelJump label ) {
end_thread();
}
}
Событие Cycle создаст автоматическую метку. В анонимной функции указывается код, который будет выполнятся в цикле. В результате мы будем иметь это:
//------------- THREAD TEST ---------------
:TEST
03A4: name_thread 'TEST'
:TEST_CYCLE_0
0001: wait 0 ms
00D6: if
0256: player $2 defined
then
0002: jump @TEST_LABEL_0
end
// LOOP FOR LABEL
0002: jump @TEST_CYCLE_0
:TEST_LABEL_0
004E: end_thread
Кроме этого, мы можем использовать циклы C#, чтобы генерировать код для main.scm:
public class TEST : Thread {
public override void START( LabelJump label ) {
// создаём 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();
}
}
Этот приём не создаст никаких циклов и будет генерировать всё последовательно. Его лучше использовать тогда, когда у нас есть данные, которые нельзя как-то упорядочить. Вот результат:
//------------- 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:
Если написать команду вне блока условий или цикла, то генератор ничего не напишет. Например, это будет проигнорировано:
public class TEST : Thread {
public override void START( LabelJump label ) {
ini_write_int( 25, "CLEO/INI", "SECTION", "KEY" ); <-- не будет генерироваться
end_thread();
}
}
В основном это связано с оптимизацией генерации: сначала собираем все условия, а только потом "печатаем" их. Циклы и условия делают это автоматически, но для ручного вызова условной команды требуется вызвать метод write
:
public class TEST : Thread {
public override void START( LabelJump label ) {
ini_write_int( 25, "CLEO/INI", "SECTION", "KEY" ).write();
( !PlayerChar.is_defined() ).write();
end_thread();
}
}
Поверку на ложь нужно взять в скобки, так как метод write ничего не возвращает. На выходе мы получим уже такое:
//------------- THREAD TEST ---------------
: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
Если команда у Вас не выводится, проверьте не является ли она условием. Такое решение было принято с теми соображениями, что некоторые опкоды можно использовать как условие и как процедуру.
Примечание: Если мы используем только одно условие, то команды and и or будут работать одинаково!
Примечание: Чтобы не играться с write в ini-файлах, используйте класс Ini
.