Support OpenKore:
Learn about
the Fund Pool

網路子系統

English | 正體中文

Contents


OpenKore 網路子系統粗略地包含了以下幾個類別:

Network 類別

這是 Connection Manager(連線管理器)。它管理 RO 伺服器的 TCP/IP socket 連線,以及處理像是連線、斷線、傳送資料至伺服器、由伺服器接受資料...等事務。但它不做太多其它事。

以圖表來說明,它長得像這樣:

關於連線到 RO 伺服器,它產生兩個物件:

Network::ReceiveNetwork::Receive::ServerTypeX 類別

這兩個是 Message parser 的類別。它們剖析了通過 Connection Manager 的 message,從 message 中萃取有用的資訊,然後存放這些資訊在全域變數裡。其它的 OpenKore 模組(像是 AI)使用全域變數裡的資訊來處理許多的事。

注意不管是 Connection Manager 類別或是 Message parser 類別都不應該包含有任何 AI 相關的程式碼。

Network::SendNetwork::Send::ServerTypeX 類別

這兩個是 message 傳送的類別。它們封存 RO 網路 message 到一個簡單、易於使用的功能函數,如此一來你就不必在 OpenKore 靜止時動手寫位元組控制的程式碼。


處理多伺服器類型(message 剖析的部分)

在 2003 年以前,事情很簡單:你只有 iRO(國際的)與 kRO(韓國的)可以選,而且就這樣了。但今天有超過 5 種官方的 RO 伺服器(巴西、菲律賓、歐洲、蘇俄等等)與上百個私服。所有的伺服器的通訊協定都有些微的不同。假如針對每一種伺服器的通訊協定來產生不同版本的 Kore 的話就太沒效率了,因為雖然通訊協定有所不同,它們並不會相差太遠。這就是為什麼 OpenKore 必須要能支援所有(至少有許多)不同的 RO 伺服器的通訊協定類型。

因此我們引入了 server type 的觀念。也許你曾經在 servers.txt 看過 "serverType" 這個選項。serverType 所指定的數字指出了伺服器是使用哪一種的通訊協定。舉例來說,這個時候(2006 年 12 月 20 日)我們有:

  • Server type 0 用於 iRO、pRO、AnimaRO 與許多其它的伺服器。
  • Server type 8 用於 kRO。
  • Server type 10 用於 vRO。
  • ...etc...

以上並非完整的伺服器類型列表。在撰寫本頁時,我們已經有 15 種伺服器類型了。

Network::ReceiveNetwork::Receive::ServerType0 類別包含了剖析伺服器類型為 0 的通訊協定的程式碼。每個伺服器類型都有屬於 Network::Receive 的一個子類別。每個子類別都特化成專門用來剖析該類型的伺服器通訊協定。類別階系圖長得像這樣:

當 Connection Manager 連上伺服器後,它會去查閱(藉由使用 servers.txt 內的資訊)哪一個伺服器類型是與這個伺服器有關聯的,然後對這個伺服器使用符合的 Message parser 類別。

細節補充

假如你在 Network::Receive 看到 'new' 這個方法,那麼你會看到一些像下面的東西:

