GCBBSMessages 是当前项目里用于下发摊位留言板内容的变长下行包。它既是 CGBBSApply 读取留言板时的直接回包,也是 CGBBSSychMessages 在新增、回复、删除、改标题后触发的统一刷新包。
包信息
| 项目 |
内容 |
| 封包 ID |
724 |
| 包名 |
GCBBSMessages |
| 方向 |
GC 下行 |
| 相关请求 |
CGBBSApply、CGBBSSychMessages |
| 作用 |
同步指定摊位的留言板版本号、标题与最多 20 条留言 |
包体结构
当前 Game.exe 与 Lua 都能确认,这条包是动态长度结构,实际长度由标题和每条留言正文决定。
size = 10 + title_len + sum(entry_size)
entry_size =
47 + message_len // has_reply == 0
50 + message_len + replymessage_len // has_reply == 1
其中固定 10 字节对应:
m_objID:4
serial:4
title_len:1
num:1
头部
| 偏移 |
长度 |
字段 |
类型 |
含义 |
0x00 |
0x04 |
m_objID |
int32 |
目标摊位所属角色对象 ID |
0x04 |
0x04 |
serial |
uint32 |
当前留言板版本号 |
0x08 |
0x01 |
title_len |
uint8 |
标题字节长度 |
0x09 |
title_len |
title |
byte[] |
GBK 标题内容 |
0x09 + title_len |
0x01 |
num |
uint8 |
留言条数,当前客户端认可上限 20 |
0x0A + title_len |
... |
messages |
entry[num] |
留言数组 |
单条留言结构
设单条留言起始偏移为 entry_base。
| 偏移 |
长度 |
字段 |
类型 |
含义 |
entry_base + 0x00 |
0x27 |
author |
char[39] |
作者名固定槽位 |
entry_base + 0x27 |
0x04 |
id |
uint32 |
留言 ID |
entry_base + 0x2B |
0x01 |
hour |
uint8 |
留言小时 |
entry_base + 0x2C |
0x01 |
min |
uint8 |
留言分钟 |
entry_base + 0x2D |
0x01 |
has_reply |
uint8 |
是否附带回复,0/1 |
当 has_reply == 0 时,后续结构为:
| 偏移 |
长度 |
字段 |
类型 |
含义 |
entry_base + 0x2E |
0x01 |
message_len |
uint8 |
留言正文长度 |
entry_base + 0x2F |
message_len |
message |
byte[] |
GBK 留言正文 |
当 has_reply == 1 时,后续结构为:
| 偏移 |
长度 |
字段 |
类型 |
含义 |
entry_base + 0x2E |
0x01 |
re_hour |
uint8 |
回复小时 |
entry_base + 0x2F |
0x01 |
re_min |
uint8 |
回复分钟 |
entry_base + 0x30 |
0x01 |
message_len |
uint8 |
留言正文长度 |
entry_base + 0x31 |
message_len |
message |
byte[] |
GBK 留言正文 |
entry_base + 0x31 + message_len |
0x01 |
replymessage_len |
uint8 |
回复正文长度 |
entry_base + 0x32 + message_len |
replymessage_len |
replymessage |
byte[] |
GBK 回复正文 |
补充说明:
title_len、message_len、replymessage_len 在协议上都是 uint8,单段文本理论上最高 255 字节。
- 当前
Game.exe 在本地包类中为标题、正文、回复正文各预留了 300 字节缓冲,但线上主链实际长度仍以 uint8 为准。
- Lua
bis() 把 serial 读作 uint32,bos() 则用 writeint() 写出同样的 4 字节;当前版本号只做正向递增,线上行为等价。
当前实际行为
- 当前
Game.exe 的 GetPacketID() 返回 724。
- 当前
Game.exe 的 Read() / Write() 顺序与 Lua packet.GCBBSMessages 一致:先读写 m_objID + serial + title_len + title + num,再逐条处理留言数组。
- 当前
Game.exe 的 GetPacketSize() 会按标题长度与每条留言是否带回复动态累加,不是固定长度包。
- 场景服
char_bbs_apply() 会先检查目标摊位是否处于 STALL_OPEN。若未开启,不会下发本包,而是返回 GCStallError(574)。
- 这条包始终会携带最新
serial。如果客户端请求里的 serial 与服务端当前版本一致,Lua 仍会发送 GCBBSMessages,但内容只剩:
m_objID
serial
title_len = 0
num = 0
- 因此当前最小包长不是
8,而是 10 字节。
- 当
serial 不一致时,Lua 会把留言板标题和留言列表完整写入协议。
- 留言底层由
stallbbs 的 20 槽位环形缓冲维护,char_bbs_apply() 从 final_index 开始按环形遍历有效槽位,所以客户端最终拿到的是当前有效留言的“从旧到新”顺序。
CGBBSSychMessages 在新增留言、回复留言、删除留言、改标题后,都会构造 ba.serial = 0 再回调 char_bbs_apply(),强制下发完整 GCBBSMessages。
stallbbs.serial 初始为 0,stallbox 初始化默认标题“欢迎你来到某某的摊位”时会先触发一次 set_bbs_title(),所以有效摊位通常从非零版本号开始。
- 当前
Game.exe Execute() 不只是空返回:
- 若
m_objID 等于本地角色对象 ID,则走自身摊位留言板分支,并用 serial 与本地缓存比对;版本相同则直接结束,不重灌列表。
- 若
m_objID 不等于本地角色对象 ID,则走查看他人摊位的留言板分支,会直接用收到的数据刷新本地列表、标题和数量。
- 两个分支最后都会向界面层发送事件
179,字符串参数分别是 "sale" 或 "buy"。
- Lua 里这条包的
bis() 仅用于解包辅助,且存在 if num > 0 and num < 0x14 then 的严格小于判断;如果真按 Lua 本地解包 20 条留言,不会进入循环。但当前正式主链只用它向客户端下发,不依赖这段下行包的 Lua 解包路径。
服务端落点
- 协议定义:
/home/ubuntu/Game2/services/game/packet.lua
- 请求入口:
/home/ubuntu/Game2/services/msgagent.lua
- 场景处理:
/home/ubuntu/Game2/services/scene/scenecore.lua
- 留言板底层:
/home/ubuntu/Game2/lualib/stallbox.lua
关键代码
当前 Lua 协议定义
packet.GCBBSMessages = {
xy_id = packet.XYID_GC_BBS_MESSSAGES,
ctor = function(self)
self.messages = {}
end,
bis = function(self, buffer)
local stream = bistream.new()
stream:attach(buffer)
self.m_objID = stream:readint()
self.serial = stream:readuint()
local len = stream:readuchar()
self.title = gbk.toutf8(stream:read(len))
local num = stream:readuchar()
if num > 0 and num < 0x14 then
for i = 1, num do
local message = {}
message.author = gbk.toutf8(stream:read(0x27))
message.id = stream:readuint()
message.hour = stream:readuchar()
message.min = stream:readuchar()
message.has_reply = stream:readuchar()
if message.has_reply == 1 then
message.re_hour = stream:readuchar()
message.re_min = stream:readuchar()
local len = stream:readuchar()
message.message = stream:read(len)
len = stream:readuchar()
message.replymessage = stream:read(len)
else
local len = stream:readuchar()
message.message = stream:read(len)
end
table.insert(self.messages, message)
end
end
end,
bos = function(self)
local stream = bostream.new()
stream:writeint(self.m_objID)
stream:writeint(self.serial)
self.title = gbk.fromutf8(self.title or "")
local len = string.len(self.title)
stream:writeuchar(len)
stream:write(self.title, len)
local num = #self.messages
stream:writeuchar(num)
...
end
}
当前 Lua 场景发送
function scenecore:char_bbs_apply(who, ba)
local tar = self.objs[ba.m_objID]
local stall_box = tar:get_stall_box()
if stall_box:get_stall_status() ~= stall_box.STALL_STATUS.STALL_OPEN then
local msg = packet_def.GCStallError.new()
msg.result = msg.STALL_MSG.ERR_ILLEGAL
self:send2client(who, msg)
return
end
local msg = packet_def.GCBBSMessages.new()
local bbs = stall_box:get_bbs()
msg.m_objID = tar:get_obj_id()
msg.serial = bbs:get_serial()
if ba.serial == bbs:get_serial() then
else
local cur_index = bbs:get_final_index()
for i = 0, bbs.MAX_BBS_MESSAGE_NUM - 1 do
local entry = bbs:get_message_by_index(cur_index)
if entry then
table.insert(msg.messages, {
id = entry.id,
author = entry.author,
hour = entry.hour,
min = entry.min,
message = entry.message,
item_transfer = entry.item_transfer,
has_reply = entry.has_reply,
re_hour = entry.re_hour,
re_min = entry.re_min,
replymessage = entry.replymessage,
})
end
cur_index = cur_index + 1
if cur_index == bbs.MAX_BBS_MESSAGE_NUM then
cur_index = 0
end
end
msg.title = bbs:get_bbs_title()
end
self:send2client(who, msg)
end
当前 Lua 留言板底层
stallbbs.MAX_BBS_MESSAGE_NUM = 20
function stallbbs:add_new_message(message_entry)
message_entry.has_reply = false
self.message_data[self.final_index] = message_entry
self.is_vaild[self.final_index] = true
self:inc_serial()
if self.final_index >= self.MAX_BBS_MESSAGE_NUM - 1 then
self.final_index = 0
else
self.final_index = self.final_index + 1
end
return true
end
function stallbbs:set_bbs_title(title)
self.title = title or ""
self:inc_serial()
return true
end
function stallbbs:get_final_index()
return self.final_index
end
当前 Game.exe 结论
GetPacketID() = 724;
GetPacketSize() =
10 + title_len
+ sum(has_reply ? 50 + message_len + reply_len
: 47 + message_len);
Read/Write() {
i32 m_objID;
u32 serial;
u8 title_len;
char title[title_len];
u8 num;
repeat num times {
char author[39];
u32 id;
u8 hour;
u8 min;
u8 has_reply;
if (has_reply) {
u8 re_hour;
u8 re_min;
u8 message_len;
char message[message_len];
u8 reply_len;
char reply[reply_len];
} else {
u8 message_len;
char message[message_len];
}
}
}
Execute() {
if (m_objID == local_obj_id) {
if (serial != sale_bbs.serial) {
refresh_sale_bbs(title, messages, num, serial);
}
notify_ui(179, "sale");
} else {
refresh_buy_bbs(title, messages, num, serial);
notify_ui(179, "buy");
}
return 2;
}
结论
GCBBSMessages(724) 是当前项目里真实在用的摊位留言板下行同步包,对应 CGBBSApply 与 CGBBSSychMessages 两条上游请求链路。
- 它不是固定长度包,长度公式应写成
10 + title_len + Σ(entry_size),最小包长是 10 字节。
- 单条留言固定包含
author[39] + id + hour + min + has_reply,正文与回复正文都用 uint8 长度前缀描述。
- 服务端实际最多编码
20 条留言,返回顺序是当前有效留言的“从旧到新”。
serial 命中时,Lua 仍会发送 GCBBSMessages,但内容是空标题与 0 条留言;当前 Game.exe 只在“自身摊位”分支上用 serial 做短路。
- Lua 的本地
bis() 辅助对 num == 20 存在严格小于判断,但这不影响当前服务端主链的对外下发行为。