Переменные, типы данных

Sanny Builder даёт возможность использовать тип данных как переменную класса, но их количество ограниченно и приходилось вручную писать классы и их методы. Генератор же использует все типы данных, и сейчас мы научимся их создавать.

Начну с простых вещей. Типы условно можно разделить на числовые и строковые. Они записываются в переменные, которые могут быть локальными или глобальными. Если в Sanny Builder нам достаточно было использовать символы @ и $ чтобы указать локальная переменная или нет, то в генераторе этот механизм совсем иной. Сначала нужно объявить переменную и только потом записать значение или вызвать команду. Для этого используются команды local и global. Давайте создадим переменную типа Int и запишем в неё число:

[Thread]
public void TEST() {
    var myLocalVariable = Int.local( 0 );         // 0@
    var myGlobalVariable = Int.global( 2000 );    // $2000

    // Не надо так! Иначе перменная "myLocalVariable" станет такой-же, как и "myGlobalVariable":
    // myLocalVariable = myGlobalVariable;

    myLocalVariable.value = 10;
    myGlobalVariable.value = myLocalVariable;

    end_thread();
}

На выходе мы получим следующий код:

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

:TEST
03A4: name_thread 'TEST'

0006: 0@ = 10 // @ = ? (int)
008A: $2000 = 0@ // $ = @ (int)

004E: end_thread

Это напоминает кострукцию "VAR-END" в Sanny Builder, только здесь она обязательна. Обратите внимание, что здесь используется свойство "value", чтобы задать новое значение. Сами значения могут принимать как обычные числа или строки, так и переменные, как в примере выше. Генератор распознает что передано и подставит нужный опкод. Таким образом нам не нужно каждый раз искать опкод для получаения значения одной переменной в другую.

Также Вы заметили, что я передал какие-то числа в методы объявления переменных. Это - индексы ( номера ) переменных. Каждый поток имеет свой набор переменных и, если задать индекс вне диапазона, генератор покажет ошибку.

Мы видим, что в результате в нас нет переменных со строковыми именами. Эту возможность я убрал, так как это сложно контролировать. Поэтому создаётся всё через её индекс. Sanny Builder поймёт нас и так.

Для GTA San Andreas мы можем использовать переменные с маркировками v и s. В этом случае в нас нет привязки к виду кавычек:

[Thread]
public void TEST() {
    var myVString = vString.local( 0 ); // 0@v
    var mySstring = sString.global( 2000 ); // s$2000

    myVString.value = "vString";
    mySstring.value = "sString";

    end_thread();
}

Генератор поймёт какие кавычки использовать. И в результате будет следующее:

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

:TEST
03A4: name_thread 'TEST'

06D2: 0@v = "vString" // @v = ? (vstring)
05A9: s$2000 = 'sString' // s$ = ? (sstring)

004E: end_thread

Некоторые типы данных поддерживают арифметические операции. Я рекомендую использовать их по очереди, хоть генератор позволяет нам делать сложные конструкции:

[Thread]
public void TEST() {

    var myInt = Int.local( 0 ); // 0@
    var myFloat = Float.local( 1 ); // 1@

    myInt.value = 0;
    myFloat.value = 0.0;
    // myInt.value = 0.0; // будет вызвана ошибка генерации кода
    // myFloat.value = 0; // будет вызвана ошибка генерации кода

    comment = "easy construction:";

    myInt += 2;
    myFloat -= 4.0;
    myInt /= 4;
    myFloat *= 6.0;

    comment = "difficult construction:";

    var myInt2 = Int.local( 2 ); // 2@
    myInt2.value = 10;

    myInt += ( myInt + myInt2 / myInt2 - myInt );

    end_thread();
}

Использование сложных конструкций часто приводит к непредсказуемым результатам, поэтому лучше не использовать их вовсе. Вот результат:

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

:TEST
03A4: name_thread 'TEST'
0006: 0@ = 0 // @ = ? (int)
0007: 1@ = 0.0 // @ = ? (float)

/* easy construction: */
000A: 0@ += 2 // @ += ? (int)
000F: 1@ -= 4.0 // @ -= ? (float)
0016: 0@ /= 4 // @ /= ? (int)
0013: 1@ *= 6.0 // @ *= ? (float)

/* difficult construction: */
0006: 2@ = 10 // @ = ? (int)

0072: 2@ /= 2@ // @ /= @ (int)
005A: 0@ += 2@ // @ += @ (int)
0062: 0@ -= 0@ // @ -= @ (int)
005A: 0@ += 0@ // @ += @ (int)
004E: end_thread

