深入淺出SagaECO RPC篇(二) “封包結構”

以下討論基於SagaECO svn 900版本
源代碼可以從這裡下載

注意: 被分發SagaECO源代碼並不代表你被授權使用、修改、發布衍生的程式
換言之SagaECO團隊仍然保留一切權利(雖然SagaECO團隊早就消失了…

這篇主要討論繼承自Packet類的各個封包
遊戲客戶端和伺服器透過不同封包來傳遞不同的信息,有玩家出現的通知,有組隊邀請的請求,有聊天信息通知…是聯繫世界中的一切的基礎。SagaECO這個ECO私服也不是例外。

這次我們從伺服器通知某人作出了坐下動作的封包入手。

動作示意圖
取自raifki0527@mottri

每一個封包有對應的ID,這時要找的ID是121C。

除了ECore之外,se-a Packet Struct Tools也能查看封包結構。
*站主曾經封鎖日本以外的IP,必要時請使用VPN。

可見兩者的記載並不吻合,在這時候建議使用Se-a的結構,它上面的資料是最新的。如果沒有理解錯誤的話,se-a的網站是從2017年起由大家分享封包紀錄。
對於121C封包,舊的封包結構不兼容於新客戶端,新客戶端試圖讀取新增的第二個DWORD時舊封包就不夠長了。更常見的不兼容狀況有類型變更,例如金錢從DWORD延長為QWORD,也會導致上述的舊封包不夠長而失效。比較另類的變更涉及OpCode的變更。

兩個工具各有優缺點,整理成如下:
Ecore
優點
– 對於資料的注釋較多、齊全、而且有提供相關連封包和流程
– 是除了代碼之外唯一有覆蓋網絡層資料的文獻
缺點
– 資料比較舊 部分資料不適用於新版客戶端
新系統例如AAA之類 很可能會沒有相關資料
– 資料失傳

Se-a dev
優點
– 覆蓋最新版本封包
– 可以透過Packet Viewer上傳特殊格式的封包紀錄並更新結構
缺點
– 自客戶端發出的封包資料較為缺乏
– 網站的配色很謎

接下來我們看看SagaECO的121C封包。

很遺憾地,代碼的版本更舊。我們這次要為它更新到最新版本。而且我們在上一篇也修改了Packet類呢。
反覆向新版客戶端發送封包測試後,新增的DWORD被認為對應重播動作的速度。數值愈大,速度愈快。而unknown仍然被稱為unknown,它的存在就是一個謎…

時間關係,這裡是修改好的版本。來玩找不同吧~

  • data = new byte[9]; —> data = new byte[14]; 變更
  • Unknown = 0; 新增
  • public int MotionSpeed { set => PutUInt(value, 9); } 新增
  • public byte Unknown { set => PutByte(value, 13); } 新增

馬上來詳細描述一下修改,請務必要堅持住 > <

  1. 預留空間予新增長度為4的DWORD,data長度+4
  2. 新增前最後一個數據是PutByte(value, 8) 因此我們put在8+1=9
  3. 預留空間予要新增長度為1的BYTE,data長度+1
  4. 剛好新增完的最後一個數據是PutUInt(value, 9) 因此我們put在9+4=13
  5. Unknown預設是0 在建構子中替它賦值

正當你看完解說,正要把代碼複製貼上的時候~~~
你已經死了~ 編譯不通過~ (才不是為了防止你們在Stack overflow上面抄代碼呢)
仔細看看, public int MotionSpeed { set => PutUInt(value, 9); } 前面定義的int後面嘗試把它當成uint寫呢,不容許數據損失的Implicit Cast的C#可是很佛心的哦 😛
你是不是很想翻桌呢。那麼,請容我再一次歡迎你來到SagaECO。

你也許會問,我可以不計數嗎? Packet類不是提供了不用傳Offset的PutInt方法嗎? 答案是不可以。賦值的先後次序影響了封包寫入的先後順序,語義完全相同的代碼結果卻相異。

你會發現這個屬性的存取子賦值並不普通,它帶有副作用。若Loop屬性被重複賦值兩次,在沒有指定Offset是8的情況下,將會寫入兩次,此時再寫MotionSpeed的話,寫入會越界而跳出錯誤,這完全並非是我們所想的。

讓我們看一下另一個封包。比卡超,就決定是你了!

05F4: ABYTE(U)|DWORD
用途: 提示玩家的輸入數值給NPC的方框

from Se-a, ABYTE(U) 表記等同 Ecore的TSTR
輸入方框示意圖
取自SnowFow@巴哈

回想起第一次看到這東西的時候,我的大腦是拒絕的。是這樣的,字串的長度我們事先是不知道的。設定Title的時候我們才根據字串的長度來擴張data,最低限量地搬移數組。同樣的代碼不知道重覆了多少遍…
再者,有發現問題嗎? 即使Type屬性有顧及前面的可變長度的Title,若是先寫Type,再寫Title,Title會無情地覆寫了Type…
在這時候 Packet.cs應負全責實作PutString方法~

這個簡化方法會自動擴張data數組,而且不會覆寫掉已有的內容 (雖然並不能解決上述代碼中先寫Type後寫Title的問題)。寫好了以後可以先睡一覺。
第二天你起床了,誒,是不是似曾相識?

一個能自動擴張容量的數組…不就是List<byte>嗎?
一個能自動擴張容量的byte數組…不就是MemoryStream嗎?

斷斷續續我花了不知道多少個月才突破盲腸,也許比起空白10年要好; 也許上一篇你已經有 Packet.cs是個毒瘤的感覺,現在可以向你確認,Packet.cs就是個毒瘤XD。

從前我的代碼嗅覺並不敏銳,在摸索的階段每句代碼對我來說都深不見底。至於向你論證毒瘤的過程,我苦思良久,愈是想得愈多,我愈有所得著。我決定嘗試從 Packet父類和各封包子類的繼承關係入手。

Packet父類和各封包子類的繼承關係是如何確立的? 從兩方面

(一) Packet Class定義了幾個虛方法,試圖運用多態
public virtual bool SizeIsOk
public virtual Packet New()
public virtual void Parse(Client client)
我們並未涉獵上述部分,因為只有閱讀進來的封包時才會使用。=

(二) Packet Class試圖運用Subclass sandbox,提供很多封包讀寫方法供子類使用
這其實是不必要的。各封包的最大作用無非是提供了四個重要資訊:ID、資料的類型、名字、先後次序。

因此雖然各封包的存在是為了序列化提供必要的資訊,作為DTO卻不需要基於二進制資料。序列化可以一次性進行,而如何序列化是可以交由另一個類別去處理。

由於由各封包定義寫入次序,調用的時候有點奇怪,看起來就像stream被packet序列化似的。無論如何,修改後的代碼不用再管理各種偏移量,使用者也不需要擔心賦值的先後順序居然導致截然不同的結果。宏觀地說,封包結構不再綁定二進制數組,職責更為單一。反映到實際上它方便了我們更新封包結構和實作新的封包。

本篇在此結束。 這系列會繼續回顧我大約兩年前經歷的一些想法,在RPC篇還會最少涉及多三篇內容。

4

2 Responses to “深入淺出SagaECO RPC篇(二) “封包結構”

  • 請問up主 還有s-ea的封包紀錄嗎。? 現在網站已經開不上去了 還有ecore的2013版本 在那邊能夠找到呢 感謝

Trackbacks & Pings

Leave a Reply

Your email address will not be published. Required fields are marked *