CGMail 是旧版客户端里用于发送玩家邮件的请求包。当前 Game.exe 可以确认它的变长结构与长度计算方式,当前 Lua 服务端则已经接入了完整的收信、发信、脚本邮件执行链。
包信息
| 项目 |
内容 |
| 封包 ID |
414 |
| 包名 |
CGMail |
| 相关回包 |
GCNotifyMail、GCMail、GCRetSendMail |
| 相关回包 ID |
633、273、329 |
| 作用 |
向服务端提交一封邮件,携带发件人信息、收件人名字、正文、邮件类型以及可选脚本参数 |
包体结构
当前 Game.exe 与 Lua 协议定义都能确认,CGMail 是可变长度结构,长度上限受三段字符串限制:
source_len <= 30
dest_len <= 30
content_len <= 256
主结构如下:
| 偏移 |
长度 |
字段 |
类型 |
含义 |
0x00 |
0x04 |
sender_guid |
int32 |
发件人 GUID;当前 Lua 会强校验它必须等于当前角色 GUID |
0x04 |
0x01 |
source_len |
uint8 |
发件人名字长度 |
0x05 |
source_len |
source |
char[n] |
发件人名字 |
0x05 + source_len |
0x04 |
portrait_id |
int32 |
发件人头像 ID |
0x09 + source_len |
0x04 |
extra_guid |
int32 |
附加整型字段,当前 Lua 主链未直接使用 |
0x0D + source_len |
0x01 |
dest_len |
uint8 |
收件人名字长度 |
0x0E + source_len |
dest_len |
dest |
char[n] |
收件人名字 |
0x0E + source_len + dest_len |
0x02 |
content_len |
uint16 |
邮件正文长度 |
0x10 + source_len + dest_len |
content_len |
content |
char[n] |
邮件正文 |
0x10 + source_len + dest_len + content_len |
0x04 |
mail_type |
uint32 |
邮件类型:普通 / 系统 / 脚本邮件 |
0x14 + source_len + dest_len + content_len |
0x04 |
create_time |
uint32 |
邮件时间戳 |
当 mail_type == 2 时,还会附带 5 个脚本参数:
| 偏移 |
长度 |
字段 |
类型 |
含义 |
0x18 + source_len + dest_len + content_len |
0x04 |
script_param_1 |
int32 |
脚本邮件参数 1 |
0x1C + source_len + dest_len + content_len |
0x04 |
script_param_2 |
int32 |
脚本邮件参数 2 |
0x20 + source_len + dest_len + content_len |
0x04 |
script_param_3 |
int32 |
脚本邮件参数 3 |
0x24 + source_len + dest_len + content_len |
0x04 |
script_param_4 |
int32 |
脚本邮件参数 4 |
0x28 + source_len + dest_len + content_len |
0x04 |
script_param_5 |
int32 |
脚本邮件参数 5 |
包尾还有 1 个额外整型字段:
| 偏移 |
长度 |
字段 |
类型 |
含义 |
0x18 + source_len + dest_len + content_len |
0x04 |
client_tag |
uint32 |
当 mail_type != 2 时出现的尾字段,当前 Lua 主链未直接使用 |
0x2C + source_len + dest_len + content_len |
0x04 |
client_tag |
uint32 |
当 mail_type == 2 时出现的尾字段,当前 Lua 主链未直接使用 |
当前 Game.exe 的长度计算方式可整理为:
mail_type != 2: source_len + dest_len + content_len + 28
mail_type == 2: source_len + dest_len + content_len + 48
当前实际行为 / 模式说明
- 当前
Game.exe 中,CGMail::GetPacketID() 返回 414。
- 当前
Game.exe 的 Read() / Write() 顺序与当前 Lua packet.CGMail.bis() 一致:
sender_guid
source_len + source
portrait_id
extra_guid
dest_len + dest
content_len + content
mail_type
create_time
script_param_1..5(仅 mail_type == 2)
client_tag
- 当前
Game.exe 的 Execute() 对客户端侧是空处理,直接返回 2。
- 当前 Lua 请求入口
request:CGMail() 只做一条硬校验:
assert(self.guid == my_guid, self.guid)
- 校验通过后,请求会直接转发到世界服:
skynet.call(".world", "lua", "send_mail", self)
- 当前世界服
worldcore:send_mail() 的实际处理流程是:
- 空正文直接拒绝
- 按
dest 查找角色名字
- 找到目标角色后把整封邮件写入角色
mail_list
- 目标在线时再通知其 agent 触发
new_mail
- 当前邮件收取链路是:
CGAskMail(386) 请求邮箱
- 登录态或新邮件提示走
GCNotifyMail(633)
- 具体邮件内容走
GCMail(273)
- 当前 Lua 还把
mail_type == 2 视为脚本邮件:
- 这类邮件不会进入普通收件箱列表
- 会在
msgagent:get_db_mail_list() 中被提前取出
- 随后调用场景服
execute_script_mail
- 最终落到
scripts/event/mail.lua 的 ExecuteMail
- 当前
Game.exe 还存在 GCRetSendMail(329) 客户端处理包,已确认 1 字节结果码分支包括:
0:对方不是正式好友
1:你不是对方的正式好友
2:对方不在线
3:输入内容包含敏感字词
- 当前 Lua
worldcore:send_mail() 现状未见显式发送 GCRetSendMail 的逻辑,主链只做名字存在与正文非空检查。
服务端落点
- 协议定义:
/home/ubuntu/Game2/services/game/packet.lua
- 请求入口:
/home/ubuntu/Game2/services/msgagent.lua
- 世界服发信处理:
/home/ubuntu/Game2/services/world/worldcore.lua
- 脚本邮件执行:
/home/ubuntu/Game2/services/scene/scenecore.lua
/home/ubuntu/Game2/services/scripts/event/mail.lua
关键代码
当前 Lua 协议定义
packet.CGMail = {
xy_id = packet.XYID_CG_MAIL,
bis = function(self, buffer)
local stream = bistream.new()
stream:attach(buffer)
self.guid = stream:readint()
local source_size = stream:readuchar()
source_size = source_size >= 0x1E and 0x1E or source_size
self.source = gbk.toutf8(stream:read(source_size))
self.portrait_id = stream:readint()
self.unknow_2 = stream:readint()
local dest_size = stream:readuchar()
dest_size = dest_size >= 0x1E and 0x1E or dest_size
self.dest = gbk.toutf8(stream:read(dest_size))
local content_size = stream:readushort()
content_size = content_size >= 0x100 and 0x100 or content_size
self.content = gbk.toutf8(stream:read(content_size))
self.flag = stream:readuint()
self.create_time = stream:readuint()
if self.flag == 2 then
self.param_1 = stream:readint()
self.param_2 = stream:readint()
self.param_3 = stream:readint()
self.param_4 = stream:readint()
self.param_5 = stream:readint()
end
self.unknow_9 = stream:readuint()
end
}
当前 Lua 请求入口与世界服处理
function request:CGMail()
local my_guid = ma_func:get_guid()
assert(self.guid == my_guid, self.guid)
skynet.call(".world", "lua", "send_mail", self)
end
function worldcore:send_mail(mail)
if trim_notice_text(mail and mail.content) == "" then
skynet.loge("worldcore:send_mail skip empty content dest =", tostring(mail and mail.dest))
return
end
local dest = mail.dest
local user = self:get_db_user_by_name(mail.dest)
if user then
self:insert_mail(user.attrib.guid, mail)
local online_user = self:find_user_by_name(dest)
if online_user then
skynet.send(online_user.uinfo.agent, "lua", "new_mail")
end
else
print("can not find user name =", mail.dest)
end
end
当前 Lua 脚本邮件处理
local function get_db_mail_list()
...
for i = #mail_list, 1, -1 do
local mail = mail_list[i]
if mail.flag == define.MAIL_TYPE.MAIL_TYPE_SCRIPT then
execute_script_mail(mail)
else
table.insert(common_mail_list, mail)
end
end
return common_mail_list
end
function scenecore:execute_script_mail(who, mail)
self.scriptenginer:call(define.MAIL_SCRIPT_ID, "ExecuteMail", who, mail.param_1, mail.param_2, mail.param_3, mail.param_4, mail.param_5)
end
当前 Game.exe 的读写与执行结论
GetPacketID() = 414;
GetPacketSize() {
if (mail_type == 2) {
return source_len + dest_len + content_len + 48;
}
return source_len + dest_len + content_len + 28;
}
Read(stream) {
read_i32(sender_guid);
read_u8(source_len);
read_bytes(source, source_len);
read_i32(portrait_id);
read_i32(extra_guid);
read_u8(dest_len);
read_bytes(dest, dest_len);
read_u16(content_len);
read_bytes(content, content_len);
read_u32(mail_type);
read_u32(create_time);
if (mail_type == 2) {
read_i32(script_param_1);
read_i32(script_param_2);
read_i32(script_param_3);
read_i32(script_param_4);
read_i32(script_param_5);
}
read_u32(client_tag);
}
Write(stream) {
write_i32(sender_guid);
write_u8(source_len);
write_bytes(source, source_len);
write_i32(portrait_id);
write_i32(extra_guid);
write_u8(dest_len);
write_bytes(dest, dest_len);
write_u16(content_len);
write_bytes(content, content_len);
write_u32(mail_type);
write_u32(create_time);
if (mail_type == 2) {
write_i32(script_param_1);
write_i32(script_param_2);
write_i32(script_param_3);
write_i32(script_param_4);
write_i32(script_param_5);
}
write_u32(client_tag);
}
Execute() {
return 2;
}
结论
这条包在当前旧版项目里要记住七点:
CGMail 的封包 ID 是 414。
- 它是标准变长邮件发送包,主体由
sender_guid + source + portrait_id + extra_guid + dest + content + mail_type + create_time 组成。
- 当前
Game.exe 与 Lua 都确认三段字符串长度限制分别是:30 / 30 / 256。
- 当
mail_type == 2 时,这条包会额外携带 5 个脚本参数,并走脚本邮件执行链。
- 当前 Lua 请求入口会先硬校验
sender_guid 是否等于当前角色 GUID,再转发到世界服。
- 当前世界服发信逻辑的核心条件是“正文非空、收件人名字存在”,成功后写库并通知在线玩家。
- 当前客户端虽然保留了
GCRetSendMail(329) 失败提示处理,但当前 Lua 主链未见显式发送该回包的实现。