Обратите внимание, что дробные числа записываются без каких либо префиксов ( синтаксис C# ). Также генератор чувствителен к типам. Если Sanny Builder мог позволить записать в переменную типа Int дробное значение, то генератор этого не позволит сделать. Это нужно для контроля типов. Другими словами чтобы не допускать логических ошибок, которые приводят к неожиданным результатам уже в игре.

Однако для типа Int мы можем указывать тип bool. В этом случае "истина" будет записана как 1, а "ложь", как 0. Если команда принимает параметр типа "bool", а мы туда передадим целое число, то генератор попытается преобразовать число в булево.

Мы можем объявить две переменные, которые имеют один и тотже индекс. В этом случае нужно использовать их по очереди, так как данные будут перезаписаны уже в игре:

[Thread]
public void TEST() {

    var myIntVal = Int.local( 0 ); // 0@
    var myFloatVal = Float.local( 0 ); // 0@

    comment = "first int:";
    myIntVal.value = true;
    myIntVal.value = 1000;
    wait( myIntVal );

    comment = "next float:";
    myFloatVal.value = 1.0;
    set_gamespeed( myFloatVal );

    end_thread();
}

Вот код, который будет сгенерирован в этом случае:

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

:TEST
03A4: name_thread 'TEST'

/* first int: */
0006: 0@ = 1 // @ = ? (int)
0006: 0@ = 1000 // @ = ? (int)
0001: wait 0@ ms

/* next float: */
0007: 0@ = 1.0 // @ = ? (float)
015D: set_gamespeed 0@

004E: end_thread

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

// Указываем переменные в области видимости класса, не инициализируя их!
static Int localTimer1, localTimer2;

// Инициализация в области видимости класса приведёт к ошибке.
// static Int index = Int.local( 0 ); // так будет ошибка

[Thread]
public void TEST1() {
    localTimer1 = Int.local( 32 );
    localTimer2 = Int.local( 33 );

    localTimer1.value = 0;
    localTimer2.value = 0;
    end_thread();
}

[Thread]
public void TEST2() {
    localTimer1.value = 0;
    localTimer2.value = 0;
    end_thread();
}

Обратите внимание, что возле типа нужно указать ключевое слово static. По непонятным мне причинам, без этого слова, переменная будет равна null и работать с ней будет нельзя. В итоге у нас будет такой результат:

//------------- TEST1 ---------------

:TEST1
03A4: name_thread 'TEST1'
0006: 32@ = 0 // @ = ? (int)
0006: 33@ = 0 // @ = ? (int)
004E: end_thread

//------------- TEST2 ---------------

:TEST2
03A4: name_thread 'TEST2'
0006: 32@ = 0 // @ = ? (int)
0006: 33@ = 0 // @ = ? (int)
004E: end_thread

Миссии имеют свой набор переменных и их количество больше. Если индекс у нас больше чем лимит потока, то инициализировать переменные нужно в коде миссии.

Класс "Script" уже имеет в своём составе некоторые переменные, которые уже можно использовать и не инициализировать. Это переменная для игрока, для актёра игрока, для статуса миссий и скриптов, переменная для группы игрока и переменная задержки по-умолчанию. Давайте создадим игрока, дадим одежду и контроль:

[Thread]
public void MAIN() {
    fade( false, 0 );
    refresh_game_renderer( 2488.562, -1666.865 );
    load_scene( 2488.562, -1666.865, 13.3757 ); // Camera.SetAtPos

    PlayerChar.create( 2488.562, -1666.865, 12.8757 );
    PlayerChar.can_move( false );
    PlayerChar.get_actor( PlayerActor );
    PlayerChar.get_group( PlayerGroup );
    PlayerActor.set_z_angle( 262.0 );
    set_camera_behind_player();

    PlayerChar.set_clothes( "VEST", "VEST", ClothesBodyPart.TORSO );
    PlayerChar.set_clothes( "JEANSDENIM", "JEANS", ClothesBodyPart.LEGS );
    PlayerChar.set_clothes( "SNEAKERBINCBLK", "SNEAKER", ClothesBodyPart.SHOES );
    PlayerChar.set_clothes( "PLAYER_FACE", "HEAD", 1 );
    PlayerChar.rebuild(); // Player.Build
    save_player_clothes();
  
    wait( 1000 );
    release_weather();
    fade( 1, 1000 );
    PlayerChar.can_move( 1 );

    end_thread();
}

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

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)
016A: fade 0 time 0
04E4: refresh_game_renderer_at 2488.562 -1666.865
03CB: set_rendering_origin_at 2488.562 -1666.865 13.3757
0053: $2 = create_player 0 at 2488.562 -1666.865 12.8757
01B4: set_player $2 can_move 0
01F5: $3 = get_player_actor $2
07AF: $11 = player $2 group
0173: set_actor $3 z_angle_to 262.0
0373: set_camera_directly_behind_player
087B: set_player $2 clothes_texture "VEST" model "VEST" body_part 0
087B: set_player $2 clothes_texture "JEANSDENIM" model "JEANS" body_part 2
087B: set_player $2 clothes_texture "SNEAKERBINCBLK" model "SNEAKER" body_part 3
087B: set_player $2 clothes_texture "PLAYER_FACE" model "HEAD" body_part 1
070D: rebuild_player $2
0793: save_player_clothes
0001: wait 1000 ms
01B7: release_weather
016A: fade 1 time 1000
01B4: set_player $2 can_move 1
004E: end_thread

Если скомпилировать в Sanny Builder, то в игре мы уже сможем управлять игроком и делать прочие базовые вещи:

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

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

[Thread]
public void TEST() {
    PlayerChar.create( 2488.562, -1666.865, 12.8757 ).get_actor( PlayerActor ).get_group( PlayerGroup );
}

Команды выстраиваются по цепочке и последовательно выполняются. Механизм цепных функций доступен только для типов данных!

P. S. Обратите внимание, что типы данных ВСЕГДА пишутся с большой буквы. "Int" и "int" - это разные вещи.