Testing in open.mp
Kiểm thử trong SA:MP không dễ chút nào. Phần lớn chỉ là kết nối với máy chủ và đặt bản in ở mọi nơi. Các thư viện như y_testing giúp viết các bài kiểm tra đơn vị dễ dàng hơn nhiều, nhưng chỉ dành cho mã khá "thuần túy" - tức là mã chạy trên máy chủ mà không có nhiều tương tác của người chơi. Bạn có thể kiểm thử một lượng hành vi đáng ngạc nhiên như thế này, nhưng vẫn còn hạn chế và không giúp thu hẹp các vấn đề khi chúng xuất hiện.
Vậy open.mp giải quyết vấn đề này như thế nào? Trước hết, mã là mã nguồn mở, vì vậy bạn có đầy đủ các công cụ gỡ lỗi hiện có như GDB và Visual Studio để bạn có thể thực hiện từng bước và kiểm tra mã. Chúng tôi cũng có kế hoạch thêm các công cụ tương tự cho chính các tập lệnh pawn.
Nhưng bổ sung lớn nhất là thư viện "giả". Rất đơn giản, điều này cho phép bạn tạo ra những người chơi giả hoạt động giống hệt như người chơi thật, nhưng không có bất kỳ máy khách nào được kết nối. Vì vậy, bất kỳ hành động nào được thực hiện trên họ bởi một tập lệnh đều có thể được đọc và phân tích bởi một tập lệnh khác. Chúng ta hãy lấy ví dụ đơn giản nhất có thể cho bản trình diễn đầu tiên - hiển thị cho người chơi một điểm kiểm tra và xác nhận rằng điểm đó đã được gửi. Với y_testing, bạn thực hiện hành động rồi phải hỏi người chơi xem hành động đó có xảy ra không, việc này rất chậm, không thể lặp lại và khá nhàm chán:
PTEST__ SetPlayerCheckpoint(playerid)
{
SetPlayerCheckpoint(playerid, 0.0, 0.0, 4.0, 5.0);
ASK("Do you see a checkpoint in the middle of the world?");
}
Sử dụng thư viện như "Pawn.RakNet" có thể loại bỏ câu hỏi này bằng cách tự động kiểm tra các gói tin gửi đi, nhưng vẫn yêu cầu trình phát được kết nối nên không thể tự động hóa và lặp lại:
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;
}
Với open.mp, chúng ta có một API tương tự như của Pawn.RakNet, nhưng với những người chơi giả. Vì vậy, bạn tạo một người chơi, không có phiên bản trò chơi, và sử dụng nó chính xác như một người chơi bình thường:
TEST__ SetPlayerCheckpoint()
{
new playerid = Mock_CreatePlayer("Mr Mock");
// Clear all existing packets, for ease of searching.
Mock_PurgeSent(playerid);
// Show a checkpoint normally.
SetPlayerCheckpoint(playerid, 0.0, 0.0, 4.0, 5.0);
// Check a "SetCheckpoint" packet was seen.
new MockPacket:packet = Mock_GetPacket(playerid, "SetCheckpoint");
ASSERT(packet);
if (!packet) return;
// Check the packet has 4 components, each 32 bits.
ASSERT(MockPacket_Components(packet) == 4);
ASSERT(MockPacket_Bits(packet) == 4 * 32);
// Check the various components.
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);
}
Mã này hoàn toàn có thể kiểm tra lặp lại, độc lập (không có biến toàn cục và lệnh gọi lại bổ sung) và dễ dàng mở rộng.
Nhưng đôi khi không đơn giản như vậy. Nếu bạn tạo một đối tượng, bạn không thể chỉ kiểm tra xem nó đã được gửi ngay lập tức đến người chơi hay chưa, vì họ có thể không ở gần nó và do đó, trình phát trực tuyến tích hợp vẫn chưa gửi cho họ. Để giải quyết vấn đề này, trước tiên chúng ta cần đi sâu hơn một chút để giải thích cách đồng bộ cơ bản hoạt động đối với những thứ như SetPlayerPos
. Khi bạn đặt vị trí của người chơi bằng SetPlayerPos
, máy chủ KHÔNG cập nhật vị trí của họ nội bộ và cho tất cả những người chơi khác ngay lập tức. Thay vào đó, lệnh SET POSITION
được gửi đến người chơi đang di chuyển, họ được dịch chuyển đến vị trí mới, sau đó báo cáo vị trí mới trở lại máy chủ trong gói đồng bộ tiếp theo của họ. Lý do về cơ bản là độ trễ - có thể có một hoặc nhiều gói đồng bộ vẫn đang bay với vị trí cũ sau khi đặt vị trí mới. Vậy điều này có ý nghĩa gì đối với người chơi giả lập? Điều đó có nghĩa là bạn không thể thực sự thiết lập vị trí của họ, ít nhất là không theo cách thông thường. Không có máy khách thực sự, vì vậy không có gì nhận được gói tin (tức là lệnh set position) và không có gì đồng bộ hóa dữ liệu đó trở lại máy chủ một lần nữa.
Chúng ta có thể tạo bất kỳ dữ liệu đồng bộ hóa giả nào cho máy chủ từ những người chơi giả, vì vậy đó là một cách để cập nhật vị trí của họ, nhưng có thể hơi cồng kềnh đối với chỉ một thao tác phổ biến, vì vậy có Mock_SetPlayerPos
cho chính xác trường hợp sử dụng này. Nhưng đây vẫn chưa phải là toàn bộ câu chuyện, vì chúng ta cần streamer chạy và cập nhật với vị trí mới của họ, điều này chỉ xảy ra theo định kỳ (cứ sau vài mili giây, tùy thuộc vào tốc độ tích tắc của máy chủ). Đối với điều này, chúng ta có một hàm giả khác - Mock_Tick
, hàm này sẽ nhảy thời gian của máy chủ về phía trước một số micro giây nhất định (KHÔNG phải mili giây). Điều này hoàn toàn không nên được sử dụng trên máy chủ trực tiếp vì nó có thể tạo ra đủ loại sự kỳ lạ với bộ đếm thời gian và mã nhạy cảm với thời gian khác, nhưng có thể được sử dụng để làm giả thời gian trôi qua trong các bài kiểm tra. Lưu ý rằng Mock_Tick(10000)
sẽ không trì hoãn trong 10ms, thay vào đó, nó sẽ ngay lập tức nhảy thời gian về phía trước 10 mili giây.
TEST__ SetPlayerCheckpoint()
{
new playerid = Mock_CreatePlayer("Mr Mock");
// Clear all existing packets, for ease of searching.
Mock_PurgeSent(playerid);
// Create an object.
CreateObject(1337, 100.0, 100.0, 4.0);
// Fake the mock player's position updating to the server, near the object.
Mock_SetPlayerPos(playerid, 105.0, 105.0, 4.0);
// Now FAKE pass some time, so the streamer can run (50ms should do).
Mock_Tick(50000);
// Check a "CreateObject" packet was seen.
new MockPacket:packet = Mock_GetPacket(playerid, "CreateObject");
ASSERT(packet);
if (!packet) return;
// Check the packet has 4 components, each 32 bits.
ASSERT(MockPacket_Components(packet) > 5);
// Check some of the packet components.
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);
}
Một lưu ý quan trọng về Mock_Tick
. Nếu bạn gọi nó là Mock_Tick(100000)
và chạy bộ đếm thời gian 1ms, điều này sẽ ngay lập tức kích hoạt bộ đếm thời gian chạy, nhưng bộ đếm thời gian sẽ nghĩ rằng nó đã bỏ lỡ thời gian của mình rất lâu (100ms) và sẽ tự xếp hàng lại. Trong khi gọi đến Mock_Tick
, bộ đếm thời gian sẽ kích hoạt một lần, nhưng sau khi thử nghiệm hiện tại kết thúc, bộ đếm thời gian sẽ được xếp hàng thêm 99 lần nữa, mỗi lần một tích tắc vì nó nghĩ rằng nó đã bỏ lỡ 100 lần gọi.
Sau này, chúng ta sẽ đề cập đến một số kỹ thuật gỡ lỗi cấp cao hơn để bạn có thể đọc và viết các gói tin giả dễ dàng hơn, thay vì từng thành phần một như trong mã đó, nhưng điều này giới thiệu những điều cơ bản mà mọi thứ khác được xây dựng dựa trên. Hy vọng rằng nó sẽ giúp việc phát triển và thử nghiệm mã dễ dàng hơn cho mọi người (và đảm bảo máy chủ cũng ổn định hơn).
Chúng ta cũng sẽ đi sâu hơn vào thiết kế API cho open.mp vào ngày mai. Nó hoàn toàn tương thích ngược với SA:MP, nhưng điều đó không có nghĩa là không có chỗ để cải thiện song song.