Testing in open.mp
Проводити тестування в SA:MP доволі складно. Здебільшого це просто підключення до сервера та розміщення відбитків скрізь. Бібліотеки, такі як y_testing, значно полегшують написання модульних тестів, та лише для досить "чистого" коду — тобто коду, який працює на сервері без великої взаємодії гравця. Ви можете перевірити таку дивовижну поведінку, як ця, але вона все ще обмежена і не допомагає звузити проблеми, коли вони з’являються.
Отже, як open.mp вирішує це? Для початку код є відкритим, тому у вас є повний спектр чинних інструментів налагодження, таких як GDB та Visual Studio, доступних для проходження та перевірки коду. Ми також плануємо додати подібні інструменти для самих скриптів.
Але найбільшим доповненням є "фіктивна" бібліотека. Це дуже просто, це дозволяє створювати фальшивих гравців, які діють точно так само, як справжні гравці, але без підключеного клієнта. Отже, будь-які дії, виконувані над ними за допомогою сценарію, можуть бути прочитані та проаналізовані за допомогою іншого сценарію. Візьмемо найпростіший приклад, можливий для першої демонстрації: показати гравцеві чекпоїнт і підтвердити, що функція спрацювала [eng]
and confirming it was sent
PTEST__ SetPlayerCheckpoint(playerid)
{
SetPlayerCheckpoint(playerid, 0.0, 0.0, 4.0, 5.0);
ASK("Чи бачите Ви чекпоїнт в центрі світу?");
}
Використання такої бібліотеки, як "Pawn.RakNet", може прибрати питання, автоматизуючи перевірку вихідних пакетів, але все одно вимагає підключеного програвача, тому не можна автоматизувати та повторювати:
static
gCheckpointPlayer,
Float:gX,
Float:gY,
Float:gZ,
Float:gS;
PTEST__ SetPlayerCheckpoint(playerid)
{
SetPlayerCheckpoint(playerid, 0.0, 0.0, 4.0, 5.0);
ASSERT(gX == 0.0);
ASSERT(gY == 0.0);
ASSERT(gZ == 4.0);
ASSERT(gS == 5.0);
ASSERT(gCheckpointPlayer == playerid);
}
OPacket:107(playerid, BitStream:bs)
{
gCheckpointPlayer = playerid;
BS_IgnoreBits(bs, 8);
BS_ReadFloat(bs, gX);
BS_ReadFloat(bs, gY);
BS_ReadFloat(bs, gZ);
BS_ReadFloat(bs, gS);
return 1;
}
У open.mp ми маємо API, подібний до Pawn.RakNet, але з підробленими гравцями. Отже, ви створюєте гравця без екземпляра гри й використовуєте його так само як наче він справжній. Приклад:
TEST__ SetPlayerCheckpoint()
{
new playerid = Mock_CreatePlayer("Mr Mock");
// Усунути всі наявні пакети для зручності пошуку.
Mock_PurgeSent(playerid);
// Показати чекпоїнт, як звичайний.
SetPlayerCheckpoint(playerid, 0.0, 0.0, 4.0, 5.0);
// Перевірка на те, чи пакет "SetCheckpoint" показався.
new MockPacket:packet = Mock_GetPacket(playerid, "SetCheckpoint");
ASSERT(packet);
if (!packet) return;
// Перевірка на те, чи містить пакет 4 компоненти, кожен по 32 біти.
ASSERT(MockPacket_Components(packet) == 4);
ASSERT(MockPacket_Bits(packet) == 4 * 32);
// Перевірка різних компонентів.
new Float:tmp;
ASSERT(MockPacket_ReadFloat(packet, 0, tmp));
ASSERT(tmp == 0.0);
ASSERT(MockPacket_ReadFloat(packet, 1, tmp));
ASSERT(tmp == 0.0);
ASSERT(MockPacket_ReadFloat(packet, 2, tmp));
ASSERT(tmp == 4.0);
ASSERT(MockPacket_ReadFloat(packet, 3, tmp));
ASSERT(tmp == 5.0);
}
Цей код є повністю повторюваним, перевіреним, автономним (без глобальних змінних та зайвих зворотних викликів) і простим для розширення.
Але іноді це не все так просто. Якщо ви створюєте об'єкт, ви не можете просто перевірити, чи його миттєво надіслано гравцеві, тому що він може не знаходитись поблизу нього, а отже, вбудований стример ще не надіслав його йому. Щоб розв'язати цю проблему, нам спочатку потрібно пройти невелику дотичну, щоб пояснити, як працює основна синхронізація для таких речей, як SetPlayerPos
. Коли ви встановлюєте позицію гравця за допомогою SetPlayerPos
, сервер НЕ оновлює їх позицію внутрішньо та для всіх інших гравців відразу. Натомість команда SET POSITION
надсилається тому гравцеві, якого переміщують, він телепортується на нову позицію, а потім повідомляє про нову позицію серверу в наступному пакеті синхронізації. Причина, по якій в основному є відставання — може бути один або кілька пакетів синхронізації, які все ще перебувають у польоті зі старим положенням після встановлення нового положення. То що це означає для фіктивних гравців? Це означає, що ви насправді не можете встановити їх позицію, принаймні не звичайним способом. Немає реального клієнта, тому немає нічого, що отримує пакети (тобто команда set position), і немає нічого, що синхронізує ці дані назад із сервером.
Ми можемо генерувати будь-які фіктивні дані синхронізації для сервера з імітаційних програвачів, так що це один зі способів оновити їх позицію, але це може бути трохи громіздко для однієї загальної операції, тому існує Mock_SetPlayerPos
саме для цього одного випадку використання. Але це все ще не зовсім історія, тому що нам потрібно, щоб стример запускався та оновлювався з новою позицією, що трапляється лише періодично (кожні кілька мілісекунд, залежно від швидкості галочки сервера). Для цього ми маємо ще одну макетну функцію — Mock_Tick
, яка стрибає час сервера вперед на задану кількість мікросекунд (НЕ мілісекунд). Це абсолютно не слід використовувати на реальних серверах, оскільки це може створювати всілякі дивацтва з таймерами та іншим чутливим до часу кодом, але може бути використано для підробки часу, що проходить в тестах. Зверніть увагу, що Mock_Tick (10000)
не буде затримуватися на 10 мс, натомість миттєво збільшить час на 10 мілісекунд.
TEST__ SetPlayerCheckpoint()
{
new playerid = Mock_CreatePlayer("Mr Mock");
// Усунути всі наявні пакети для зручності пошуку.
Mock_PurgeSent(playerid);
// Створити об’єкт.
CreateObject(1337, 100.0, 100.0, 4.0);
// Фейкове оновлення позиції фейково гравця на сервері, біля об’єкта <details><summary>[eng]</summary>// Fake the mock player's position updating to the server, near the object.</details>.
Mock_SetPlayerPos(playerid, 105.0, 105.0, 4.0);
// Тепер фейк проходить деякий час, тому стример може працювати (50 мс має зробити).<details><summary>[eng]</summary>// Now FAKE pass some time, so the streamer can run (50ms should do).</details>
Mock_Tick(50000);
// Перевірка на те чи функція "CreateObject" спрацювала.
new MockPacket:packet = Mock_GetPacket(playerid, "CreateObject");
ASSERT(packet);
if (!packet) return;
// Перевірка на те, чи містить пакет 4 компоненти, кожен по 32 біти.
ASSERT(MockPacket_Components(packet) > 5);
// Перевірка різних компонентів.
new int;
new Float:tmp;
ASSERT(MockPacket_ReadInt32(packet, 1, int));
ASSERT(int == 1337);
ASSERT(MockPacket_ReadFloat(packet, 2, tmp));
ASSERT(tmp == 100.0);
ASSERT(MockPacket_ReadFloat(packet, 3, tmp));
ASSERT(tmp == 100.0);
ASSERT(MockPacket_ReadFloat(packet, 4, tmp));
ASSERT(tmp == 4.0);
}
Важлива примітка про Mock_Tick
. Якщо ви називаєте це як, скажімо Mock_Tick (100000)
і у вас працює таймер 1 мс, це моментально спрацьовує, щоб таймер запустився, але таймер буде думати, що він довго пропускав свій час (100 мс), і поставить в чергу. знову. Під час виклику "Mock_Tick", таймер спрацює один раз, але після закінчення поточного тесту цей таймер буде черговим ще 99 разів, по одному на перевірку, оскільки вважає, що пропустив 100 викликів.
Згодом ми розглянемо деякі методи налагодження вищого рівня, щоб ви могли легше читати та писати підроблені пакети, а не по одному компоненту, як у цьому коді, але це вводить основи, на яких побудовано все інше. Сподіваємось, це полегшить розробку та тестування коду для всіх (і забезпечить також стабільність сервера).
Також завтра ми підемо набагато більше про розробку API для open.mp. Він повністю сумісний із SA:MP, та це не означає, що паралельних можливостей для вдосконалення немає.