深入淺出SagaECO RPC篇(二) “封包結構”
以下討論基於SagaECO svn 900版本
源代碼可以從這裡下載
注意: 被分發SagaECO源代碼並不代表你被授權使用、修改、發布衍生的程式
換言之SagaECO團隊仍然保留一切權利(雖然SagaECO團隊早就消失了…
這篇主要討論繼承自Packet類的各個封包
遊戲客戶端和伺服器透過不同封包來傳遞不同的信息,有玩家出現的通知,有組隊邀請的請求,有聊天信息通知…是聯繫世界中的一切的基礎。SagaECO這個ECO私服也不是例外。
這次我們從伺服器通知某人作出了坐下動作的封包入手。
每一個封包有對應的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); }
新增
馬上來詳細描述一下修改,請務必要堅持住 > <
- 預留空間予新增長度為4的DWORD,data長度+4
- 新增前最後一個數據是
PutByte(value, 8)
因此我們put在8+1=9 - 預留空間予要新增長度為1的BYTE,data長度+1
- 剛好新增完的最後一個數據是
PutUInt(value, 9)
因此我們put在9+4=13 - 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
from Se-a, ABYTE(U) 表記等同 Ecore的TSTR
用途: 提示玩家的輸入數值給NPC的方框
取自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篇還會最少涉及多三篇內容。
請問up主 還有s-ea的封包紀錄嗎。? 現在網站已經開不上去了 還有ecore的2013版本 在那邊能夠找到呢 感謝