$self{packet_list} = {
    '0069' => ['account_server_info', 'x2 a4 a4 a4 x30 C1 a*', [qw(sessionID accountID sessionID2 accountSex serverInfo)]],
    '006A' => ['login_error', 'C1', [qw(type)]],
    ...

$self{packet_list} 是個對映一個 message ID(有時也稱做 packet switch)至一個陣列的雜湊,該陣列擁有以下資訊:

  • 此功能函數將會剖析 message。在以上的程式碼片段中,'account_server_info' 是該功能函數的名稱,將會處理該 message。
  • 該 message 看起來如何,換言之,就是 message 的結構。這些有趣的字串像是 'x2 v1' 之類的,會通過 Perl 的 unpack() 功能函數。假如你不清楚它們代表的意義,請閱讀 這裡這裡
  • 變數名稱列表伴隨著 message 結構而來。它詳細說明了對於結構來說有哪些變數名稱,變數名稱與 unpack 字串排列次序相同。

在子類別中,你可以隨你的意願來修改雜湊。你也可以無視處理功能函數而去做其它的事,端視於你想要它對於該伺服器類型有什麼樣的表現。

以下的 arguments 會通過處理功能函數:

  • The packet parser instance.
  • 被當做雜湊參照的 message 變數,我們稱之為 message arguments

在上面的例子中,account_server_info 看起來會像這樣:

sub account_server_info {
    my ($self, $args) = @_;
    # $args->{sessionID} can be accessed.
    # $args->{accountID} can be accessed.
    # ...etc...
}

這個 message arguments 雜湊有兩個特別的項目:

$args->{switch}
這個包含了目前 message 的 ID(識別碼)。
$args->{RAW_MSG_SIZE}
目前原始 message 的長度(以位元組來計算)。
$args->{RAW_MSG}
原始的、完整的 message,包含了 message ID。假如 message 的結構無法用一個 unpack 字串被描述(在 packet_list中),那麼你可能要使用這個變數來動手處理一下這個資料。注意:RAW_MSG 可能比目前的 message 包含較多的資料。它也可能包含有(部份的)下一筆的 message。通常來說,不要處理超過 RAW_MSG_SIZE 個位元組的長度。

範例 1:加入一個新的 message 處理

舉例來說,我們假設 RO 伺服器現在加入了一個新的封包叫做 "你正在跳舞",這個 message 也告訴你你正在跳舞的確切位置與舞步的種類。假設這個 message 長這樣:

  • 它的 message ID 是 "1234"。
  • 這個 message 有三個定義域在裡面:一個 16-bit 的整數來描述 X 座標,另一個 16-bit 的整數來描述 Y 座標,與一個 1-byte 的定義域以告知你正在跳的舞步種類。

然後你可以加入以下細項至 $self{packet_list} 雜湊:

'1234' => ['dancing', 'v v C', qw(x y type)]

這裡的 'v v C' 告知了這個 message 包含了一個 16-bit 的整數 ('v'),緊跟著另一個 16-bit 的整數 ('v'),再緊跟著一個 1-byte 的字元 ('C')。'qw(x y type)' 這個部分告知了第一個定義域應該要用 'x' 做為它的名稱,第二個定義域應該要用 'y' 做為它的名稱,第三個定義域應該要用 'type' 做為它的名稱。

你也要加入以下的 message 處理功能函數至 Network/Receive.pm:

sub dancing {
    my ($self, $args) = @_;
    message "I am dancing on position ($args->{x}, $args->{y})! My dance type is $args->{type}\n";
}

這樣就好了!至於其它的事會由 framework 自動處理。

範例 2:處理一個不一樣的伺服器類型

我們假設伺服器類型為 12 的伺服器對於 "你正在跳舞" 的封包有了些微的改變,它的 X 座標與 Y 座標被調換了。也就是說,Y 座標之後緊跟著的是 X 座標(而不是先 X 後 Y)。你可以修改 packet_list 雜湊以更新它的 message 結構。所以在 '新的' Network::Receive::ServerType12 功能函數中,你可以寫成這樣:

sub new {
    my ($class) = @_;
    my $self = $class->SUPER::new;

    # BEGIN: YOUR CODE
    $self->{packet_list}{1234}[2] = [qw(y x type)];
    # END: YOUR CODE

    return $self;
}

假如你的 Perl 不夠好:recall that $self->{packet_list}{1234} 看起來像是這樣:

['dancing', 'v v C', qw(x y type)]

然後你希望修改它的變數名稱列表 qw(x y type),此列表在整個陣列中的索引值為 2。所以你可以寫成

$self->{packet_list}{1234}[2] = [qw(y x type)];

現在,$self->{packet_list}{1234} 看起來會是這樣:

['dancing', 'v v C', qw(y x type)]


處理多個伺服器類型(message sender 部分)

在 message sender 部分對多個伺服器類型的處理幾乎與 message parser 部分相同。類別的階系圖看起來像是這樣:

不同的是伺服器類型 0 的處理程式碼是在類別 Network::Send::ServerType0 中。所有其它的 Network::Send::ServerTypeX 子類別是從 Network::Send::ServerType0 繼承而來的。

有趣的是,伺服器通訊協定類型間大部分的相異處都在傳送的部分!我們從不同伺服器類型的伺服器所接收的 message 並不會相差太多,相較之下,我們要傳送到伺服器的 message 的差異性卻大得多了。


使用 message sender

就像 message parser 物件一樣,message sender 物件也由 connection manager 所產生。這個物件是存放在全域變數的 $messageSender 裡。要傳送 message 至伺服器端,請寫:

$messageSender->sendFoo();

每一個 message sender 功能函數名稱都有一個前置詞 'send'。請參見 Network::Send::ServerType0 的傳送功能函數的列表。舉例來說,假如你想要傳送 "公頻" 的 message 到 RO 伺服器,那麼請寫:

$messageSender->sendChat("hello there");

相容性注意

本頁物件導向的 message sender 結構(如前所述)是在 2006 年 12 月 20 日被引入的。在較早期的 OpenKore 版本中要傳送 message 給伺服器的話,你得用以下其中一種方法來寫:

# OpenKore 1.6 與 1.9.0-1.9.2 的語法
sendFoo(\$remote_socket, args);
# OpenKore 1.9.0-1.9.2 的語法
sendFoo($net, args);
$net->sendFoo(args);

但這些將無法繼續運作了。取代的寫法要寫成:

$messageSender->sendFoo(args);

或者,假如你的 plugin 必須要與 1.6 版相容的話請寫這樣:

if (defined $Globals::messageSender) {
    $Globals::messageSender->sendFoo(args);
} else {
    sendFoo(\$remote_socket, args);
}


Hooks

message sender 類別提供了一些 hooks 可允許 plugins 處理這些 messages。

"packet_pre/$HANDLER_NAME"

此處,$HANDLER_NAME 是處理功能函數的名字,詳載於 packet_list 雜湊中。舉例來說,"packet_pre/account_server_info"。

這個 hook 會在處理功能函數被呼叫前被呼叫。但這個 hook 只有在對於目前封包有處理功能函數時被呼叫。給予這個 hook 的 argument 是一個包含 message 定義域(the message arguments)的陣列。

"packet/$HANDLER_NAME"

這個 hook 會在處理功能函數被呼叫後被呼叫,或是對於目前的 message 沒有處理功能函數時被呼叫。它的 argument 是該 message 的 arguments。


附錄 A:Ragnarok Online 通訊協定的介紹

Ragnarok Online 使用 TCP 做為它的傳輸通訊協定。每個 RO 伺服器傳送至客戶端的 message 有以下的格式:
一個 message 由最少一個定義域構成:message ID(message 識別碼)(也叫做 packet switch,但 "message ID" 比較容易理解)。這個 message ID告訴客戶端這是哪一種類型的 message。靠著 message ID,我們才知道要如何去解釋這個 message。

有兩種類型的 messages:

固定長度的 messages
這些 messages 總是有著同樣的長度。這種 message 的例子像是 "某人發出一個表情符號" 的 message 與 "某怪物出現了" 的 message。
變動長度的 messages
這些 message 的長度隨著它們的內容而有所不同。因為它們的長度會變,所以它們有一個特別的 message length 定義域,可告訴客戶端程式 message 的確切長度是多少。這個長度指的是包括 message ID 在內的全部 message 的長度。像 "您傳送了公頻訊息" 的 message 就是一個變動長度 message 的例子。

最後,我們有 message arguments。arguments 裡的確切內容依賴 message 本身。舉例來說,"某人發出一個表情符號" 的 message 擁有以下資訊在它的 message arguments 裡:

  • 發送這個表情符號的角色 ID(角色識別碼)。
  • 所發送的表情符號的類別。

附錄 B:recvpackets.txt 與處理 message lengths

當我們經由 RO 伺服器的 socket 接收到資料時,我們無法假設每次我們從 socket 讀到的是否為確切的 1 個完整的 message。我們可能接受到部分的 message,或是兩個的 messages,或是一個完整的 message 及一個不完整的下一筆的 message。這就是為什麼我們必須把從 RO 伺服器接收過來的資料先放在一個 緩衝區。當我們確定我們已接收到至少一個完整的 message 時,我們會開始處理該 message 並把它從緩衝區移走。然後我們持續等待,直到我們已知我們又接收到了另一個完整的 message,依此類推。

但我們要怎麼知道一個 message 是否是完整的?為了要知道這件事,我們必須知道每一個 message 的確切長度。這就是 recvpackets.txt 的來由:它詳細敘述了哪一個 message 有哪個長度。舉例來說,recvpackets.txt 有一行是這樣的:

00C0 3

這表示有著識別碼 "00C0" 的 message 是一個固定長度的 message,且長度為 3。但有時你也會看到像這樣的情形:

00D4 0

0 表示長度是變動的,所以在這個例子裡它代表 00D4 這個 message 是一個變動長度的 message。如附錄 A 所提,變動長度的 message 會有一個 message length 的定義域,可告訴我們該 message 的長度是多少。